Bug 1371611 - UnifiedComplete::LoadPrefs is slow. draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 26 Jul 2017 18:47:35 +0200
changeset 619221 f01cc5f42f4878a005754132a473c1e0ea245453
parent 619065 ef9a0f01e4f68214f0ff8f4631783b8a0e075a82
child 640344 7700a78bcce660b6849fb844c44a2826f8a52377
push id71620
push usermak77@bonardo.net
push dateTue, 01 Aug 2017 19:48:13 +0000
bugs1371611
milestone56.0a1
Bug 1371611 - UnifiedComplete::LoadPrefs is slow. MozReview-Commit-ID: G0r2aogwvyb
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/test_enabled.js
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -9,50 +9,51 @@
 // Constants
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 const MS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000
 
 // Match type constants.
 // These indicate what type of search function we should be using.
-const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
-const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
-const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
-const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
-const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
-
-const PREF_BRANCH = "browser.urlbar.";
+const {
+  MATCH_ANYWHERE,
+  MATCH_BOUNDARY_ANYWHERE,
+  MATCH_BOUNDARY,
+  MATCH_BEGINNING,
+  MATCH_BEGINNING_CASE_SENSITIVE,
+} = Ci.mozIPlacesAutoComplete;
 
 // Prefs are defined as [pref name, default value].
-const PREF_ENABLED =                [ "autocomplete.enabled",   true ];
-const PREF_AUTOFILL =               [ "autoFill",               true ];
-const PREF_AUTOFILL_TYPED =         [ "autoFill.typed",         true ];
-const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", false ];
-const PREF_RESTYLESEARCHES        = [ "restyleSearches",        false ];
-const PREF_DELAY =                  [ "delay",                  50 ];
-const PREF_BEHAVIOR =               [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
-const PREF_FILTER_JS =              [ "filter.javascript",      true ];
-const PREF_MAXRESULTS =             [ "maxRichResults",         25 ];
-
-const PREF_SUGGEST_HISTORY =        [ "suggest.history",        true ];
-const PREF_SUGGEST_BOOKMARK =       [ "suggest.bookmark",       true ];
-const PREF_SUGGEST_OPENPAGE =       [ "suggest.openpage",       true ];
-const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
-const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       false ];
-
-const PREF_MAX_CHARS_FOR_SUGGEST =  [ "maxCharsForSearchSuggestions", 20];
-const PREF_MAX_HISTORICAL_SUGGESTIONS =  [ "maxHistoricalSearchSuggestions", 0];
-
-const PREF_PRELOADED_SITES_ENABLED =  [ "usepreloadedtopurls.enabled",   true ];
-const PREF_PRELOADED_SITES_EXPIRE_DAYS = [ "usepreloadedtopurls.expire_days",  14 ];
-
-const PREF_MATCH_BUCKETS = [ "matchBuckets", "general:5,suggestion:Infinity" ];
-// Will default to matchBuckets if not defined.
-const PREF_MATCH_BUCKETS_SEARCH = [ "matchBucketsSearch", "" ];
+const PREF_URLBAR_BRANCH = "browser.urlbar.";
+const PREF_URLBAR_DEFAULTS = new Map([
+  ["autocomplete.enabled", true],
+  ["autoFill", true],
+  ["autoFill.typed", true],
+  ["autoFill.searchEngines", false],
+  ["restyleSearches", false],
+  ["delay", 50],
+  ["matchBehavior", MATCH_BOUNDARY_ANYWHERE],
+  ["filter.javascript", true],
+  ["maxRichResults", 10],
+  ["suggest.history", true],
+  ["suggest.bookmark", true],
+  ["suggest.openpage", true],
+  ["suggest.history.onlyTyped", false],
+  ["suggest.searches", false],
+  ["maxCharsForSearchSuggestions", 20],
+  ["maxHistoricalSearchSuggestions", 0],
+  ["usepreloadedtopurls.enabled", true],
+  ["usepreloadedtopurls.expire_days", 14],
+  ["matchBuckets", "general:5,suggestion:Infinity"],
+  ["matchBucketsSearch", ""],
+]);
+const PREF_OTHER_DEFAULTS = new Map([
+  ["keyword.enabled", true],
+]);
 
 // AutoComplete query type constants.
 // Describes the various types of queries that we can process rows for.
 const QUERYTYPE_FILTERED            = 0;
 const QUERYTYPE_AUTOFILL_HOST       = 1;
 const QUERYTYPE_AUTOFILL_URL        = 2;
 
 // This separator is used as an RTL-friendly way to split the title and tags.
