/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

// Performance monitoring and calculation.

function round_up(val, interval) {
  return val + (interval - (val % interval));
}

// Class for inter-frame timing, which handles being paused and resumed.
var FrameTimer = class {
  constructor() {
    // Start time of the current active test, adjusted for any time spent
    // stopped (so `now - this.start` is how long the current active test
    // has run for.)
    this.start = undefined;

    // Timestamp of callback following the previous frame.
    this.prev = undefined;

    // Timestamp when drawing was paused, or zero if drawing is active.
    this.stopped = 0;
  }

  is_stopped() {
    return this.stopped != 0;
  }

  start_recording(now = gHost.now()) {
    this.start = this.prev = now;
  }

  on_frame_finished(now = gHost.now()) {
    const delay = now - this.prev;
    this.prev = now;
    return delay;
  }

  pause(now = gHost.now()) {
    this.stopped = now;
    // Abuse this.prev to store the time elapsed since the previous frame.
    // This will be used to adjust this.prev when we resume.
    this.prev = now - this.prev;
  }

  resume(now = gHost.now()) {
    this.prev = now - this.prev;
    const stop_duration = now - this.stopped;
    this.start += stop_duration;
    this.stopped = 0;
  }
};

// Per-frame time sampling infra.
var sampleTime = 16.666667; // ms
var sampleIndex = 0;

// Class for maintaining a rolling window of per-frame GC-related counters:
// inter-frame delay, minor/major/slice GC counts, cumulative bytes, etc.
var FrameHistory = class {
  constructor(numSamples) {
    // Private
    this._frameTimer = new FrameTimer();
    this._numSamples = numSamples;

    // Public API
    this.delays = new Array(numSamples);
    this.gcBytes = new Array(numSamples);
    this.mallocBytes = new Array(numSamples);
    this.gcs = new Array(numSamples);
    this.minorGCs = new Array(numSamples);
    this.majorGCs = new Array(numSamples);
    this.slices = new Array(numSamples);

    sampleIndex = 0;
    this.reset();
  }

  start(now = gHost.now()) {
    this._frameTimer.start_recording(now);
  }

  reset() {
    this.delays.fill(0);
    this.gcBytes.fill(0);
    this.mallocBytes.fill(0);
    this.gcs.fill(this.gcs[sampleIndex]);
    this.minorGCs.fill(this.minorGCs[sampleIndex]);
    this.majorGCs.fill(this.majorGCs[sampleIndex]);
    this.slices.fill(this.slices[sampleIndex]);

    sampleIndex = 0;
  }

  get numSamples() {
    return this._numSamples;
  }

  findMax(collection) {
    // Depends on having at least one non-negative entry, and unfilled
    // entries being <= max.
    var maxIndex = 0;
    for (let i = 0; i < this._numSamples; i++) {
      if (collection[i] >= collection[maxIndex]) {
        maxIndex = i;
      }
    }
    return maxIndex;
  }

  findMaxDelay() {
    return this.findMax(this.delays);
  }

  on_frame(now = gHost.now()) {
    const delay = this._frameTimer.on_frame_finished(now);

    // Total time elapsed while the active test has been running.
    var t = now - this._frameTimer.start;
    var newIndex = Math.round(t / sampleTime);
    while (sampleIndex < newIndex) {
      sampleIndex++;
      var idx = sampleIndex % this._numSamples;
      this.delays[idx] = delay;
      if (gHost.features.haveMemorySizes) {
        this.gcBytes[idx] = gHost.gcBytes;
        this.mallocBytes[idx] = gHost.mallocBytes;
      }
      if (gHost.features.haveGCCounts) {
        this.minorGCs[idx] = gHost.minorGCCount;
        this.majorGCs[idx] = gHost.majorGCCount;
        this.slices[idx] = gHost.GCSliceCount;
      }
    }

    return delay;
  }

  pause() {
    this._frameTimer.pause();
  }

  resume() {
    this._frameTimer.resume();
  }

  is_stopped() {
    return this._frameTimer.is_stopped();
  }
};

var PerfTracker = class {
  constructor() {
    // Private
    this._currentLoadStart = undefined;
    this._frameCount = undefined;
    this._mutating_ms = undefined;
    this._suspend_sec = undefined;
    this._minorGCs = undefined;
    this._majorGCs = undefined;

    // Public
    this.results = [];
  }

  on_load_start(load, now = gHost.now()) {
    this._currentLoadStart = now;
    this._frameCount = 0;
    this._mutating_ms = 0;
    this._suspend_sec = 0;
    this._majorGCs = gHost.majorGCCount;
    this._minorGCs = gHost.minorGCCount;
  }

  on_load_end(load, now = gHost.now()) {
    const elapsed_time = (now - this._currentLoadStart) / 1000;
    const full_time = round_up(elapsed_time, 1 / 60);
    const frame_60fps_limit = Math.round(full_time * 60);
    const dropped_60fps_frames = frame_60fps_limit - this._frameCount;
    const dropped_60fps_fraction = dropped_60fps_frames / frame_60fps_limit;

    const mutating_and_gc_fraction = this._mutating_ms / (full_time * 1000);

    this.results.push({
      load,
      elapsed_time,
      mutating: this._mutating_ms / 1000,
      mutating_and_gc_fraction,
      suspended: this._suspend_sec,
      full_time,
      frames: this._frameCount,
      dropped_60fps_frames,
      dropped_60fps_fraction,
      majorGCs: gHost.majorGCCount - this._majorGCs,
      minorGCs: gHost.minorGCCount - this._minorGCs,
    });
    this._currentLoadStart = undefined;
    this._frameCount = 0;
  }

  after_suspend(wait_sec) {
    this._suspend_sec += wait_sec;
  }

  before_mutator(now = gHost.now()) {
    this._frameCount++;
  }

  after_mutator(start_time, end_time = gHost.now()) {
    // Warning: this is called before on_load_start is called from
    // handle_tick_events.
  }

  handle_tick_events(events, loadMgr, tick_start, tick_end) {
    // When the load manager switches from one load to another, there will be
    // the tick start, the load is switched, the new load runs for a bit, then
    // the tick end. Associate the timestamp of the start of the tick with both
    // the end of the previous load and the beginning of the new one.
    let load_running = true;
    if (events & loadMgr.LOAD_ENDED) {
      this.on_load_end(loadMgr.lastActive, tick_start);
      load_running = false;
    }
    if (events & loadMgr.LOAD_STARTED) {
      this.on_load_start(loadMgr.active, tick_start);
      load_running = true;
    }

    if (load_running) {
      this._mutating_ms += tick_end - tick_start;
    }
  }
};
