Bug 1495183 - Create a first muxer implementation. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Sun, 02 Dec 2018 09:58:15 +0000
changeset 508380 f23d8b5068f7905a1f708c7aa1037ccb75943610
parent 508378 8e021c409c6a16d9bab9713d20e30fa05c5b8365
child 508381 324e00e894edd403d6a1a5ab439641b42807707d
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1495183
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1495183 - Create a first muxer implementation. r=adw Differential Revision: https://phabricator.services.mozilla.com/D13552
browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/moz.build
browser/components/urlbar/tests/unit/test_muxer.js
browser/components/urlbar/tests/unit/test_providersManager.js
browser/components/urlbar/tests/unit/xpcshell.ini
browser/docs/AddressBar.rst
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a component used to sort matches in a QueryContext.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarMuxerUnifiedComplete"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Log: "resource://gre/modules/Log.jsm",
+  UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+  Log.repository.getLogger("Places.Urlbar.UrlbarMuxerUnifiedComplete"));
+
+const MATCH_TYPE_TO_GROUP = new Map([
+  [ UrlbarUtils.MATCH_TYPE.TAB_SWITCH, UrlbarUtils.MATCH_GROUP.GENERAL ],
+  [ UrlbarUtils.MATCH_TYPE.SEARCH, UrlbarUtils.MATCH_GROUP.SUGGESTION ],
+  [ UrlbarUtils.MATCH_TYPE.URL, UrlbarUtils.MATCH_GROUP.GENERAL ],
+  [ UrlbarUtils.MATCH_TYPE.KEYWORD, UrlbarUtils.MATCH_GROUP.GENERAL ],
+  [ UrlbarUtils.MATCH_TYPE.OMNIBOX, UrlbarUtils.MATCH_GROUP.EXTENSION ],
+  [ UrlbarUtils.MATCH_TYPE.REMOTE_TAB, UrlbarUtils.MATCH_GROUP.GENERAL ],
+]);
+
+/**
+ * Class used to create a muxer.
+ * The muxer receives and sorts matches in a QueryContext.
+ */
+class MuxerUnifiedComplete {
+  constructor() {
+    // Nothing.
+  }
+
+  get name() {
+    return "MuxerUnifiedComplete";
+  }
+
+  /**
+   * Sorts matches in the given QueryContext.
+   * @param {object} context a QueryContext
+   */
+  sort(context) {
+    if (!context.results.length) {
+      return;
+    }
+    // Check the first match, if it's a preselected search match, use search buckets.
+    let firstMatch = context.results[0];
+    let buckets = context.preselected &&
+                  firstMatch.type == UrlbarUtils.MATCH_TYPE.SEARCH ?
+                    UrlbarPrefs.get("matchBucketsSearch") :
+                    UrlbarPrefs.get("matchBuckets");
+    logger.debug(`Buckets: ${buckets}`);
+    let sortedMatches = [];
+    for (let [group, count] of buckets) {
+      // Search all the available matches and fill this bucket.
+      for (let match of context.results) {
+        if (count == 0) {
+          // There's no more space in this bucket.
+          break;
+        }
+
+        // Handle the heuristic result.
+        if (group == UrlbarUtils.MATCH_GROUP.HEURISTIC &&
+            match == firstMatch && context.preselected) {
+          sortedMatches.push(match);
+          count--;
+        } else if (group == MATCH_TYPE_TO_GROUP.get(match.type)) {
+          sortedMatches.push(match);
+          count--;
+        } else {
+          let errorMsg = `Match type ${match.type} is not mapped to a match group.`;
+          logger.error(errorMsg);
+          Cu.reportError(errorMsg);
+        }
+      }
+    }
+  }
+}
+
+var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -24,16 +24,21 @@ XPCOMUtils.defineLazyGetter(this, "logge
   Log.repository.getLogger("Places.Urlbar.ProvidersManager"));
 
 // List of available local providers, each is implemented in its own jsm module
 // and will track different queries internally by queryContext.
 var localProviderModules = {
   UrlbarProviderUnifiedComplete: "resource:///modules/UrlbarProviderUnifiedComplete.jsm",
 };
 
