Bug 1951773 - add some documentation for callModulesFromCategory, r=mconley,frontend-codestyle-reviewers,Standard8
Differential Revision: https://phabricator.services.mozilla.com/D244049
This commit is contained in:
@@ -391,7 +391,10 @@ const rollouts = [
|
||||
"toolkit/components/workerloader/require.js",
|
||||
"toolkit/content/**",
|
||||
"toolkit/crashreporter/**",
|
||||
"toolkit/modules/**",
|
||||
"toolkit/modules/{A,C,D,E,F,G,I,J,K,L,N,O,P,R,S,U,W}*.sys.mjs",
|
||||
"toolkit/modules/sessionstore/**",
|
||||
"toolkit/modules/subprocess/**",
|
||||
"toolkit/modules/tests/**",
|
||||
"toolkit/mozapps/downloads/**",
|
||||
"toolkit/mozapps/extensions/**",
|
||||
"toolkit/mozapps/handling/**",
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# Browser Startup
|
||||
|
||||
## Invoking your code on browser startup
|
||||
|
||||
The first rule of running code during startup is: don't.
|
||||
|
||||
We take performance very seriously and ideally your component/feature should
|
||||
initialize only when needed.
|
||||
|
||||
If you have established that you really must run code during startup,
|
||||
available entrypoints are:
|
||||
|
||||
- registering a `browser-idle-startup` category entry for your JS module (or
|
||||
even a "best effort" user idle task, see `BrowserGlue.sys.mjs`)
|
||||
- registering a `browser-window-delayed-startup` category entry for your JS
|
||||
module. **Note that this is invoked for each browser window.**
|
||||
- registering a `browser-before-ui-startup` category entry if you really really
|
||||
need to. This will run code before the first browser window appears on the
|
||||
screen and make Firefox seem slow, so please don't do it unless absolutely
|
||||
necessary.
|
||||
|
||||
See [the category manager indirection docs](./CategoryManagerIndirection.md) for
|
||||
more details on this.
|
||||
|
||||
Other useful points in startup are:
|
||||
- `BrowserGlue`'s `_onFirstWindowLoaded` (which should be converted to use a
|
||||
category manager call instead), which fires after the first browser window's
|
||||
`browser-window-delayed-startup` call (see above).
|
||||
- `BrowserGlue`'s `_scheduleBestEffortUserIdleTasks` as mentioned above.
|
||||
Note that in this case, your code **may not run at all** if the browser is
|
||||
shut down quickly.
|
||||
- `BrowserGlue`'s `_onWindowsRestored`, and/or the observer service's
|
||||
`sessionstore-windows-restored` topic, and/or a category manager call that
|
||||
should replace the `BrowserGlue` list of direct calls. This fires after
|
||||
session restore has completed restoring all windows (but before all pages
|
||||
that may have been restored have necessarily loaded). Note that this is
|
||||
guaranteed to fire even if automatic session restore is not enabled.
|
||||
|
||||
## How do first run/first startup experiments work?
|
||||
|
||||
Why does synchronously reading Nimbus feature values work for
|
||||
|
||||
138
browser/docs/CategoryManagerIndirection.md
Normal file
138
browser/docs/CategoryManagerIndirection.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Category manager indirection (callModulesFromCategory)
|
||||
|
||||
Firefox front-end code uses the category manager as a publish/subscribe
|
||||
mechanism for dependency injection, so consumers can be notified of interesting
|
||||
things happening without having to directly talk to the publisher/actor who
|
||||
decides the interesting thing is happening.
|
||||
|
||||
There are 2 parts to this:
|
||||
|
||||
1. Consumers registering with the category manager
|
||||
2. Publishers/actors invoking consumers via the category manager.
|
||||
|
||||
## Consumer registration with the category manager.
|
||||
|
||||
The category manager is used for various purposes within Firefox; it is more or
|
||||
less an arbitrary double string-keyed data store.
|
||||
|
||||
For this particular usecase, the publisher/consumer have to use the same
|
||||
primary key (i.e. category name), such as `browser-idle-startup`.
|
||||
|
||||
The secondary key is a full URL to a `sys.mjs` module. Note that because this
|
||||
is a key, **only one consumer per module & category combination is possible**.
|
||||
|
||||
The "value" part is an `Object.method` notation, where the expectation is that
|
||||
`Object` is an exported symbol from the module identified as the secondary key,
|
||||
and `method` is some method on that object.
|
||||
|
||||
At compile-time, registration can happen with an entry in a `.manifest` file
|
||||
like [BrowserComponents.manifest](https://searchfox.org/mozilla-central/source/browser/components/BrowserComponents.manifest).
|
||||
Note that any manifest successfully processed by the build system would do,
|
||||
we don't need to use `BrowserComponents.manifest` specifically. In fact, it
|
||||
would be preferable if components used their own manifest files.
|
||||
|
||||
An example registration looks like:
|
||||
|
||||
```
|
||||
category browser-idle-startup moz-src://browser/components/tabbrowser/TabUnloader.sys.mjs TabUnloader.init
|
||||
```
|
||||
|
||||
This will ensure that when the `browser-idle-startup` publisher is invoked,
|
||||
the `TabUnloader.sys.mjs` module is loaded and the `init` method on the exported
|
||||
`TabUnloader` object is invoked.
|
||||
|
||||
### Runtime registration
|
||||
|
||||
Runtime registration is less-often used, but can be done using the category
|
||||
manager's XPCOM API:
|
||||
|
||||
```js
|
||||
Services.catMan.addCategoryEntry(
|
||||
"browser-idle-startup",
|
||||
"moz-src://browser/components/tabbrowser/TabUnloader.sys.mjs",
|
||||
"TabUnloader.init"
|
||||
)
|
||||
```
|
||||
|
||||
## Publishers/actors invoking consumers
|
||||
|
||||
Publishers call `BrowserUtils.callModulesFromCategory` with a dictionary of
|
||||
options as the first argument. If provided, any other arguments are passed
|
||||
straight through to any consumers.
|
||||
|
||||
```{js:autofunction} BrowserUtils.callModulesFromCategory
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
BrowserUtils.callModulesFromCategory({
|
||||
categoryName: "my-fancy-category-name",
|
||||
profilerMarker: "markMyCategories",
|
||||
idleDispatch: true,
|
||||
someArgument
|
||||
});
|
||||
```
|
||||
|
||||
This will pass `someArgument` to each consumer registered for
|
||||
`my-fancy-category-name`. Each consumer will be invoked via an idle task, and
|
||||
each task will get a profiler marker (labelled `"markMyCategories"`) in the
|
||||
[Firefox Profiler](https://profiler.firefox.com/) so it's easy to find in
|
||||
performance profiles.
|
||||
|
||||
You should consider using `idleDispatch: true` if invocation of the consumers
|
||||
does not need to happen synchronously.
|
||||
|
||||
If you need to care about errors produced by consumers, you can specify
|
||||
a function for `failureHandler` and handle any exceptions/errors using your own
|
||||
logic. Note that it may be invoked asynchronously if the consumers are async.
|
||||
|
||||
## Caveats
|
||||
|
||||
Any errors thrown by consumers are automatically caught and reported via the
|
||||
[Browser Console](/devtools-user/browser_console/index.rst).
|
||||
|
||||
Async functions are not awaited before invoking other consumers. Note that
|
||||
rejections (exceptions from async code) are still caught and reported to the
|
||||
console, and that the async duration of a given consumer will be what
|
||||
determines the length of the profiler marker, if the publisher asks for profiler
|
||||
markers.
|
||||
|
||||
## Why not just call consumers directly?
|
||||
|
||||
There are a number of benefits over direct method calls and module imports.
|
||||
|
||||
### Reducing direct dependencies between different parts of the code-base
|
||||
|
||||
Code that looks like this:
|
||||
|
||||
```
|
||||
Foo.thingHappened(arg);
|
||||
Bar.thingHappened(arg);
|
||||
Baz.thingHappened(arg);
|
||||
```
|
||||
|
||||
is not only repetitive, it also means that the code in question has to directly
|
||||
import all the modules that provide `Foo`, `Bar` and `Baz`. It means that if
|
||||
those modules change or move or are refactored, the "publisher" code has to
|
||||
be updated, with all the added burdens that comes with (potential for merge
|
||||
conflicts, more automatically added reviewer groups for trivial changes, easy
|
||||
to miss if dependencies are widespread).
|
||||
|
||||
### Avoiding a bootstrap problem in favour of a "just in time" approach
|
||||
|
||||
To make sure code is invoked later, when using the observer service, DOM event
|
||||
listeners, or other mechanisms, it usually needs to add a listener
|
||||
before the event of interest happens. If not managed carefully, this often leads
|
||||
to component initialization being front-loaded to make sure not to "miss" it
|
||||
later. This in turn makes browser startup more heavyweight rather than it needs
|
||||
to be, because we set up listeners for _everything_, potentially loading entire
|
||||
JS modules just to do that.
|
||||
|
||||
### Unified error-handling, performance inspection, and scheduling
|
||||
|
||||
Using the `BrowserUtils.callModulesFromCategory` API allows specifying error
|
||||
handling, performance profiler markers, and scheduling (use of idle tasks) in
|
||||
one place. This abstracts away the fact that we never want observers, event
|
||||
listeners or other mechanisms like this to break and stop notifying (or worse,
|
||||
propagate an exception themselves) when one of the consumers breaks.
|
||||
@@ -12,6 +12,7 @@ This is the nascent documentation of the Firefox front-end code.
|
||||
FrontendCodeReviewBestPractices
|
||||
CommandLineParameters
|
||||
BrowserStartup
|
||||
CategoryManagerIndirection
|
||||
components/customizableui/docs/index
|
||||
components/enterprisepolicies/docs/index
|
||||
extensions/formautofill/docs/index
|
||||
|
||||
@@ -83,6 +83,7 @@ js_source_path = [
|
||||
"../toolkit/components/extensions",
|
||||
"../toolkit/components/extensions/parent",
|
||||
"../toolkit/components/ml/content/backends/ONNXPipeline.mjs",
|
||||
"../toolkit/modules/BrowserUtils.sys.mjs",
|
||||
"../toolkit/mozapps/extensions",
|
||||
"../toolkit/components/prompts/src",
|
||||
"../toolkit/components/pictureinpicture",
|
||||
|
||||
@@ -9,7 +9,7 @@ export var BinarySearch = Object.freeze({
|
||||
*
|
||||
* See search() for a description of this function's parameters.
|
||||
*
|
||||
* @return The index of `target` in `array` or -1 if `target` is not found.
|
||||
* @returns {integer} The index of `target` in `array` or -1 if `target` is not found.
|
||||
*/
|
||||
indexOf(comparator, array, target) {
|
||||
let [found, idx] = this.search(comparator, array, target);
|
||||
@@ -22,7 +22,7 @@ export var BinarySearch = Object.freeze({
|
||||
*
|
||||
* See search() for a description of this function's parameters.
|
||||
*
|
||||
* @return The index in `array` where `target` may be inserted to keep `array`
|
||||
* @returns {integer} The index in `array` where `target` may be inserted to keep `array`
|
||||
* ordered.
|
||||
*/
|
||||
insertionIndexOf(comparator, array, target) {
|
||||
@@ -32,18 +32,18 @@ export var BinarySearch = Object.freeze({
|
||||
/**
|
||||
* Searches for the given target in the given array.
|
||||
*
|
||||
* @param comparator
|
||||
* @param {(a: any, b: any) => number} comparator
|
||||
* A function that takes two arguments and compares them, returning a
|
||||
* negative number if the first should be ordered before the second,
|
||||
* zero if the first and second have the same ordering, or a positive
|
||||
* number if the second should be ordered before the first. The first
|
||||
* argument is always `target`, and the second argument is a value
|
||||
* from the array.
|
||||
* @param array
|
||||
* @param {Array} array
|
||||
* An array whose elements are ordered by `comparator`.
|
||||
* @param target
|
||||
* @param {any} target
|
||||
* The value to search for.
|
||||
* @return An array with two elements. If `target` is found, the first
|
||||
* @returns {Array} An array with two elements. If `target` is found, the first
|
||||
* element is true, and the second element is its index in the array.
|
||||
* If `target` is not found, the first element is false, and the
|
||||
* second element is the index where it may be inserted to keep the
|
||||
|
||||
@@ -103,6 +103,13 @@ function stringPrefToSet(prefVal) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @class BrowserUtils
|
||||
*
|
||||
* A motley collection of utilities (also known as a dumping ground).
|
||||
*
|
||||
* Please avoid expanding this if possible.
|
||||
*/
|
||||
export var BrowserUtils = {
|
||||
/**
|
||||
* Return or create a principal with the content of one, and the originAttributes
|
||||
@@ -110,12 +117,12 @@ export var BrowserUtils = {
|
||||
* not to change, that is, we should keep the userContextId, privateBrowsingId,
|
||||
* etc. the same when changing the principal).
|
||||
*
|
||||
* @param principal
|
||||
* @param {nsIPrincipal} principal
|
||||
* The principal whose content/null/system-ness we want.
|
||||
* @param existingPrincipal
|
||||
* @param {nsIPrincipal} existingPrincipal
|
||||
* The principal whose originAttributes we want, usually the current
|
||||
* principal of a docshell.
|
||||
* @return an nsIPrincipal that matches the content/null/system-ness of the first
|
||||
* @returns {nsIPrincipal} an nsIPrincipal that matches the content/null/system-ness of the first
|
||||
* param, and the originAttributes of the second.
|
||||
*/
|
||||
principalWithMatchingOA(principal, existingPrincipal) {
|
||||
@@ -166,7 +173,7 @@ export var BrowserUtils = {
|
||||
/**
|
||||
* Returns true if |mimeType| is text-based, or false otherwise.
|
||||
*
|
||||
* @param mimeType
|
||||
* @param {string} mimeType
|
||||
* The MIME type to check.
|
||||
*/
|
||||
mimeTypeIsTextBased(mimeType) {
|
||||
@@ -213,7 +220,7 @@ export var BrowserUtils = {
|
||||
*
|
||||
* @param {string} topic
|
||||
* The topic to observe.
|
||||
* @param {function(nsISupports, string)} [test]
|
||||
* @param {(subject: nsISupports, data: string) => boolean} [test]
|
||||
* An optional test function which, when called with the
|
||||
* observer's subject and data, should return true if this is the
|
||||
* expected notification, false otherwise.
|
||||
@@ -221,7 +228,7 @@ export var BrowserUtils = {
|
||||
*/
|
||||
promiseObserved(topic, test = () => true) {
|
||||
return new Promise(resolve => {
|
||||
let observer = (subject, topic, data) => {
|
||||
let observer = (subject, _topic, data) => {
|
||||
if (test(subject, data)) {
|
||||
Services.obs.removeObserver(observer, topic);
|
||||
resolve({ subject, data });
|
||||
@@ -337,11 +344,11 @@ export var BrowserUtils = {
|
||||
/**
|
||||
* Extracts linkNode and href for a click event.
|
||||
*
|
||||
* @param event
|
||||
* @param {UIEvent} event
|
||||
* The click event.
|
||||
* @return [href, linkNode, linkPrincipal].
|
||||
* @returns {Array<any>} [href, linkNode, linkPrincipal].
|
||||
*
|
||||
* @note linkNode will be null if the click wasn't on an anchor
|
||||
* Note that linkNode will be null if the click wasn't on an anchor
|
||||
* element. This includes SVG links, because callers expect |node|
|
||||
* to behave like an <a> element, which SVG links (XLink) don't.
|
||||
*/
|
||||
@@ -419,9 +426,10 @@ export var BrowserUtils = {
|
||||
* - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable".
|
||||
* - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click.
|
||||
*
|
||||
* @param e {Event|Object} Event or JSON Object
|
||||
* @param ignoreButton {Boolean}
|
||||
* @param ignoreAlt {Boolean}
|
||||
* @param {Event|object} e
|
||||
* Event or JSON Object
|
||||
* @param {boolean} ignoreButton
|
||||
* @param {boolean} ignoreAlt
|
||||
* @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
|
||||
*/
|
||||
whereToOpenLink(e, ignoreButton, ignoreAlt) {
|
||||
@@ -493,32 +501,23 @@ export var BrowserUtils = {
|
||||
|
||||
/**
|
||||
* Invoke all the category manager consumers of a given JS consumer.
|
||||
* Similar to the (C++-only) NS_CreateServicesFromCategory in that it'll
|
||||
* Similar to the (C++-only) ``NS_CreateServicesFromCategory`` in that it'll
|
||||
* abstract away the actual work of invoking the modules/services.
|
||||
* Different in that it's JS-only and will invoke methods in modules
|
||||
* instead of using XPCOM services.
|
||||
*
|
||||
* The main benefits of using this over direct calls are:
|
||||
* - error handling (one consumer throwing an exception doesn't stop the
|
||||
* others being called)
|
||||
* - dependency injection (callsite doesn't have to [lazy] import half
|
||||
* the world to call all the methods)
|
||||
* - performance/bootstrapping using build-time registration, when
|
||||
* compared to nsIObserver or events: with nsIObserver/handleEvent,
|
||||
* you'd have to call addObserver or addEventListener somewhere, which
|
||||
* means either loading your code early (bad for performance) or burdening
|
||||
* other code that already runs early with adding your handlers (not great
|
||||
* for code cleanliness).
|
||||
* More context is available in
|
||||
* https://firefox-source-docs.mozilla.org/browser/CategoryManagerIndirection.html
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {object} options
|
||||
* @param {string} options.categoryName
|
||||
* What category's consumers to call
|
||||
* What category's consumers to call.
|
||||
* @param {boolean} [options.idleDispatch=false]
|
||||
* If set to true, call each consumer in an idle task.
|
||||
* @param {string} [options.profilerMarker=""]
|
||||
* If specified, will create a profiler marker with the provided
|
||||
* identifier for each consumer.
|
||||
* @param {function} [options.failureHandler]
|
||||
* @param {Function} [options.failureHandler]
|
||||
* If specified, will be called for any exceptions raised, in
|
||||
* order to do custom failure handling.
|
||||
* @param {...any} args
|
||||
@@ -580,7 +579,7 @@ export var BrowserUtils = {
|
||||
/**
|
||||
* Returns whether the build is a China repack.
|
||||
*
|
||||
* @return {boolean} True if the distribution ID is 'MozillaOnline',
|
||||
* @returns {boolean} True if the distribution ID is 'MozillaOnline',
|
||||
* otherwise false.
|
||||
*/
|
||||
isChinaRepack() {
|
||||
@@ -616,7 +615,7 @@ export var BrowserUtils = {
|
||||
*
|
||||
* @param {BrowserUtils.PromoType} promoType - What promo are we checking on?
|
||||
*
|
||||
* @return {boolean} - should we display this promo now or not?
|
||||
* @returns {boolean} - should we display this promo now or not?
|
||||
*/
|
||||
shouldShowPromo(promoType) {
|
||||
switch (promoType) {
|
||||
|
||||
Reference in New Issue
Block a user