tor-browser

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

AboutHomeStartupCache.sys.mjs (30172B)


      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 let lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
      8  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
      9  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     10  HomePage: "resource:///modules/HomePage.sys.mjs",
     11  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     12  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     13  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     14 });
     15 
     16 /**
     17 * AboutHomeStartupCache is responsible for reading and writing the
     18 * initial about:home document from the HTTP cache as a startup
     19 * performance optimization. It only works when the "privileged about
     20 * content process" is enabled and when ENABLED_PREF is set to true.
     21 *
     22 * See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html
     23 * for further details.
     24 */
     25 export var AboutHomeStartupCache = {
     26  ABOUT_HOME_URI_STRING: "about:home",
     27  SCRIPT_EXTENSION: "script",
     28  ENABLED_PREF: "browser.startup.homepage.abouthome_cache.enabled",
     29  PRELOADED_NEWTAB_PREF: "browser.newtab.preload",
     30  LOG_LEVEL_PREF: "browser.startup.homepage.abouthome_cache.loglevel",
     31 
     32  // It's possible that the layout of about:home will change such that
     33  // we want to invalidate any pre-existing caches. We do this by setting
     34  // this meta key in the nsICacheEntry for the page.
     35  //
     36  // The version is currently set to the build ID, meaning that the cache
     37  // is invalidated after every upgrade (like the main startup cache).
     38  CACHE_VERSION_META_KEY: "version",
     39 
     40  LOG_NAME: "AboutHomeStartupCache",
     41 
     42  // These messages are used to request the "privileged about content process"
     43  // to create the cached document, and then to receive that document.
     44  CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest",
     45  CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse",
     46  CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult",
     47 
     48  // When a "privileged about content process" is launched, this message is
     49  // sent to give it some nsIInputStream's for the about:home document they
     50  // should load.
     51  SEND_STREAMS_MESSAGE: "AboutHomeStartupCache:InputStreams",
     52 
     53  // This time in ms is used to debounce messages that are broadcast to
     54  // all about:newtab's, or the preloaded about:newtab. We use those
     55  // messages as a signal that it's likely time to refresh the cache.
     56  CACHE_DEBOUNCE_RATE_MS: 5000,
     57 
     58  // This is how long we'll block the AsyncShutdown while waiting for
     59  // the cache to write. If we fail to write within that time, we will
     60  // allow the shutdown to proceed.
     61  SHUTDOWN_CACHE_WRITE_TIMEOUT_MS: 1000,
     62 
     63  // The following values are as possible values for the
     64  // browser.startup.abouthome_cache_result scalar. Keep these in sync with the
     65  // scalar definition in Scalars.yaml and the matching Glean metric in
     66  // browser/components/metrics.yaml. See setDeferredResult for more
     67  // information.
     68  CACHE_RESULT_SCALARS: {
     69    UNSET: 0,
     70    DOES_NOT_EXIST: 1,
     71    CORRUPT_PAGE: 2,
     72    CORRUPT_SCRIPT: 3,
     73    INVALIDATED: 4,
     74    LATE: 5,
     75    VALID_AND_USED: 6,
     76    DISABLED: 7,
     77    NOT_LOADING_ABOUTHOME: 8,
     78    PRELOADING_DISABLED: 9,
     79  },
     80 
     81  // This will be set to one of the values of CACHE_RESULT_SCALARS
     82  // once it is determined which result best suits what occurred.
     83  _cacheDeferredResultScalar: -1,
     84 
     85  // A reference to the nsICacheEntry to read from and write to.
     86  _cacheEntry: null,
     87 
     88  // These nsIPipe's are sent down to the "privileged about content process"
     89  // immediately after the process launches. This allows us to race the loading
     90  // of the cache entry in the parent process with the load of the about:home
     91  // page in the content process, since we'll connect the InputStream's to
     92  // the pipes as soon as the nsICacheEntry is available.
     93  //
     94  // The page pipe is for the HTML markup for the page.
     95  _pagePipe: null,
     96  // The script pipe is for the JavaScript that the HTML markup loads
     97  // to set its internal state.
     98  _scriptPipe: null,
     99  _cacheDeferred: null,
    100 
    101  _enabled: false,
    102  _initted: false,
    103  _hasWrittenThisSession: false,
    104  _finalized: false,
    105  _firstPrivilegedProcessCreated: false,
    106 
    107  init() {
    108    if (this._initted) {
    109      throw new Error("AboutHomeStartupCache already initted.");
    110    }
    111 
    112    if (
    113      Services.startup.isInOrBeyondShutdownPhase(
    114        Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
    115      )
    116    ) {
    117      // Stay not initted, such that using us will reject or be a no-op.
    118      return;
    119    }
    120 
    121    this.setDeferredResult(this.CACHE_RESULT_SCALARS.UNSET);
    122 
    123    this._enabled = Services.prefs.getBoolPref(
    124      "browser.startup.homepage.abouthome_cache.enabled"
    125    );
    126 
    127    if (!this._enabled) {
    128      this.recordResult(this.CACHE_RESULT_SCALARS.DISABLED);
    129      return;
    130    }
    131 
    132    this.log = console.createInstance({
    133      prefix: this.LOG_NAME,
    134      maxLogLevelPref: this.LOG_LEVEL_PREF,
    135    });
    136 
    137    this.log.trace("Initting.");
    138 
    139    // If the user is not configured to load about:home at startup, then
    140    // let's not bother with the cache - loading it needlessly is more likely
    141    // to hinder what we're actually trying to load.
    142    let willLoadAboutHome =
    143      !lazy.HomePage.overridden &&
    144      Services.prefs.getIntPref("browser.startup.page") === 1;
    145 
    146    if (!willLoadAboutHome) {
    147      this.log.trace("Not configured to load about:home by default.");
    148      this.recordResult(this.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME);
    149      return;
    150    }
    151 
    152    if (!Services.prefs.getBoolPref(this.PRELOADED_NEWTAB_PREF, false)) {
    153      this.log.trace("Preloaded about:newtab disabled.");
    154      this.recordResult(this.CACHE_RESULT_SCALARS.PRELOADING_DISABLED);
    155      return;
    156    }
    157 
    158    Services.obs.addObserver(this, "ipc:content-created");
    159    Services.obs.addObserver(this, "process-type-set");
    160    Services.obs.addObserver(this, "ipc:content-shutdown");
    161    Services.obs.addObserver(this, "intl:app-locales-changed");
    162 
    163    this.log.trace("Constructing pipes.");
    164    this._pagePipe = this.makePipe();
    165    this._scriptPipe = this.makePipe();
    166 
    167    this._cacheEntryPromise = new Promise(resolve => {
    168      this._cacheEntryResolver = resolve;
    169    });
    170 
    171    let lci = Services.loadContextInfo.default;
    172    let storage = Services.cache2.diskCacheStorage(lci);
    173    try {
    174      storage.asyncOpenURI(
    175        this.aboutHomeURI,
    176        "",
    177        Ci.nsICacheStorage.OPEN_PRIORITY,
    178        this
    179      );
    180    } catch (e) {
    181      this.log.error("Failed to open about:home cache entry", e);
    182    }
    183 
    184    this._cacheTask = new lazy.DeferredTask(async () => {
    185      await this.cacheNow();
    186    }, this.CACHE_DEBOUNCE_RATE_MS);
    187 
    188    this._shutdownBlocker = async () => {
    189      await this.onShutdown();
    190    };
    191 
    192    lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
    193      "AboutHomeStartupCache: Writing cache",
    194      this._shutdownBlocker,
    195      () => this._cacheProgress
    196    );
    197 
    198    this._cacheDeferred = null;
    199    this._initted = true;
    200    this.log.trace("Initialized.");
    201  },
    202 
    203  get initted() {
    204    return this._initted;
    205  },
    206 
    207  uninit() {
    208    if (!this._enabled) {
    209      return;
    210    }
    211 
    212    try {
    213      Services.obs.removeObserver(this, "ipc:content-created");
    214      Services.obs.removeObserver(this, "process-type-set");
    215      Services.obs.removeObserver(this, "ipc:content-shutdown");
    216      Services.obs.removeObserver(this, "intl:app-locales-changed");
    217    } catch (e) {
    218      // If we failed to initialize and register for these observer
    219      // notifications, then attempting to remove them will throw.
    220      // It's fine to ignore that case on shutdown.
    221    }
    222 
    223    if (this._cacheTask) {
    224      this._cacheTask.disarm();
    225      this._cacheTask = null;
    226    }
    227 
    228    this._pagePipe = null;
    229    this._scriptPipe = null;
    230    this._initted = false;
    231    this._cacheEntry = null;
    232    this._hasWrittenThisSession = false;
    233    this._cacheEntryPromise = null;
    234    this._cacheEntryResolver = null;
    235    this._cacheDeferredResultScalar = -1;
    236 
    237    if (this.log) {
    238      this.log.trace("Uninitialized.");
    239      this.log = null;
    240    }
    241 
    242    this._procManager = null;
    243    this._procManagerID = null;
    244    this._appender = null;
    245    this._cacheDeferred = null;
    246    this._finalized = false;
    247    this._firstPrivilegedProcessCreated = false;
    248 
    249    lazy.AsyncShutdown.appShutdownConfirmed.removeBlocker(
    250      this._shutdownBlocker
    251    );
    252    this._shutdownBlocker = null;
    253  },
    254 
    255  _aboutHomeURI: null,
    256 
    257  get aboutHomeURI() {
    258    if (this._aboutHomeURI) {
    259      return this._aboutHomeURI;
    260    }
    261 
    262    this._aboutHomeURI = Services.io.newURI(this.ABOUT_HOME_URI_STRING);
    263    return this._aboutHomeURI;
    264  },
    265 
    266  // For the AsyncShutdown blocker, this is used to populate the progress
    267  // value.
    268  _cacheProgress: "Not yet begun",
    269 
    270  /**
    271   * Called by the AsyncShutdown blocker on quit-application
    272   * to potentially flush the most recent cache to disk. If one was
    273   * never written during the session, one is generated and written
    274   * before the async function resolves.
    275   *
    276   * @param {boolean} withTimeout
    277   *   Whether or not the timeout mechanism should be used. Defaults
    278   *   to true.
    279   * @returns {Promise<boolean>}
    280   *   If a cache has never been written, or a cache write is in
    281   *   progress, resolves true when the cache has been written. Also
    282   *   resolves to true if a cache didn't need to be written.
    283   *
    284   *   Resolves to false if a cache write unexpectedly timed out.
    285   */
    286  async onShutdown(withTimeout = true) {
    287    // If we never wrote this session, arm the task so that the next
    288    // step can finalize.
    289    if (!this._hasWrittenThisSession) {
    290      this.log.trace("Never wrote a cache this session. Arming cache task.");
    291      this._cacheTask.arm();
    292    }
    293 
    294    Glean.browserStartup.abouthomeCacheShutdownwrite.set(
    295      this._cacheTask.isArmed
    296    );
    297 
    298    if (this._cacheTask.isArmed) {
    299      this.log.trace("Finalizing cache task on shutdown");
    300      this._finalized = true;
    301 
    302      // To avoid hanging shutdowns, we'll ensure that we wait a maximum of
    303      // SHUTDOWN_CACHE_WRITE_TIMEOUT_MS millseconds before giving up.
    304      const TIMED_OUT = Symbol();
    305      let timeoutID = 0;
    306 
    307      let timeoutPromise = new Promise(resolve => {
    308        timeoutID = lazy.setTimeout(
    309          () => resolve(TIMED_OUT),
    310          this.SHUTDOWN_CACHE_WRITE_TIMEOUT_MS
    311        );
    312      });
    313 
    314      let promises = [this._cacheTask.finalize()];
    315      if (withTimeout) {
    316        this.log.trace("Using timeout mechanism.");
    317        promises.push(timeoutPromise);
    318      } else {
    319        this.log.trace("Skipping timeout mechanism.");
    320      }
    321 
    322      let result = await Promise.race(promises);
    323      this.log.trace("Done blocking shutdown.");
    324      lazy.clearTimeout(timeoutID);
    325      if (result === TIMED_OUT) {
    326        this.log.error("Timed out getting cache streams. Skipping cache task.");
    327        return false;
    328      }
    329    }
    330    this.log.trace("onShutdown is exiting");
    331    return true;
    332  },
    333 
    334  /**
    335   * Called by the _cacheTask DeferredTask to actually do the work of
    336   * caching the about:home document.
    337   *
    338   * @returns {Promise<undefined>}
    339   *   Resolves when a fresh version of the cache has been written.
    340   */
    341  async cacheNow() {
    342    this.log.trace("Caching now.");
    343    this._cacheProgress = "Getting cache streams";
    344 
    345    let { pageInputStream, scriptInputStream } = await this.requestCache();
    346 
    347    if (!pageInputStream || !scriptInputStream) {
    348      this.log.trace("Failed to get cache streams.");
    349      this._cacheProgress = "Failed to get streams";
    350      return;
    351    }
    352 
    353    this.log.trace("Got cache streams.");
    354 
    355    this._cacheProgress = "Writing to cache";
    356 
    357    try {
    358      this.log.trace("Populating cache.");
    359      await this.populateCache(pageInputStream, scriptInputStream);
    360    } catch (e) {
    361      this._cacheProgress = "Failed to populate cache";
    362      this.log.error("Populating the cache failed: ", e);
    363      return;
    364    }
    365 
    366    this._cacheProgress = "Done";
    367    this.log.trace("Done writing to cache.");
    368    this._hasWrittenThisSession = true;
    369  },
    370 
    371  /**
    372   * Requests the cached document streams from the "privileged about content
    373   * process".
    374   *
    375   * @returns {Promise<object>}
    376   *   Resolves with an Object with the following properties:
    377   *
    378   *   pageInputStream (nsIInputStream)
    379   *     The page content to write to the cache, or null if request the streams
    380   *     failed.
    381   *
    382   *   scriptInputStream (nsIInputStream)
    383   *     The script content to write to the cache, or null if request the streams
    384   *     failed.
    385   */
    386  requestCache() {
    387    this.log.trace("Parent is requesting Activity Stream state object.");
    388 
    389    if (!this._initted) {
    390      this.log.error("requestCache called despite not initted!");
    391      return { pageInputStream: null, scriptInputStream: null };
    392    }
    393 
    394    if (!this._procManager) {
    395      this.log.error("requestCache called with no _procManager!");
    396      return { pageInputStream: null, scriptInputStream: null };
    397    }
    398 
    399    if (
    400      this._procManager.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
    401    ) {
    402      this.log.error("Somehow got the wrong process type.");
    403      return { pageInputStream: null, scriptInputStream: null };
    404    }
    405 
    406    this.log.error("Activity Stream is disabled.");
    407    return { pageInputStream: null, scriptInputStream: null };
    408  },
    409 
    410  /**
    411   * Helper function that returns a newly constructed nsIPipe instance.
    412   *
    413   * @returns {nsIPipe}
    414   */
    415  makePipe() {
    416    let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
    417    pipe.init(
    418      true /* non-blocking input */,
    419      true /* non-blocking output */,
    420      0 /* segment size */,
    421      0 /* max segments */
    422    );
    423    return pipe;
    424  },
    425 
    426  get pagePipe() {
    427    return this._pagePipe;
    428  },
    429 
    430  get scriptPipe() {
    431    return this._scriptPipe;
    432  },
    433 
    434  /**
    435   * Called when the nsICacheEntry has been accessed. If the nsICacheEntry
    436   * has content that we want to send down to the "privileged about content
    437   * process", then we connect that content to the nsIPipe's that may or
    438   * may not have already been sent down to the process.
    439   *
    440   * In the event that the nsICacheEntry doesn't contain anything usable,
    441   * the nsInputStreams on the nsIPipe's are closed.
    442   */
    443  connectToPipes() {
    444    this.log.trace(`Connecting nsICacheEntry to pipes.`);
    445 
    446    // If the cache doesn't yet exist, we'll know because the version metadata
    447    // won't exist yet.
    448    let version;
    449    try {
    450      this.log.trace("");
    451      version = this._cacheEntry.getMetaDataElement(
    452        this.CACHE_VERSION_META_KEY
    453      );
    454    } catch (e) {
    455      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
    456        this.log.debug("Cache meta data does not exist. Closing streams.");
    457        this.pagePipe.outputStream.close();
    458        this.scriptPipe.outputStream.close();
    459        this.setDeferredResult(this.CACHE_RESULT_SCALARS.DOES_NOT_EXIST);
    460        return;
    461      }
    462 
    463      throw e;
    464    }
    465 
    466    this.log.info("Version retrieved is", version);
    467 
    468    if (version != Services.appinfo.appBuildID) {
    469      this.log.info("Version does not match! Dooming and closing streams.\n");
    470      // This cache is no good - doom it, and prepare for a new one.
    471      this.clearCache();
    472      this.pagePipe.outputStream.close();
    473      this.scriptPipe.outputStream.close();
    474      this.setDeferredResult(this.CACHE_RESULT_SCALARS.INVALIDATED);
    475      return;
    476    }
    477 
    478    let cachePageInputStream;
    479 
    480    try {
    481      cachePageInputStream = this._cacheEntry.openInputStream(0);
    482    } catch (e) {
    483      this.log.error("Failed to open main input stream for cache entry", e);
    484      this.pagePipe.outputStream.close();
    485      this.scriptPipe.outputStream.close();
    486      this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_PAGE);
    487      return;
    488    }
    489 
    490    this.log.trace("Connecting page stream to pipe.");
    491    lazy.NetUtil.asyncCopy(
    492      cachePageInputStream,
    493      this.pagePipe.outputStream,
    494      () => {
    495        this.log.info("Page stream connected to pipe.");
    496      }
    497    );
    498 
    499    let cacheScriptInputStream;
    500    try {
    501      this.log.trace("Connecting script stream to pipe.");
    502      cacheScriptInputStream =
    503        this._cacheEntry.openAlternativeInputStream("script");
    504      lazy.NetUtil.asyncCopy(
    505        cacheScriptInputStream,
    506        this.scriptPipe.outputStream,
    507        () => {
    508          this.log.info("Script stream connected to pipe.");
    509        }
    510      );
    511    } catch (e) {
    512      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
    513        // For some reason, the script was not available. We'll close the pipe
    514        // without sending anything into it. The privileged about content process
    515        // will notice that there's nothing available in the pipe, and fall back
    516        // to dynamically generating the page.
    517        this.log.error("Script stream not available! Closing pipe.");
    518        this.scriptPipe.outputStream.close();
    519        this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_SCRIPT);
    520      } else {
    521        throw e;
    522      }
    523    }
    524 
    525    this.setDeferredResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED);
    526    this.log.trace("Streams connected to pipes.");
    527  },
    528 
    529  /**
    530   * Called when we have received a the cache values from the "privileged
    531   * about content process". The page and script streams are written to
    532   * the nsICacheEntry.
    533   *
    534   * This writing is asynchronous, and if a write happens to already be
    535   * underway when this function is called, that latter call will be
    536   * ignored.
    537   *
    538   * @param {nsIInputStream} pageInputStream
    539   *   A stream containing the HTML markup to be saved to the cache.
    540   * @param {nsIInputStream} scriptInputStream
    541   *   A stream containing the JS hydration script to be saved to the cache.
    542   * @returns {Promise<undefined, Error>}
    543   *   When the cache has been successfully written to.
    544 
    545   *   Rejects with a JS Error if writing any part of the cache happens to
    546   *   fail.
    547   */
    548  async populateCache(pageInputStream, scriptInputStream) {
    549    await this.ensureCacheEntry();
    550 
    551    await new Promise((resolve, reject) => {
    552      // Doom the old cache entry, so we can start writing to a new one.
    553      this.log.trace("Populating the cache. Dooming old entry.");
    554      this.clearCache();
    555 
    556      this.log.trace("Opening the page output stream.");
    557      let pageOutputStream;
    558      try {
    559        pageOutputStream = this._cacheEntry.openOutputStream(0, -1);
    560      } catch (e) {
    561        reject(e);
    562        return;
    563      }
    564 
    565      this.log.info("Writing the page cache.");
    566      lazy.NetUtil.asyncCopy(pageInputStream, pageOutputStream, pageResult => {
    567        if (!Components.isSuccessCode(pageResult)) {
    568          this.log.error("Failed to write page. Result: " + pageResult);
    569          reject(new Error(pageResult));
    570          return;
    571        }
    572 
    573        this.log.trace(
    574          "Writing the page data is complete. Now opening the " +
    575            "script output stream."
    576        );
    577 
    578        let scriptOutputStream;
    579        try {
    580          scriptOutputStream = this._cacheEntry.openAlternativeOutputStream(
    581            "script",
    582            -1
    583          );
    584        } catch (e) {
    585          reject(e);
    586          return;
    587        }
    588 
    589        this.log.info("Writing the script cache.");
    590        lazy.NetUtil.asyncCopy(
    591          scriptInputStream,
    592          scriptOutputStream,
    593          scriptResult => {
    594            if (!Components.isSuccessCode(scriptResult)) {
    595              this.log.error("Failed to write script. Result: " + scriptResult);
    596              reject(new Error(scriptResult));
    597              return;
    598            }
    599 
    600            this.log.trace(
    601              "Writing the script cache is done. Setting version."
    602            );
    603            try {
    604              this._cacheEntry.setMetaDataElement(
    605                "version",
    606                Services.appinfo.appBuildID
    607              );
    608            } catch (e) {
    609              this.log.error("Failed to write version.");
    610              reject(e);
    611              return;
    612            }
    613            this.log.trace(`Version is set to ${Services.appinfo.appBuildID}.`);
    614            this.log.info("Caching of page and script is done.");
    615            resolve();
    616          }
    617        );
    618      });
    619    });
    620 
    621    this.log.trace("populateCache has finished.");
    622  },
    623 
    624  /**
    625   * Returns a Promise that resolves once the nsICacheEntry for the cache
    626   * is available to write to and read from.
    627   *
    628   * @returns {Promise<nsICacheEntry, string>}
    629   *   Resolves once the cache entry has become available.
    630   *
    631   *   Rejects with an error message if getting the cache entry is attempted
    632   *   before the AboutHomeStartupCache component has been initialized.
    633   */
    634  ensureCacheEntry() {
    635    if (!this._initted) {
    636      return Promise.reject(
    637        "Cannot ensureCacheEntry - AboutHomeStartupCache is not initted"
    638      );
    639    }
    640 
    641    return this._cacheEntryPromise;
    642  },
    643 
    644  /**
    645   * Clears the contents of the cache.
    646   */
    647  clearCache() {
    648    this.log.trace("Clearing the cache.");
    649    this._cacheEntry = this._cacheEntry.recreate();
    650    this._cacheEntryPromise = new Promise(resolve => {
    651      resolve(this._cacheEntry);
    652    });
    653    this._hasWrittenThisSession = false;
    654  },
    655 
    656  /**
    657   * Clears the contents of the cache, and then completely uninitializes the
    658   * AboutHomeStartupCache caching mechanism until the next time it's
    659   * initialized (which outside of testing scenarios, is the next browser
    660   * start).
    661   */
    662  clearCacheAndUninit() {
    663    if (this._enabled && this.initted) {
    664      this.log.trace("Clearing the cache and uninitializing.");
    665      this.clearCache();
    666      this.uninit();
    667    }
    668  },
    669 
    670  /**
    671   * Called when a content process is created. If this is the "privileged
    672   * about content process", then the cache streams will be sent to it.
    673   *
    674   * @param {number} childID
    675   *   The unique ID for the content process that was created, as passed by
    676   *   ipc:content-created.
    677   * @param {ProcessMessageManager} procManager
    678   *   The ProcessMessageManager for the created content process.
    679   * @param {nsIDOMProcessParent} processParent
    680   *   The nsIDOMProcessParent for the tab.
    681   */
    682  onContentProcessCreated(childID, procManager, processParent) {
    683    if (procManager.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) {
    684      if (this._finalized) {
    685        this.log.trace(
    686          "Ignoring privileged about content process launch after finalization."
    687        );
    688        return;
    689      }
    690 
    691      if (this._firstPrivilegedProcessCreated) {
    692        this.log.trace(
    693          "Ignoring non-first privileged about content processes."
    694        );
    695        return;
    696      }
    697 
    698      this.log.trace(
    699        `A privileged about content process is launching with ID ${childID}.`
    700      );
    701 
    702      this.log.info("Sending input streams down to content process.");
    703      let actor = processParent.getActor("BrowserProcess");
    704      actor.sendAsyncMessage(this.SEND_STREAMS_MESSAGE, {
    705        pageInputStream: this.pagePipe.inputStream,
    706        scriptInputStream: this.scriptPipe.inputStream,
    707      });
    708 
    709      procManager.addMessageListener(this.CACHE_RESPONSE_MESSAGE, this);
    710      procManager.addMessageListener(this.CACHE_USAGE_RESULT_MESSAGE, this);
    711      this._procManager = procManager;
    712      this._procManagerID = childID;
    713      this._firstPrivilegedProcessCreated = true;
    714    }
    715  },
    716 
    717  /**
    718   * Called when a content process is destroyed. Either it shut down normally,
    719   * or it crashed. If this is the "privileged about content process", then some
    720   * internal state is cleared.
    721   *
    722   * @param {number} childID
    723   *   The unique ID for the content process that was created, as passed by
    724   *   ipc:content-shutdown.
    725   */
    726  onContentProcessShutdown(childID) {
    727    this.log.info(`Content process shutdown: ${childID}`);
    728    if (this._procManagerID == childID) {
    729      this.log.info("It was the current privileged about process.");
    730      if (this._cacheDeferred) {
    731        this.log.error(
    732          "A privileged about content process shut down while cache streams " +
    733            "were still en route."
    734        );
    735        // The crash occurred while we were waiting on cache input streams to
    736        // be returned to us. Resolve with null streams instead.
    737        this._cacheDeferred({ pageInputStream: null, scriptInputStream: null });
    738        this._cacheDeferred = null;
    739      }
    740 
    741      this._procManager.removeMessageListener(
    742        this.CACHE_RESPONSE_MESSAGE,
    743        this
    744      );
    745      this._procManager.removeMessageListener(
    746        this.CACHE_USAGE_RESULT_MESSAGE,
    747        this
    748      );
    749      this._procManager = null;
    750      this._procManagerID = null;
    751    }
    752  },
    753 
    754  /**
    755   * Called externally by ActivityStreamMessageChannel anytime
    756   * a message is broadcast to all about:newtabs, or sent to the
    757   * preloaded about:newtab. This is used to determine if we need
    758   * to refresh the cache.
    759   */
    760  onPreloadedNewTabMessage() {
    761    if (!this._initted || !this._enabled) {
    762      return;
    763    }
    764 
    765    if (this._finalized) {
    766      this.log.trace("Ignoring preloaded newtab update after finalization.");
    767      return;
    768    }
    769 
    770    this.log.trace("Preloaded about:newtab was updated.");
    771 
    772    this._cacheTask.disarm();
    773    this._cacheTask.arm();
    774  },
    775 
    776  /**
    777   * Stores the CACHE_RESULT_SCALARS value that most accurately represents
    778   * the current notion of how the cache has operated so far. It is stored
    779   * temporarily like this because we need to hear from the privileged
    780   * about content process to hear whether or not retrieving the cache
    781   * actually worked on that end. The success state reported back from
    782   * the privileged about content process will be compared against the
    783   * deferred result scalar to compute what will be recorded to
    784   * Telemetry.
    785   *
    786   * Note that this value will only be recorded if its value is GREATER
    787   * than the currently recorded value. This is because it's possible for
    788   * certain functions that record results to re-enter - but we want to record
    789   * the _first_ condition that caused the cache to not be read from.
    790   *
    791   * @param {number} result
    792   *   One of the CACHE_RESULT_SCALARS values. If this value is less than
    793   *   the currently recorded value, it is ignored.
    794   */
    795  setDeferredResult(result) {
    796    if (this._cacheDeferredResultScalar < result) {
    797      this._cacheDeferredResultScalar = result;
    798    }
    799  },
    800 
    801  /**
    802   * Records the final result of how the cache operated for the user
    803   * during this session to Telemetry.
    804   *
    805   * @param {number} result
    806   *   One of the result constants from CACHE_RESULT_SCALARS.
    807   */
    808  recordResult(result) {
    809    // Note: this can be called very early on in the lifetime of
    810    // AboutHomeStartupCache, so things like this.log might not exist yet.
    811    Glean.browserStartup.abouthomeCacheResult.set(result);
    812  },
    813 
    814  /**
    815   * Called when the parent process receives a message from the privileged
    816   * about content process saying whether or not reading from the cache
    817   * was successful.
    818   *
    819   * @param {boolean} success
    820   *   True if reading from the cache succeeded.
    821   */
    822  onUsageResult(success) {
    823    this.log.trace(`Received usage result. Success = ${success}`);
    824    if (success) {
    825      if (
    826        this._cacheDeferredResultScalar !=
    827        this.CACHE_RESULT_SCALARS.VALID_AND_USED
    828      ) {
    829        this.log.error(
    830          "Somehow got a success result despite having never " +
    831            "successfully sent down the cache streams"
    832        );
    833        this.recordResult(this._cacheDeferredResultScalar);
    834      } else {
    835        this.recordResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED);
    836      }
    837 
    838      return;
    839    }
    840 
    841    if (
    842      this._cacheDeferredResultScalar ==
    843      this.CACHE_RESULT_SCALARS.VALID_AND_USED
    844    ) {
    845      // We failed to read from the cache despite having successfully
    846      // sent it down to the content process. We presume then that the
    847      // streams just didn't provide any bytes in time.
    848      this.recordResult(this.CACHE_RESULT_SCALARS.LATE);
    849    } else {
    850      // We failed to read the cache, but already knew why. We can
    851      // now record that value.
    852      this.recordResult(this._cacheDeferredResultScalar);
    853    }
    854  },
    855 
    856  QueryInterface: ChromeUtils.generateQI([
    857    "nsICacheEntryOpenallback",
    858    "nsIObserver",
    859  ]),
    860 
    861  /* MessageListener */
    862 
    863  receiveMessage(message) {
    864    // Only the privileged about content process can write to the cache.
    865    if (
    866      message.target.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
    867    ) {
    868      this.log.error(
    869        "Received a message from a non-privileged content process!"
    870      );
    871      return;
    872    }
    873 
    874    switch (message.name) {
    875      case this.CACHE_RESPONSE_MESSAGE: {
    876        this.log.trace("Parent received cache streams.");
    877        if (!this._cacheDeferred) {
    878          this.log.error("Parent doesn't have _cacheDeferred set up!");
    879          return;
    880        }
    881 
    882        this._cacheDeferred(message.data);
    883        this._cacheDeferred = null;
    884        break;
    885      }
    886      case this.CACHE_USAGE_RESULT_MESSAGE: {
    887        this.onUsageResult(message.data.success);
    888        break;
    889      }
    890    }
    891  },
    892 
    893  /* nsIObserver */
    894 
    895  observe(aSubject, aTopic, aData) {
    896    switch (aTopic) {
    897      case "intl:app-locales-changed": {
    898        this.clearCache();
    899        break;
    900      }
    901      case "process-type-set":
    902      // Intentional fall-through
    903      case "ipc:content-created": {
    904        let childID = aData;
    905        let procManager = aSubject
    906          .QueryInterface(Ci.nsIInterfaceRequestor)
    907          .getInterface(Ci.nsIMessageSender);
    908        let pp = aSubject.QueryInterface(Ci.nsIDOMProcessParent);
    909        this.onContentProcessCreated(childID, procManager, pp);
    910        break;
    911      }
    912 
    913      case "ipc:content-shutdown": {
    914        let childID = aData;
    915        this.onContentProcessShutdown(childID);
    916        break;
    917      }
    918    }
    919  },
    920 
    921  /* nsICacheEntryOpenCallback */
    922 
    923  onCacheEntryCheck() {
    924    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
    925  },
    926 
    927  onCacheEntryAvailable(aEntry) {
    928    this.log.trace("Cache entry is available.");
    929 
    930    this._cacheEntry = aEntry;
    931    this.connectToPipes();
    932    this._cacheEntryResolver(this._cacheEntry);
    933  },
    934 };