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 };