+// List of available local muxers, each is implemented in its own jsm module.
+var localMuxerModules = {
+  UrlbarMuxerUnifiedComplete: "resource:///modules/UrlbarMuxerUnifiedComplete.jsm",
+};
+
 // To improve dataflow and reduce UI work, when a match is added by a
 // non-immediate provider, we notify it to the controller after a delay, so
 // that we can chunk matches coming in that timeframe into a single call.
 const CHUNK_MATCHES_DELAY_MS = 16;
 
 /**
  * Class used to create a manager.
  * The manager is responsible to keep a list of providers, instantiate query
@@ -54,47 +59,87 @@ class ProvidersManager {
     }
     // Tracks ongoing Query instances by queryContext.
     this.queries = new Map();
 
     // Interrupt() allows to stop any running SQL query, some provider may be
     // running a query that shouldn't be interrupted, and if so it should
     // bump this through disableInterrupt and enableInterrupt.
     this.interruptLevel = 0;
+
+    // This maps muxer names to muxers.
+    this.muxers = new Map();
+    for (let [symbol, module] of Object.entries(localMuxerModules)) {
+      let {[symbol]: muxer} = ChromeUtils.import(module, {});
+      this.registerMuxer(muxer);
+    }
   }
 
   /**
    * Registers a provider object with the manager.
    * @param {object} provider
    */
   registerProvider(provider) {
-    logger.info(`Registering provider ${provider.name}`);
+    if (!provider || !provider.name ||
+        (typeof provider.startQuery != "function") ||
+        (typeof provider.cancelQuery != "function")) {
+      throw new Error(`Trying to register an invalid provider`);
+    }
     if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
       throw new Error(`Unknown provider type ${provider.type}`);
     }
+    logger.info(`Registering provider ${provider.name}`);
     this.providers.get(provider.type).set(provider.name, provider);
   }
 
   /**
    * Unregisters a previously registered provider object.
    * @param {object} provider
    */
   unregisterProvider(provider) {
     logger.info(`Unregistering provider ${provider.name}`);
     this.providers.get(provider.type).delete(provider.name);
   }
 
   /**
+   * Registers a muxer object with the manager.
+   * @param {object} muxer a UrlbarMuxer object
+   */
+  registerMuxer(muxer) {
+    if (!muxer || !muxer.name || (typeof muxer.sort != "function")) {
+      throw new Error(`Trying to register an invalid muxer`);
+    }
+    logger.info(`Registering muxer ${muxer.name}`);
+    this.muxers.set(muxer.name, muxer);
+  }
+
+  /**
+   * Unregisters a previously registered muxer object.
+   * @param {object} muxer a UrlbarMuxer object or name.
+   */
+  unregisterMuxer(muxer) {
+    let muxerName = typeof muxer == "string" ? muxer : muxer.name;
+    logger.info(`Unregistering muxer ${muxerName}`);
+    this.muxers.delete(muxerName);
+  }
+
+  /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {object} controller a UrlbarController instance
    */
   async startQuery(queryContext, controller) {
     logger.info(`Query start ${queryContext.searchString}`);
-    let query = new Query(queryContext, controller, this.providers);
+    let muxerName = queryContext.muxer || "MuxerUnifiedComplete";
+    logger.info(`Using muxer ${muxerName}`);
+    let muxer = this.muxers.get(muxerName);
+    if (!muxer) {
+      throw new Error(`Muxer with name ${muxerName} not found`);
+    }
+    let query = new Query(queryContext, controller, muxer, this.providers);
     this.queries.set(queryContext, query);
     await query.start();
   }
 
   /**
    * Cancels a running query.
    * @param {object} queryContext
    */
