tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

StartupRecorder.sys.mjs (7752B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const Cm = Components.manager;
      6 Cm.QueryInterface(Ci.nsIServiceManager);
      7 
      8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     10 
     11 const lazy = {};
     12 
     13 XPCOMUtils.defineLazyPreferenceGetter(
     14  lazy,
     15  "BROWSER_STARTUP_RECORD",
     16  "browser.startup.record",
     17  false
     18 );
     19 
     20 XPCOMUtils.defineLazyPreferenceGetter(
     21  lazy,
     22  "BROWSER_STARTUP_RECORD_IMAGES",
     23  "browser.startup.recordImages",
     24  false
     25 );
     26 
     27 let firstPaintNotification = "xul-window-visible";
     28 // On Linux widget-first-paint fires much later than expected and
     29 // xul-window-visible fires too early for currently unknown reasons.
     30 if (AppConstants.platform == "linux") {
     31  firstPaintNotification = "document-shown";
     32 }
     33 
     34 let win, canvas;
     35 let paints = [];
     36 let afterPaintListener = () => {
     37  let startTime = ChromeUtils.now();
     38  let width, height;
     39  canvas.width = width = win.innerWidth;
     40  canvas.height = height = win.innerHeight;
     41  if (width < 1 || height < 1) {
     42    return;
     43  }
     44  let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
     45 
     46  ctx.drawWindow(
     47    win,
     48    0,
     49    0,
     50    width,
     51    height,
     52    "white",
     53    ctx.DRAWWINDOW_DO_NOT_FLUSH |
     54      ctx.DRAWWINDOW_DRAW_VIEW |
     55      ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
     56      ctx.DRAWWINDOW_USE_WIDGET_LAYERS
     57  );
     58  paints.push({
     59    data: ctx.getImageData(0, 0, width, height).data,
     60    width,
     61    height,
     62  });
     63  ChromeUtils.addProfilerMarker(
     64    "startupRecorder",
     65    { category: "Test", startTime },
     66    `screenshot: ${width}x${height}px`
     67  );
     68 };
     69 
     70 /**
     71 * The StartupRecorder component observes notifications at various stages of
     72 * startup and records the set of JS modules that were already loaded at
     73 * each of these points.
     74 * The records are meant to be used by startup tests in
     75 * browser/base/content/test/performance
     76 * This component only exists in nightly and debug builds, it doesn't ship in
     77 * our release builds.
     78 */
     79 export function StartupRecorder() {
     80  this.wrappedJSObject = this;
     81  this.data = {
     82    images: {
     83      "image-drawing": new Set(),
     84      "image-loading": new Set(),
     85    },
     86    code: {},
     87    prefStats: {},
     88  };
     89  this.done = new Promise(resolve => {
     90    this._resolve = resolve;
     91  });
     92 }
     93 
     94 StartupRecorder.prototype = {
     95  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
     96 
     97  record(name) {
     98    ChromeUtils.addProfilerMarker(
     99      "startupRecorder",
    100      { category: "Test" },
    101      name
    102    );
    103    this.data.code[name] = {
    104      modules: Cu.loadedESModules,
    105      services: Object.keys(Cc).filter(c => {
    106        try {
    107          return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports);
    108        } catch (e) {
    109          return false;
    110        }
    111      }),
    112    };
    113  },
    114 
    115  observe(subject, topic, data) {
    116    if (topic == "app-startup" || topic == "content-process-ready-for-script") {
    117      // Don't do anything in xpcshell.
    118      if (Services.appinfo.ID != "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") {
    119        return;
    120      }
    121 
    122      if (!lazy.BROWSER_STARTUP_RECORD && !lazy.BROWSER_STARTUP_RECORD_IMAGES) {
    123        this._resolve();
    124        this._resolve = null;
    125        return;
    126      }
    127 
    128      // We can't ensure our observer will be called first or last, so the list of
    129      // topics we observe here should avoid the topics used to trigger things
    130      // during startup (eg. the topics observed by BrowserGlue.sys.mjs).
    131      let topics = [
    132        "profile-do-change", // This catches stuff loaded during app-startup
    133        "toplevel-window-ready", // Catches stuff from final-ui-startup
    134        firstPaintNotification,
    135        "sessionstore-windows-restored",
    136        "browser-startup-idle-tasks-finished",
    137      ];
    138 
    139      if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
    140        // For code simplicify, recording images excludes the other startup
    141        // recorder behaviors, so we can observe only the image topics.
    142        topics = [
    143          "image-loading",
    144          "image-drawing",
    145          "browser-startup-idle-tasks-finished",
    146        ];
    147      }
    148      for (let t of topics) {
    149        Services.obs.addObserver(this, t);
    150      }
    151      return;
    152    }
    153 
    154    // We only care about the first paint notification for browser windows, and
    155    // not other types (for example, the gfx sanity test window)
    156    if (topic == firstPaintNotification) {
    157      // In the case we're handling xul-window-visible, we'll have been handed
    158      // an nsIAppWindow instead of an nsIDOMWindow.
    159      if (subject instanceof Ci.nsIAppWindow) {
    160        subject = subject
    161          .QueryInterface(Ci.nsIInterfaceRequestor)
    162          .getInterface(Ci.nsIDOMWindow);
    163      }
    164 
    165      // In the case we're handling document-shown, we'll have been handed
    166      // an HTMLDocument instead of an nsIDOMWindow.
    167      let doc = topic == "document-shown" ? subject : subject.document;
    168 
    169      if (
    170        doc.documentElement.getAttribute("windowtype") != "navigator:browser"
    171      ) {
    172        return;
    173      }
    174    }
    175 
    176    if (topic == "image-drawing" || topic == "image-loading") {
    177      this.data.images[topic].add(data);
    178      return;
    179    }
    180 
    181    Services.obs.removeObserver(this, topic);
    182 
    183    if (topic == firstPaintNotification) {
    184      // Because of the check for navigator:browser we made earlier, we know
    185      // that if we got here, then the subject must be the first browser window.
    186      win = topic == "document-shown" ? subject.defaultView : subject;
    187      canvas = win.document.createElementNS(
    188        "http://www.w3.org/1999/xhtml",
    189        "canvas"
    190      );
    191      canvas.mozOpaque = true;
    192      afterPaintListener();
    193      win.addEventListener("MozAfterPaint", afterPaintListener);
    194    }
    195 
    196    if (topic == "sessionstore-windows-restored") {
    197      // We use idleDispatchToMainThread here to record the set of
    198      // loaded scripts after we are fully done with startup and ready
    199      // to react to user events.
    200      Services.tm.dispatchToMainThread(
    201        this.record.bind(this, "before handling user events")
    202      );
    203    } else if (topic == "browser-startup-idle-tasks-finished") {
    204      if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
    205        Services.obs.removeObserver(this, "image-drawing");
    206        Services.obs.removeObserver(this, "image-loading");
    207        this._resolve();
    208        this._resolve = null;
    209        return;
    210      }
    211 
    212      this.record("before becoming idle");
    213      win.removeEventListener("MozAfterPaint", afterPaintListener);
    214      win = null;
    215      this.data.frames = paints;
    216      this.data.prefStats = {};
    217      if (AppConstants.DEBUG) {
    218        Services.prefs.readStats(
    219          (key, value) => (this.data.prefStats[key] = value)
    220        );
    221      }
    222      paints = null;
    223 
    224      if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) {
    225        this._resolve();
    226        this._resolve = null;
    227        return;
    228      }
    229 
    230      Services.profiler.getProfileDataAsync().then(profileData => {
    231        this.data.profile = profileData;
    232        // There's no equivalent StartProfiler call in this file because the
    233        // profiler is started using the MOZ_PROFILER_STARTUP environment
    234        // variable in browser/base/content/test/performance/browser.toml
    235        Services.profiler.StopProfiler();
    236 
    237        this._resolve();
    238        this._resolve = null;
    239      });
    240    } else {
    241      const topicsToNames = {
    242        "profile-do-change": "before profile selection",
    243        "toplevel-window-ready": "before opening first browser window",
    244      };
    245      topicsToNames[firstPaintNotification] = "before first paint";
    246      this.record(topicsToNames[topic]);
    247    }
    248  },
    249 };