/* 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/. */

const Cm = Components.manager;
Cm.QueryInterface(Ci.nsIServiceManager);

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "BROWSER_STARTUP_RECORD",
  "browser.startup.record",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "BROWSER_STARTUP_RECORD_IMAGES",
  "browser.startup.recordImages",
  false
);

let firstPaintNotification = "widget-first-paint";
// On Linux widget-first-paint fires much later than expected and
// xul-window-visible fires too early for currently unknown reasons.
if (AppConstants.platform == "linux") {
  firstPaintNotification = "document-shown";
} else if (
  Services.prefs.getBoolPref("browser.startup.preXulSkeletonUI", false)
) {
  firstPaintNotification = "xul-window-visible";
}

let win, canvas;
let paints = [];
let afterPaintListener = () => {
  let startTime = Cu.now();
  let width, height;
  canvas.width = width = win.innerWidth;
  canvas.height = height = win.innerHeight;
  if (width < 1 || height < 1) {
    return;
  }
  let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });

  ctx.drawWindow(
    win,
    0,
    0,
    width,
    height,
    "white",
    ctx.DRAWWINDOW_DO_NOT_FLUSH |
      ctx.DRAWWINDOW_DRAW_VIEW |
      ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
      ctx.DRAWWINDOW_USE_WIDGET_LAYERS
  );
  paints.push({
    data: ctx.getImageData(0, 0, width, height).data,
    width,
    height,
  });
  ChromeUtils.addProfilerMarker(
    "startupRecorder",
    { category: "Test", startTime },
    `screenshot: ${width}x${height}px`
  );
};

/**
 * The StartupRecorder component observes notifications at various stages of
 * startup and records the set of JS modules that were already loaded at
 * each of these points.
 * The records are meant to be used by startup tests in
 * browser/base/content/test/performance
 * This component only exists in nightly and debug builds, it doesn't ship in
 * our release builds.
 */
export function StartupRecorder() {
  this.wrappedJSObject = this;
  this.data = {
    images: {
      "image-drawing": new Set(),
      "image-loading": new Set(),
    },
    code: {},
    prefStats: {},
  };
  this.done = new Promise(resolve => {
    this._resolve = resolve;
  });
}

StartupRecorder.prototype = {
  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  record(name) {
    ChromeUtils.addProfilerMarker(
      "startupRecorder",
      { category: "Test" },
      name
    );
    this.data.code[name] = {
      modules: Cu.loadedESModules,
      services: Object.keys(Cc).filter(c => {
        try {
          return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports);
        } catch (e) {
          return false;
        }
      }),
    };
  },

  observe(subject, topic, data) {
    if (topic == "app-startup" || topic == "content-process-ready-for-script") {
      // Don't do anything in xpcshell.
      if (Services.appinfo.ID != "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") {
        return;
      }

      if (!lazy.BROWSER_STARTUP_RECORD && !lazy.BROWSER_STARTUP_RECORD_IMAGES) {
        this._resolve();
        this._resolve = null;
        return;
      }

      // We can't ensure our observer will be called first or last, so the list of
      // topics we observe here should avoid the topics used to trigger things
      // during startup (eg. the topics observed by BrowserGlue.sys.mjs).
      let topics = [
        "profile-do-change", // This catches stuff loaded during app-startup
        "toplevel-window-ready", // Catches stuff from final-ui-startup
        firstPaintNotification,
        "sessionstore-windows-restored",
        "browser-startup-idle-tasks-finished",
      ];

      if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
        // For code simplicify, recording images excludes the other startup
        // recorder behaviors, so we can observe only the image topics.
        topics = [
          "image-loading",
          "image-drawing",
          "browser-startup-idle-tasks-finished",
        ];
      }
      for (let t of topics) {
        Services.obs.addObserver(this, t);
      }
      return;
    }

    // We only care about the first paint notification for browser windows, and
    // not other types (for example, the gfx sanity test window)
    if (topic == firstPaintNotification) {
      // In the case we're handling xul-window-visible, we'll have been handed
      // an nsIAppWindow instead of an nsIDOMWindow.
      if (subject instanceof Ci.nsIAppWindow) {
        subject = subject
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIDOMWindow);
      }

      // In the case we're handling document-shown, we'll have been handed
      // an HTMLDocument instead of an nsIDOMWindow.
      let doc = topic == "document-shown" ? subject : subject.document;

      if (
        doc.documentElement.getAttribute("windowtype") != "navigator:browser"
      ) {
        return;
      }
    }

    if (topic == "image-drawing" || topic == "image-loading") {
      this.data.images[topic].add(data);
      return;
    }

    Services.obs.removeObserver(this, topic);

    if (topic == firstPaintNotification) {
      // Because of the check for navigator:browser we made earlier, we know
      // that if we got here, then the subject must be the first browser window.
      win = topic == "document-shown" ? subject.defaultView : subject;
      canvas = win.document.createElementNS(
        "http://www.w3.org/1999/xhtml",
        "canvas"
      );
      canvas.mozOpaque = true;
      afterPaintListener();
      win.addEventListener("MozAfterPaint", afterPaintListener);
    }

    if (topic == "sessionstore-windows-restored") {
      // We use idleDispatchToMainThread here to record the set of
      // loaded scripts after we are fully done with startup and ready
      // to react to user events.
      Services.tm.dispatchToMainThread(
        this.record.bind(this, "before handling user events")
      );
    } else if (topic == "browser-startup-idle-tasks-finished") {
      if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
        Services.obs.removeObserver(this, "image-drawing");
        Services.obs.removeObserver(this, "image-loading");
        this._resolve();
        this._resolve = null;
        return;
      }

      this.record("before becoming idle");
      win.removeEventListener("MozAfterPaint", afterPaintListener);
      win = null;
      this.data.frames = paints;
      this.data.prefStats = {};
      if (AppConstants.DEBUG) {
        Services.prefs.readStats(
          (key, value) => (this.data.prefStats[key] = value)
        );
      }
      paints = null;

      if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) {
        this._resolve();
        this._resolve = null;
        return;
      }

      Services.profiler.getProfileDataAsync().then(profileData => {
        this.data.profile = profileData;
        // There's no equivalent StartProfiler call in this file because the
        // profiler is started using the MOZ_PROFILER_STARTUP environment
        // variable in browser/base/content/test/performance/browser.ini
        Services.profiler.StopProfiler();

        this._resolve();
        this._resolve = null;
      });
    } else {
      const topicsToNames = {
        "profile-do-change": "before profile selection",
        "toplevel-window-ready": "before opening first browser window",
      };
      topicsToNames[firstPaintNotification] = "before first paint";
      this.record(topicsToNames[topic]);
    }
  },
};