@@ -140,22 +185,25 @@ var UrlbarProvidersManager = new Provide
  */
 class Query {
   /**
    * Initializes the query object.
    * @param {object} queryContext
    *        The query context
    * @param {object} controller
    *        The controller to be notified
+   * @param {object} muxer
+   *        The muxer to sort matches
    * @param {object} providers
    *        Map of all the providers by type and name
    */
-  constructor(queryContext, controller, providers) {
+  constructor(queryContext, controller, muxer, providers) {
     this.context = queryContext;
     this.context.results = [];
+    this.muxer = muxer;
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
     // Array of acceptable MATCH_SOURCE values for this query. Providers not
     // returning any of these will be skipped, as well as matches not part of
     // this subset (Note we still expect the provider to do its own internal
@@ -262,18 +310,17 @@ class Query {
 
     this.context.results.push(match);
 
     let notifyResults = () => {
       if (this._chunkTimer) {
         this._chunkTimer.cancel().catch(Cu.reportError);
         delete this._chunkTimer;
       }
-      // TODO:
-      //  * pass results to a muxer before sending them back to the controller.
+      this.muxer.sort(this.context);
       this.controller.receiveResults(this.context);
     };
 
     // If the provider is not of immediate type, chunk results, to improve the
     // dataflow and reduce UI flicker.
     if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
       notifyResults();
     } else if (!this._chunkTimer) {
--- a/browser/components/urlbar/moz.build
+++ b/browser/components/urlbar/moz.build
@@ -4,16 +4,17 @@
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Address Bar")
 
 EXTRA_JS_MODULES += [
     'UrlbarController.jsm',
     'UrlbarInput.jsm',
     'UrlbarMatch.jsm',
+    'UrlbarMuxerUnifiedComplete.jsm',
     'UrlbarPrefs.jsm',
     'UrlbarProviderOpenTabs.jsm',
     'UrlbarProvidersManager.jsm',
     'UrlbarProviderUnifiedComplete.jsm',
     'UrlbarTokenizer.jsm',
     'UrlbarUtils.jsm',
     'UrlbarValueFormatter.jsm',
     'UrlbarView.jsm',
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_muxer() {
+  Assert.throws(() => UrlbarProvidersManager.registerMuxer(),
+                /invalid muxer/,
+                "Should throw with no arguments");
+  Assert.throws(() => UrlbarProvidersManager.registerMuxer({}),
+                /invalid muxer/,
+                "Should throw with empty object");
+  Assert.throws(() => UrlbarProvidersManager.registerMuxer({
+                  name: "",
+                }),
+                /invalid muxer/,
+                "Should throw with empty name");
+  Assert.throws(() => UrlbarProvidersManager.registerMuxer({
+                  name: "test",
+                  sort: "no",
+                }),
+                /invalid muxer/,
+                "Should throw with invalid sort");
+
+  let matches = [
+    new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                    UrlbarUtils.MATCH_SOURCE.TABS,
+                    { url: "http://mozilla.org/tab/" }),
+    new UrlbarMatch(UrlbarUtils.MATCH_TYPE.URL,
+                    UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
+                    { url: "http://mozilla.org/bookmark/" }),
+    new UrlbarMatch(UrlbarUtils.MATCH_TYPE.URL,
+                    UrlbarUtils.MATCH_SOURCE.HISTORY,
+                    { url: "http://mozilla.org/history/" }),
+  ];
+  registerBasicTestProvider(matches);
+
+  let context = createContext();
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+  let muxer = {
+    get name() {
+      return "TestMuxer";
+    },
+    sort(queryContext) {
+      queryContext.results.sort((a, b) => {
+        if (b.source == UrlbarUtils.MATCH_SOURCE.TABS) {
+          return -1;
+        }
+        if (b.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS) {
+          return 1;
+        }
+        return a.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS ? -1 : 1;
+      });
+    },
+  };
+  UrlbarProvidersManager.registerMuxer(muxer);
+  context.muxer = "TestMuxer";
+
+  info("Check results, the order should be: bookmark, history, tab");
+  await UrlbarProvidersManager.startQuery(context, controller);
+  Assert.deepEqual(context.results, [
+    matches[1],
+    matches[2],
+    matches[0],
+  ]);
+
+  // Sanity check, should not throw.
+  UrlbarProvidersManager.unregisterMuxer(muxer);
+  UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op.
+});
--- a/browser/components/urlbar/tests/unit/test_providersManager.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -1,14 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function test_providers() {
+  Assert.throws(() => UrlbarProvidersManager.registerProvider(),
+                /invalid provider/,
+                "Should throw with no arguments");
+  Assert.throws(() => UrlbarProvidersManager.registerProvider({}),
+                /invalid provider/,
+                "Should throw with empty object");
+  Assert.throws(() => UrlbarProvidersManager.registerProvider({
+                  name: "",
+                }),
+                /invalid provider/,
+                "Should throw with empty name");
+  Assert.throws(() => UrlbarProvidersManager.registerProvider({
+                  name: "test",
+                  startQuery: "no",
+                }),
+                /invalid provider/,
+                "Should throw with invalid startQuery");
+  Assert.throws(() => UrlbarProvidersManager.registerProvider({
+                  name: "test",
+                  startQuery: () => {},
+                  cancelQuery: "no",
+                }),
+                /invalid provider/,
+                "Should throw with invalid cancelQuery");
+
   let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
                               UrlbarUtils.MATCH_SOURCE.TABS,
                               { url: "http://mozilla.org/foo/" });
   registerBasicTestProvider([match]);
 
   let context = createContext();
   let controller = new UrlbarController({
     browserWindow: {
--- a/browser/components/urlbar/tests/unit/xpcshell.ini
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
+[test_muxer.js]
 [test_providerOpenTabs.js]
 [test_providersManager.js]
 [test_providersManager_filtering.js]
 [test_QueryContext.js]
 [test_tokenizer.js]
 [test_UrlbarController_unit.js]
 [test_UrlbarController_integration.js]
 [test_UrlbarUtils_addToUrlbarHistory.js]
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -60,16 +60,20 @@ It is augmented as it progresses through
              // behavior, for example by not autofilling again when the user
              // hit backspace.
     maxResults; // {integer} The maximum number of results requested. The Model
                 // may actually return more results than expected, so that the
                 // View and the Controller can do additional filtering.
     isPrivate; // {boolean} Whether the search started in a private context.
     userContextId; // {integer} The user context ID (containers feature).
 
+    // Optional properties.
+    muxer; // Name of a registered muxer. Muxers can be registered through the
+           // UrlbarProvidersManager
+
     // Properties added by the Model.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value}.
     results; // {array} list of UrlbarMatch objects.
     preselected; // {boolean} whether the first match should be preselected.
     autofill; // {boolean} whether the first match is an autofill match.
   }
 
@@ -83,19 +87,20 @@ At the core is the `UrlbarProvidersManag
 a component tracking all the available search providers, and managing searches
 across them.
 
 The *UrlbarProvidersManager* is a singleton, it registers internal providers on
 startup and can register/unregister providers on the fly.
 It can manage multiple concurrent queries, and tracks them internally as
 separate *Query* objects.
 
-The *Controller* starts and stops queries through the *ProvidersManager*. It's
-possible to wait for the promise returned by *startQuery* to know when no more
-matches will be returned, it is not mandatory though. Queries can be canceled.
+The *Controller* starts and stops queries through the *UrlbarProvidersManager*.
+It's possible to wait for the promise returned by *startQuery* to know when no
+more matches will be returned, it is not mandatory though.
+Queries can be canceled.
 
 .. note::
 
   Canceling a query will issue an interrupt() on the database connection,
   terminating any running and future SQL query, unless a query is running inside
   a *runInCriticalSection* task.
 
 The *searchString* gets tokenized by the `UrlbarTokenizer <https://dxr.mozilla.org/mozilla-central/source/browser/components/urlbar/UrlbarTokenizer.jsm>`_
@@ -109,16 +114,18 @@ used by the user to restrict the search 
   consumer may want to check the value before applying filters.
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarProvidersManager {
     registerProvider(providerObj);
     unregisterProvider(providerObj);
+    registerMuxer(muxerObj);
+    unregisterMuxer(muxerObjOrName);
     async startQuery(queryContext);
     cancelQuery(queryContext);
     // Can be used by providers to run uninterruptible queries.
     runInCriticalSection(taskFn);
   }
 
 UrlbarProvider
 --------------
@@ -155,24 +162,32 @@ implementation details may vary deeply a
     // Any cleaning/resetting task should happen here.
     cancelQuery(QueryContext);
   }
 
 UrlbarMuxer
 -----------
 
 The *Muxer* is responsible for sorting matches based on their importance and
-additional rules that depend on the QueryContext.
+additional rules that depend on the QueryContext. The muxer to use is indicated
+by the QueryContext.muxer property.
 
 .. caution
 
   The Muxer is a replaceable component, as such what is described here is a
   reference for the default View, but may not be valid for other implementations.
 
-*Content to be written*
+.. highlight:: JavaScript
+.. code:
+
+  UrlbarMuxer {
+    name; // {string} A simple name to track the provider.
+    // Invoked by the ProvidersManager to sort matches.
+    sort(queryContext);
+  }
 
 
 The Controller
 ==============
 
 `UrlbarController <https://dxr.mozilla.org/mozilla-central/source/browser/components/urlbar/UrlbarController.jsm>`_
 is the component responsible for reacting to user's input, by communicating
 proper course of action to the Model (e.g. starting/stopping a query) and the