@@ -122,17 +123,17 @@ const MATCHTYPE = {
 
 // Buckets for match insertion.
 // Every time a new match is returned, we go through each bucket in array order,
 // and look for the first one having available space for the given match type.
 // Each bucket is an array containing the following indices:
 //   0: The match type of the acceptable entries.
 //   1: available number of slots in this bucket.
 // There are different matchBuckets definition for different contexts, currently
-// a general one (_matchBuckets) and a search one (_matchBucketsSearch).
+// a general one (matchBuckets) and a search one (matchBucketsSearch).
 //
 // First buckets. Anything with an Infinity frecency ends up here.
 const DEFAULT_BUCKETS_BEFORE = [
   [MATCHTYPE.HEURISTIC, 1],
   [MATCHTYPE.EXTENSION, MAXIMUM_ALLOWED_EXTENSION_MATCHES - 1],
 ];
 // => USER DEFINED BUCKETS WILL BE INSERTED HERE <=
 //
@@ -297,24 +298,20 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.importGlobalProperties(["fetch"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-                                  "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
                                   "resource://gre/modules/ExtensionSearchHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
                                   "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
                                   "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
@@ -326,21 +323,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsITextToSubURI");
 
 function setTimeout(callback, ms) {
   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
 }
 
 function convertBucketsCharPrefToArray(str) {
-  return str.toLowerCase()
-            .split(",")
+  return str.split(",")
             .map(v => {
               let bucket = v.split(":");
-              return [ bucket[0].trim(), Number(bucket[1]) ];
+              return [ bucket[0].trim().toLowerCase(), Number(bucket[1]) ];
             });
 }
 
 /**
  * Storage object for switch-to-tab entries.
  * This takes care of caching and registering open pages, that will be reused
  * by switch-to-tab queries.  It has an internal cache, so that the Sqlite
  * store is lazy initialized only on first use.
@@ -436,169 +432,173 @@ XPCOMUtils.defineLazyGetter(this, "Switc
 
   shutdown() {
     this._conn = null;
     this._queue.clear();
   }
 }));
 
 /**
- * This helper keeps track of preferences and keeps their values up-to-date.
+ * This helper keeps track of preferences and their updates.
  */
 XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
-  let prefs = new Preferences(PREF_BRANCH);
-  let types = ["History", "Bookmark", "Openpage", "Searches"];
+  let branch = Services.prefs.getBranch(PREF_URLBAR_BRANCH);
+  let types = ["history", "bookmark", "openpage", "searches"];
+  let prefTypes = new Map([["boolean", "Bool"], ["string", "Char"], ["number", "Int"]]);
 
-  function syncEnabledPref() {
-    loadSyncedPrefs();
+  function readPref(pref) {
+    let prefs = branch;
+    let def = PREF_URLBAR_DEFAULTS.get(pref);
+    if (def === undefined) {
+      prefs = Services.prefs;
+      def = PREF_OTHER_DEFAULTS.get(pref);
+    }
+    if (def === undefined)
+      throw new Error("Trying to access an unknown pref " + pref);
+    return prefs[`get${prefTypes.get(typeof def)}Pref`](pref, def);
+  }
 
-    let suggestPrefs = [
-      PREF_SUGGEST_HISTORY,
-      PREF_SUGGEST_BOOKMARK,
-      PREF_SUGGEST_OPENPAGE,
-      PREF_SUGGEST_SEARCHES,
-    ];
+  function getPrefValue(pref) {
+    switch (pref) {
+      case "matchBuckets": {
+        // Convert from pref char format to an array and add the default buckets.
+        let val = readPref(pref);
+        try {
+          val = convertBucketsCharPrefToArray(val);
+        } catch (ex) {
+          val = convertBucketsCharPrefToArray(PREF_URLBAR_DEFAULTS.get(pref));
+        }
+        return [ ...DEFAULT_BUCKETS_BEFORE,
+                ...val,
+                ...DEFAULT_BUCKETS_AFTER ];
+      }
+      case "matchBucketsSearch": {
+        // Convert from pref char format to an array and add the default buckets.
+        let val = readPref(pref);
+        if (val) {
+          // Convert from pref char format to an array and add the default buckets.
+          try {
+            val = convertBucketsCharPrefToArray(val);
+            return [ ...DEFAULT_BUCKETS_BEFORE,
+                    ...val,
+                    ...DEFAULT_BUCKETS_AFTER ];
+          } catch (ex) { /* invalid format, will just return matchBuckets */ }
+        }
+        return store.get("matchBuckets");
+      }
+      case "suggest.history.onlyTyped": {
+        // If history is not set, onlyTyped value should be ignored.
+        return store.get("suggest.history") && readPref(pref);
+      }
+      case "defaultBehavior": {
+        let val = 0;
+        for (let type of [...types, "history.onlyTyped"]) {
+          let behavior = type == "history.onlyTyped" ? "TYPED" : type.toUpperCase();
+          val |= store.get("suggest." + type) &&
+                      Ci.mozIPlacesAutoComplete["BEHAVIOR_" + behavior];
+        }
+        return val;
+      }
+      case "emptySearchDefaultBehavior": {
+        // Further restrictions to apply for "empty searches" (searching for "").
+        // The empty behavior is typed history, if history is enabled. Otherwise,
+        // it is bookmarks, if they are enabled. If both history and bookmarks are
+        // disabled, it defaults to open pages.
+        let val = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
+        if (store.get("suggest.history")) {
+          val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+                Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
+        } else if (store.get("suggest.bookmark")) {
+          val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+        } else {
+          val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
+        }
+        return val;
+      }
+      case "matchBehavior": {
+        // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+        let val = readPref(pref);
+        if (![MATCH_ANYWHERE, MATCH_BOUNDARY, MATCH_BEGINNING].includes(val)) {
+          val = MATCH_BOUNDARY_ANYWHERE;
+        }
+        return val;
+      }
+    }
+    return readPref(pref);
+  }
 
-    if (store.enabled) {
-      // If the autocomplete preference is active, set to default value all suggest
-      // preferences only if all of them are false.
-      if (types.every(type => store["suggest" + type] == false)) {
-        for (let type of suggestPrefs) {
-          prefs.set(...type);
+  // Used to keep some pref values linked.
+  // TODO: remove autocomplete.enabled and rely only on suggest.* prefs once we
+  // can drop legacy add-ons compatibility.
+  let linkingPrefs = false;
+  function updateLinkedPrefs(changedPref = "") {
+    // Avoid re-entrance.
+    if (linkingPrefs)
+      return;
+    linkingPrefs = true;
+    try {
+      if (changedPref.startsWith("suggest.")) {
+        // A suggest pref changed, fix autocomplete.enabled.
+        branch.setBoolPref("autocomplete.enabled",
+                          types.some(type => store.get("suggest." + type)));
+      } else if (store.get("autocomplete.enabled")) {
+        // If autocomplete is enabled and all of the suggest.* prefs are disabled,
+        // reset the suggest.* prefs to their default value.
+        if (types.every(type => !store.get("suggest." + type))) {
+          for (let type of types) {
+            let def = PREF_URLBAR_DEFAULTS.get("suggest." + type);
+            branch.setBoolPref("suggest." + type, def);
+          }
+        }
+      } else {
+        // If autocomplete is disabled, deactivate all suggest preferences.
+        for (let type of types) {
+          branch.setBoolPref("suggest." + type, false);
         }
       }
-    } else {
-      // If the preference was deactivated, deactivate all suggest preferences.
-      for (let type of suggestPrefs) {
-        prefs.set(type[0], false);
-      }
+    } finally {
+      linkingPrefs = false;
     }
   }
 
-  function loadSyncedPrefs() {
-    store.enabled = prefs.get(...PREF_ENABLED);
-    store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
-    store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
-    store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
-    store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
-    store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
-  }
-
-  function loadPrefs(subject, topic, data) {
-    if (data) {
-      // Synchronize suggest.* prefs with autocomplete.enabled.
-      if (data == PREF_BRANCH + PREF_ENABLED[0]) {
-        syncEnabledPref();
-      } else if (data.startsWith(PREF_BRANCH + "suggest.")) {
-        loadSyncedPrefs();
-        prefs.set(PREF_ENABLED[0], types.some(type => store["suggest" + type]));
+  let store = {
+    _map: new Map(),
+    get(pref) {
+      if (!this._map.has(pref))
+        this._map.set(pref, getPrefValue(pref));
+      return this._map.get(pref);
+    },
+    observe(subject, topic, data) {
+      let pref = data.replace(PREF_URLBAR_BRANCH, "");
+      if (!PREF_URLBAR_DEFAULTS.has(pref) && !PREF_OTHER_DEFAULTS.has(pref))
+        return;
+      this._map.delete(pref);
+      // Some prefs may influence others.
+      if (pref == "matchBuckets") {
+        this._map.delete("matchBucketsSearch");
+      } else if (pref == "suggest.history") {
+        this._map.delete("suggest.history.onlyTyped");
       }
-    }
-
-    store.enabled = prefs.get(...PREF_ENABLED);
-    store.autofill = prefs.get(...PREF_AUTOFILL);
-    store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
-    store.autofillSearchEngines = prefs.get(...PREF_AUTOFILL_SEARCHENGINES);
-    store.restyleSearches = prefs.get(...PREF_RESTYLESEARCHES);
-    store.delay = prefs.get(...PREF_DELAY);
-    store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
-    store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
-    store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
-    store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
-    store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
-    store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
-    store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
-    store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
-    store.maxCharsForSearchSuggestions = prefs.get(...PREF_MAX_CHARS_FOR_SUGGEST);
-    store.maxHistoricalSearchSuggestions = prefs.get(...PREF_MAX_HISTORICAL_SUGGESTIONS);
-    store.preloadedSitesEnabled = prefs.get(...PREF_PRELOADED_SITES_ENABLED);
-    store.preloadedSitesExpireDays = prefs.get(...PREF_PRELOADED_SITES_EXPIRE_DAYS);
-    store.matchBuckets = prefs.get(...PREF_MATCH_BUCKETS);
-    // Convert from pref char format to an array and add the default buckets.
-    try {
-      store.matchBuckets = convertBucketsCharPrefToArray(store.matchBuckets);
-    } catch (ex) {
-      store.matchBuckets = convertBucketsCharPrefToArray(PREF_MATCH_BUCKETS[1]);
-    }
-    store.matchBuckets = [ ...DEFAULT_BUCKETS_BEFORE,
-                           ...store.matchBuckets,
-                           ...DEFAULT_BUCKETS_AFTER ];
-    store.matchBucketsSearch = prefs.get(...PREF_MATCH_BUCKETS_SEARCH);
-    // Default to matchBuckets if not defined.
-    if (!store.matchBucketsSearch) {
-      store.matchBucketsSearch = store.matchBuckets;
-    } else {
-      // Convert from pref char format to an array and add the default buckets.
-      try {
-        store.matchBucketsSearch = convertBucketsCharPrefToArray(store.matchBucketsSearch);
-        store.matchBucketsSearch = [ ...DEFAULT_BUCKETS_BEFORE,
-                                     ...store.matchBucketsSearch,
-                                     ...DEFAULT_BUCKETS_AFTER ];
-      } catch (ex) {
-        store.matchBucketsSearch = store.matchBuckets;
+      if (pref == "autocomplete.enabled" || pref.startsWith("suggest.")) {
+        this._map.delete("defaultBehavior");
+        this._map.delete("emptySearchDefaultBehavior");
+        updateLinkedPrefs(pref);
       }
-    }
-    store.keywordEnabled = Services.prefs.getBoolPref("keyword.enabled", true);
-
-    // If history is not set, onlyTyped value should be ignored.
-    if (!store.suggestHistory) {
-      store.suggestTyped = false;
-    }
-    store.defaultBehavior = [...types, "Typed"].reduce((memo, type) => {
-      let prefValue = store["suggest" + type];
-      return memo | (prefValue &&
-                     Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
-    }, 0);
-
-    // Further restrictions to apply for "empty searches" (i.e. searches for "").
-    // The empty behavior is typed history, if history is enabled. Otherwise,
-    // it is bookmarks, if they are enabled. If both history and bookmarks are disabled,
-    // it defaults to open pages.
-    store.emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
-    if (store.suggestHistory) {
-      store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
-                                          Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
-    } else if (store.suggestBookmark) {
-      store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
-    } else {
-      store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
-    }
-
-    // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
-    if (store.matchBehavior != MATCH_ANYWHERE &&
-        store.matchBehavior != MATCH_BOUNDARY &&
-        store.matchBehavior != MATCH_BEGINNING) {
-      store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
-    }
-
-  }
-
-  let store = {
-    _ignoreNotifications: false,
-    observe(subject, topic, data) {
-      // Avoid re-entrancy when flipping linked preferences.
-      if (this._ignoreNotifications)
-        return;
-      this._ignoreNotifications = true;
-      loadPrefs(subject, topic, data);
-      this._ignoreNotifications = false;
     },
     QueryInterface: XPCOMUtils.generateQI([
       Ci.nsIObserver,
-      Ci.nsISupportsWeakReference ])
+      Ci.nsISupportsWeakReference
+    ])
   };
-
-  // Synchronize suggest.* prefs with autocomplete.enabled at initialization
-  syncEnabledPref();
-
-  loadPrefs();
-  Services.prefs.addObserver(PREF_BRANCH, store);
+  Services.prefs.addObserver(PREF_URLBAR_BRANCH, store, true);
   Services.prefs.addObserver("keyword.enabled", store, true);
 
-  return Object.seal(store);
+  // On startup we must check that some prefs are linked.
+  updateLinkedPrefs();
+  return store;
 });
 
 // Preloaded Sites related
 
 function PreloadedSite(url, title) {
   this.uri = Services.io.newURI(url);
   this.title = title;
   this._matchTitle = title.toLowerCase();
@@ -787,20 +787,20 @@ function Search(searchString, searchPara
     textURIService.unEscapeURIForUI("UTF-8", strippedOriginalSearchString);
 
   // The protocol and the host are lowercased by nsIURI, so it's fine to
   // lowercase the typed prefix, to add it back to the results later.
   this._strippedPrefix = this._trimmedOriginalSearchString.slice(
     0, this._trimmedOriginalSearchString.length - strippedOriginalSearchString.length
   ).toLowerCase();
 
-  this._matchBehavior = Prefs.matchBehavior;
+  this._matchBehavior = Prefs.get("matchBehavior");
   // Set the default behavior for this search.
-  this._behavior = this._searchString ? Prefs.defaultBehavior
-                                      : Prefs.emptySearchDefaultBehavior;
+  this._behavior = this._searchString ? Prefs.get("defaultBehavior")
+                                      : Prefs.get("emptySearchDefaultBehavior");
 
   let params = new Set(searchParam.split(" "));
   this._enableActions = params.has("enable-actions");
   this._disablePrivateActions = params.has("disable-private-actions");
   this._inPrivateWindow = params.has("private-window");
   this._prohibitAutoFill = params.has("prohibit-autofill");
 
   let userContextId = searchParam.match(REGEXP_USER_CONTEXT_ID);
@@ -881,26 +881,27 @@ Search.prototype = {
 
     return this._behavior & behavior;
   },
 
   /**
    * Used to delay the most complex queries, to save IO while the user is
    * typing.
    */
-  _sleepDeferred: null,
+  _sleepResolve: null,
   _sleep(aTimeMs) {
     // Reuse a single instance to try shaving off some usless work before
     // the first query.
     if (!this._sleepTimer)
       this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    this._sleepDeferred = PromiseUtils.defer();
-    this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
-                                      aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
-    return this._sleepDeferred.promise;
+    return new Promise(resolve => {
+      this._sleepResolve = resolve;
+      this._sleepTimer.initWithCallback(resolve, aTimeMs,
+                                        Ci.nsITimer.TYPE_ONE_SHOT);
+    });
   },
 
   /**
    * Given an array of tokens, this function determines which query should be
    * ran.  It also removes any special search tokens.
    *
    * @param tokens
    *        An array of search tokens.
@@ -925,34 +926,34 @@ Search.prototype = {
         this.setBehavior(behavior);
         tokens.splice(i, 1);
       }
     }
 
     // Set the right JavaScript behavior based on our preference.  Note that the
     // preference is whether or not we should filter JavaScript, and the
     // behavior is if we should search it or not.
-    if (!Prefs.filterJavaScript) {
+    if (!Prefs.get("filter.javascript")) {
       this.setBehavior("javascript");
     }
 
     return tokens;
   },
 
   /**
    * Stop this search.
    * After invoking this method, we won't run any more searches or heuristics,
    * and no new matches may be added to the current result.
    */
   stop() {
     if (this._sleepTimer)
       this._sleepTimer.cancel();
-    if (this._sleepDeferred) {
-      this._sleepDeferred.resolve();
-      this._sleepDeferred = null;
+    if (this._sleepResolve) {
+      this._sleepResolve();
+      this._sleepResolve = null;
     }
     if (this._searchSuggestionController) {
       this._searchSuggestionController.stop();
       this._searchSuggestionController = null;
     }
     this.pending = false;
   },
 
@@ -1027,24 +1028,25 @@ Search.prototype = {
       return;
 
     // We sleep a little between adding the heuristicFirstMatch and matching
     // any other searches so we aren't kicking off potentially expensive
     // searches on every keystroke.
     // Though, if there's no heuristic result, we start searching immediately,
     // since autocomplete may be waiting for us.
     if (hasHeuristic) {
-      await this._sleep(Prefs.delay);
+      await this._sleep(Prefs.get("delay"));
       if (!this.pending)
         return;
     }
 
     // Only add extension suggestions if the first token is a registered keyword
     // and the search string has characters after the first token.
-    if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+    if (this._searchTokens.length > 0 &&
+        ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
         this._originalSearchString.length > this._searchTokens[0].length) {
       await this._matchExtensionSuggestions();
       if (!this.pending)
         return;
     } else if (ExtensionSearchHandler.hasActiveInputSession()) {
       ExtensionSearchHandler.handleInputCancelled();
     }
 
@@ -1065,17 +1067,17 @@ Search.prototype = {
       if (!this.pending)
         return;
     }
 
     // If we do not have enough results, and our match type is
     // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
     // results.
     if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
-        this._counts[MATCHTYPE.GENERAL] < Prefs.maxRichResults) {
+        this._counts[MATCHTYPE.GENERAL] < Prefs.get("maxRichResults")) {
       this._matchBehavior = MATCH_ANYWHERE;
       for (let [query, params] of [ this._adaptiveQuery,
                                     this._searchQuery ]) {
         await conn.executeCached(query, params, this._onResultRow.bind(this));
         if (!this.pending)
           return;
       }
     }
@@ -1084,26 +1086,26 @@ Search.prototype = {
 
     // Ensure to fill any remaining space. Suggestions which come from extensions are
     // inserted at the beginning, so any suggestions
     await Promise.all(this._allMatchesPromises);
   },
 
 
   async _checkPreloadedSitesExpiry() {
-    if (!Prefs.preloadedSitesEnabled)
+    if (!Prefs.get("usepreloadedtopurls.enabled"))
       return;
     let profileCreationDate = await ProfileAgeCreatedPromise;
     let daysSinceProfileCreation = (Date.now() - profileCreationDate) / MS_PER_DAY;
-    if (daysSinceProfileCreation > Prefs.preloadedSitesExpireDays)
+    if (daysSinceProfileCreation > Prefs.get("usepreloadedtopurls.expire_days"))
       Services.prefs.setBoolPref("browser.urlbar.usepreloadedtopurls.enabled", false);
   },
 
   _matchPreloadedSites() {
-    if (!Prefs.preloadedSitesEnabled)
+    if (!Prefs.get("usepreloadedtopurls.enabled"))
       return;
 
     // In case user typed just "https://" or "www." or "https://www."
     // - we do not put out the whole lot of sites
     if (!this._searchString)
       return;
 
     if (!(this._searchStringScheme === "" ||
@@ -1132,17 +1134,17 @@ Search.prototype = {
       }
     }
     for (let match of [...strictMatches, ...looseMatches]) {
       this._addMatch(match);
     }
   },
 
   _matchPreloadedSiteForAutofill() {
-    if (!Prefs.preloadedSitesEnabled)
+    if (!Prefs.get("usepreloadedtopurls.enabled"))
       return false;
 
     if (!(this._searchStringScheme === "" ||
           this._searchStringScheme === "https" ||
           this._searchStringScheme === "http"))
       return false;
 
     let searchStringSchemePrefix = this._searchStringScheme
@@ -1266,17 +1268,17 @@ Search.prototype = {
       let matched = await this._matchUnknownUrl();
       if (matched) {
         // Since we can't tell if this is a real URL and
         // whether the user wants to visit or search for it,
         // we always provide an alternative searchengine match.
         try {
           new URL(this._originalSearchString);
         } catch (ex) {
-          if (Prefs.keywordEnabled && !looksLikeUrl(this._originalSearchString, true)) {
+          if (Prefs.get("keyword.enabled") && !looksLikeUrl(this._originalSearchString, true)) {
             this._addingHeuristicFirstMatch = false;
             await this._matchCurrentSearchEngine();
             this._addingHeuristicFirstMatch = true;
           }
         }
         return true;
       }
     }
@@ -1291,30 +1293,30 @@ Search.prototype = {
     }
 
     return false;
   },
 
   async _matchSearchSuggestions() {
     // Limit the string sent for search suggestions to a maximum length.
     let searchString = this._searchTokens.join(" ")
-                           .substr(0, Prefs.maxCharsForSearchSuggestions);
+                           .substr(0, Prefs.get("maxCharsForSearchSuggestions"));
     // Avoid fetching suggestions if they are not required, private browsing
     // mode is enabled, or the search string may expose sensitive information.
     if (!this.hasBehavior("searches") || this._inPrivateWindow ||
         this._prohibitSearchSuggestionsFor(searchString)) {
       return;
     }
 
     this._searchSuggestionController =
       PlacesSearchAutocompleteProvider.getSuggestionController(
         searchString,
         this._inPrivateWindow,
-        Prefs.maxHistoricalSearchSuggestions,
-        Prefs.maxRichResults - Prefs.maxHistoricalSearchSuggestions,
+        Prefs.get("maxHistoricalSearchSuggestions"),
+        Prefs.get("maxRichResults") - Prefs.get("maxHistoricalSearchSuggestions"),
         this._userContextId
       );
     let promise = this._searchSuggestionController.fetchCompletePromise
       .then(() => {
         // The search has been canceled already.
         if (!this._searchSuggestionController)
           return;
         if (this._searchSuggestionController.resultsCount >= 0 &&
@@ -1454,17 +1456,17 @@ Search.prototype = {
       icon: "page-icon:" + entry.url.href,
       style,
       frecency: Infinity
     });
     return true;
   },
 
   async _matchSearchEngineUrl() {
-    if (!Prefs.autofillSearchEngines)
+    if (!Prefs.get("autoFill.searchEngines"))
       return false;
 
     let match = await PlacesSearchAutocompleteProvider.findMatchByToken(
                                                            this._searchString);
     if (!match)
       return false;
 
     // The match doesn't contain a 'scheme://www.' prefix, but since we have
@@ -1630,17 +1632,17 @@ Search.prototype = {
   _matchUnknownUrl() {
     let flags = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
                 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
     let fixupInfo = null;
     try {
       fixupInfo = Services.uriFixup.getFixupURIInfo(this._originalSearchString,
                                                     flags);
     } catch (e) {
-      if (e.result == Cr.NS_ERROR_MALFORMED_URI && !Prefs.keywordEnabled) {
+      if (e.result == Cr.NS_ERROR_MALFORMED_URI && !Prefs.get("keyword.enabled")) {
         let value = PlacesUtils.mozActionURI("visiturl", {
           url: this._originalSearchString,
           input: this._originalSearchString,
         });
         this._addMatch({
           value,
           comment: this._originalSearchString,
           style: "action visiturl",
@@ -1721,17 +1723,17 @@ Search.prototype = {
         break;
       case QUERYTYPE_FILTERED:
         match = this._processRow(row);
         break;
     }
     this._addMatch(match);
     // If the search has been canceled by the user or by _addMatch, or we
     // fetched enough results, we can stop the underlying Sqlite query.
-    if (!this.pending || this._counts[MATCHTYPE.GENERAL] == Prefs.maxRichResults)
+    if (!this.pending || this._counts[MATCHTYPE.GENERAL] == Prefs.get("maxRichResults"))
       throw StopIteration;
   },
 
   _maybeRestyleSearchMatch(match) {
     // Return if the URL does not represent a search result.
     let parseResult =
       PlacesSearchAutocompleteProvider.parseSubmissionURL(match.value);
     if (!parseResult) {
@@ -1802,17 +1804,17 @@ Search.prototype = {
     // are faster too.
     if (match.placeId)
       this._usedPlaceIds.add(match.placeId);
     this._usedURLs.add(urlMapKey);
 
     match.style = match.style || "favicon";
 
     // Restyle past searches, unless they are bookmarks or special results.
-    if (Prefs.restyleSearches && match.style == "favicon") {
+    if (Prefs.get("restyleSearches") && match.style == "favicon") {
       this._maybeRestyleSearchMatch(match);
     }
 
     if (this._addingHeuristicFirstMatch) {
       match.style += " heuristic";
     }
 
     match.icon = match.icon || "";
@@ -1833,18 +1835,18 @@ Search.prototype = {
   },
 
   _getInsertIndexForMatch(match) {
     let index = 0;
     // The buckets change depending on the context, that is currently decided by
     // the first added match (the heuristic one).
     if (!this._buckets) {
       // Convert the buckets to readable objects with a count property.
-      let buckets = match.style.includes("searchengine") ? Prefs.matchBucketsSearch
-                                                         : Prefs.matchBuckets;
+      let buckets = match.style.includes("searchengine") ? Prefs.get("matchBucketsSearch")
+                                                         : Prefs.get("matchBuckets");
       this._buckets = buckets.map(([type, available]) => ({ type,
                                                             available,
                                                             count: 0,
                                                           }));
     }
     for (let bucket of this._buckets) {
       // Move to the next bucket if the match type is incompatible, or if there
       // is no available space or if the frecency is below the threshold.
@@ -2041,17 +2043,17 @@ Search.prototype = {
         matchBehavior: this._matchBehavior,
         searchBehavior: this._behavior,
         // We only want to search the tokens that we are left with - not the
         // original search string.
         searchString: this._searchTokens.join(" "),
         userContextId: this._userContextId,
         // Limit the query to the the maximum number of desired results.
         // This way we can avoid doing more work than needed.
-        maxResults: Prefs.maxRichResults
+        maxResults: Prefs.get("maxRichResults")
       }
     ];
   },
 
   /**
    * Obtains the query to search for switch-to-tab entries.
    *
    * @return an array consisting of the correctly optimized query to search the
@@ -2063,17 +2065,17 @@ Search.prototype = {
       {
         query_type: QUERYTYPE_FILTERED,
         matchBehavior: this._matchBehavior,
         searchBehavior: this._behavior,
         // We only want to search the tokens that we are left with - not the
         // original search string.
         searchString: this._searchTokens.join(" "),
         userContextId: this._userContextId,
-        maxResults: Prefs.maxRichResults
+        maxResults: Prefs.get("maxRichResults")
       }
     ];
   },
 
   /**
    * Obtains the query to search for adaptive results.
    *
    * @return an array consisting of the correctly optimized query to search the
@@ -2093,17 +2095,17 @@ Search.prototype = {
     ];
   },
 
   /**
    * Whether we should try to autoFill.
    */
   get _shouldAutofill() {
     // First of all, check for the autoFill pref.
-    if (!Prefs.autofill)
+    if (!Prefs.get("autoFill"))
       return false;
 
     if (this._searchTokens.length != 1)
       return false;
 
     // autoFill can only cope with history or bookmarks entries.
     if (!this.hasBehavior("history") &&
         !this.hasBehavior("bookmark"))
@@ -2131,17 +2133,17 @@ Search.prototype = {
 
   /**
    * Obtains the query to search for autoFill host results.
    *
    * @return an array consisting of the correctly optimized query to search the
    *         database with and an object containing the params to bound.
    */
   get _hostQuery() {
-    let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+    let typed = Prefs.get("autoFill.typed") || this.hasBehavior("typed");
     let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
 
     let query = [];
     if (bookmarked) {
       query.push(typed ? SQL_BOOKMARKED_TYPED_HOST_QUERY
                        : SQL_BOOKMARKED_HOST_QUERY);
     } else {
       query.push(typed ? SQL_TYPED_HOST_QUERY
@@ -2172,17 +2174,17 @@ Search.prototype = {
     let revHost = this._trimmedOriginalSearchString
                       .substring(this._strippedPrefix.length, pathIndex)
                       .toLowerCase().split("").reverse().join("") + ".";
     let searchString = stripPrefix(
       this._trimmedOriginalSearchString.slice(0, pathIndex).toLowerCase() +
       this._trimmedOriginalSearchString.slice(pathIndex)
     );
 
-    let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+    let typed = Prefs.get("autoFill.typed") || this.hasBehavior("typed");
     let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
 
     let query = [];
     if (bookmarked) {
       query.push(typed ? SQL_BOOKMARKED_TYPED_URL_QUERY
                        : SQL_BOOKMARKED_URL_QUERY);
     } else {
       query.push(typed ? SQL_TYPED_URL_QUERY
@@ -2220,17 +2222,17 @@ Search.prototype = {
 
 function UnifiedComplete() {
   // Make sure the preferences are initialized as soon as possible.
   // If the value of browser.urlbar.autocomplete.enabled is set to false,
   // then all the other suggest preferences for history, bookmarks and
   // open pages should be set to false.
   Prefs;
 
-  if (Prefs.preloadedSitesEnabled) {
+  if (Prefs.get("usepreloadedtopurls.enabled")) {
     // force initializing the profile age check
     // to ensure the off-main-thread-IO happens ASAP
     // and we don't have to wait for it when doing an autocomplete lookup
     ProfileAgeCreatedPromise;
 
     fetch("chrome://global/content/unifiedcomplete-top-urls.json")
       .then(response => response.json())
       .then(sites => PreloadedSiteStorage.populate(sites))
@@ -2250,17 +2252,17 @@ UnifiedComplete.prototype = {
   /**
    * Gets a Sqlite database handle.
    *
    * @return {Promise}
    * @resolves to the Sqlite database handle (according to Sqlite.jsm).
    * @rejects javascript exception.
    */
   getDatabaseHandle() {
-    if (Prefs.enabled && !this._promiseDatabase) {
+    if (Prefs.get("autocomplete.enabled") && !this._promiseDatabase) {
       this._promiseDatabase = (async function() {
         let conn = await Sqlite.cloneStorageConnection({
           connection: PlacesUtils.history.DBConnection,
           readOnly: true
         });
 
         try {
            Sqlite.shutdown.addBlocker("Places UnifiedComplete.js clone closing",
@@ -2314,26 +2316,26 @@ UnifiedComplete.prototype = {
     }
 
     // Note: We don't use previousResult to make sure ordering of results are
     //       consistent.  See bug 412730 for more details.
 
     // If the previous search didn't fetch enough search suggestions, it's
     // unlikely a longer text would do.
     let prohibitSearchSuggestions =
-      this._lastLowResultsSearchSuggestion &&
+      !!this._lastLowResultsSearchSuggestion &&
       searchString.length > this._lastLowResultsSearchSuggestion.length &&
       searchString.startsWith(this._lastLowResultsSearchSuggestion);
 
     this._currentSearch = new Search(searchString, searchParam, listener,
                                      this, this, prohibitSearchSuggestions);
 
     // If we are not enabled, we need to return now.  Notice we need an empty
     // result regardless, so we still create the Search object.
-    if (!Prefs.enabled) {
+    if (!Prefs.get("autocomplete.enabled")) {
       this.finishSearch(true);
       return;
     }
 
     let search = this._currentSearch;
     this.getDatabaseHandle().then(conn => search.execute(conn))
                             .catch(ex => {
                               dump(`Query failed: ${ex}\n`);
--- a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
@@ -22,17 +22,17 @@ add_task(async function test_enabled() {
   await check_autocomplete({
     search: "url",
     matches: [ { uri, title: "title" } ]
   });
 
   await cleanup();
 });
 
-add_task(async function test_sync_enabled() {
+add_task(async function test_linked_enabled_prefs() {
   // Initialize unified complete.
   Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
     .getService(Ci.mozIPlacesAutoComplete);
 
   let types = [ "history", "bookmark", "openpage", "searches" ];
 
   // Test the service keeps browser.urlbar.autocomplete.enabled synchronized
   // with browser.urlbar.suggest prefs.