Bug 1402286 - chunk notifyResults calls so that we don't run all the autocomplete js for each match. draft
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 31 Oct 2017 11:13:47 +0100
changeset 695529 5c709fd966acbafbd657ed4781d8baa067a37c82
parent 695523 d16b52f5d1955192c42c7b5c5da4e05a7dffef27
child 739630 0a4905be91e79325fa0444c02bf0ec9e4f562bc3
push id88456
push usermak77@bonardo.net
push dateThu, 09 Nov 2017 11:18:30 +0000
bugs1402286
milestone58.0a1
Bug 1402286 - chunk notifyResults calls so that we don't run all the autocomplete js for each match. MozReview-Commit-ID: GuYew5B30WQ
browser/base/content/test/performance/browser_urlbar_keyed_search_reflows.js
browser/base/content/test/performance/browser_urlbar_search_reflows.js
browser/base/content/test/urlbar/browser_urlbarOneOffs.js
browser/base/content/test/urlbar/head.js
browser/components/extensions/test/browser/browser_ext_omnibox.js
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
--- a/browser/base/content/test/performance/browser_urlbar_keyed_search_reflows.js
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search_reflows.js
@@ -40,16 +40,17 @@ const EXPECTED_REFLOWS_FIRST_OPEN = [
     times: 5, // This number should only ever go down - never up.
   },
 
   {
     stack: [
       "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
     ],
+    minTimes: 39, // This number should only ever go down - never up.
     times: 51, // This number should only ever go down - never up.
   },
 
   {
     stack: [
       "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
       "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
       "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
--- a/browser/base/content/test/performance/browser_urlbar_search_reflows.js
+++ b/browser/base/content/test/performance/browser_urlbar_search_reflows.js
@@ -52,17 +52,17 @@ const EXPECTED_REFLOWS_FIRST_OPEN = [
     stack: [
       "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
       "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
       "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
       "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate@chrome://global/content/bindings/autocomplete.xml",
       "invalidate@chrome://global/content/bindings/autocomplete.xml"
     ],
-    times: 60, // This number should only ever go down - never up.
+    times: 36, // This number should only ever go down - never up.
   },
 
   {
     stack: [
       "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
       "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
       "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
@@ -97,16 +97,28 @@ const EXPECTED_REFLOWS_SECOND_OPEN = [
   {
     stack: [
       "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
     ],
     times: 3, // This number should only ever go down - never up.
   },
 
+  {
+    stack: [
+      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
+      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate@chrome://global/content/bindings/autocomplete.xml",
+      "invalidate@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 24, // This number should only ever go down - never up.
+  },
+
   // Bug 1359989
   {
     stack: [
       "openPopup@chrome://global/content/bindings/popup.xml",
       "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openPopup@chrome://global/content/bindings/autocomplete.xml",
       "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
--- a/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -31,16 +31,17 @@ add_task(async function init() {
 });
 
 // Keys up and down through the history panel, i.e., the panel that's shown when
 // there's no text in the textbox.
 add_task(async function history() {
   gURLBar.focus();
   EventUtils.synthesizeKey("VK_DOWN", {});
   await promisePopupShown(gURLBar.popup);
+  await waitForAutocompleteResultAt(gMaxResults - 1)
 
   assertState(-1, -1, "");
 
   // Key down through each result.
   for (let i = 0; i < gMaxResults; i++) {
     EventUtils.synthesizeKey("VK_DOWN", {});
     assertState(i, -1,
       "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
@@ -101,17 +102,17 @@ add_task(async function history() {
 
 // Keys up and down through the non-history panel, i.e., the panel that's shown
 // when you type something in the textbox.
 add_task(async function() {
   // Use a typed value that returns the visits added above but that doesn't
   // trigger autofill since that would complicate the test.
   let typedValue = "browser_urlbarOneOffs";
   await promiseAutocompleteResultPopup(typedValue, window, true);
-
+  await waitForAutocompleteResultAt(gMaxResults - 1);
   assertState(0, -1, typedValue);
 
   // Key down through each result.  The first result is already selected, which
   // is why gMaxResults - 1 is the correct number of times to do this.
   for (let i = 0; i < gMaxResults - 1; i++) {
     EventUtils.synthesizeKey("VK_DOWN", {});
     // i starts at zero so that the textValue passed to assertState is correct.
     // But that means that i + 1 is the expected selected index, since initially
@@ -153,17 +154,17 @@ add_task(async function() {
   await hidePopup();
 });
 
 // Checks that "Search with Current Search Engine" items are updated to "Search
 // with One-Off Engine" when a one-off is selected.
 add_task(async function searchWith() {
   let typedValue = "foo";
   await promiseAutocompleteResultPopup(typedValue);
-
+  await waitForAutocompleteResultAt(0);
   assertState(0, -1, typedValue);
 
   let item = gURLBar.popup.richlistbox.firstChild;
   Assert.equal(item._actionText.textContent,
                "Search with " + Services.search.currentEngine.name,
                "Sanity check: first result's action text");
 
   // Alt+Down to the first one-off.  Now the first result and the first one-off
@@ -185,17 +186,17 @@ add_task(async function searchWith() {
 // Clicks a one-off.
 add_task(async function oneOffClick() {
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
 
   // We are explicitly using something that looks like a url, to make the test
   // stricter. Even if it looks like a url, we should search.
   let typedValue = "foo.bar";
   await promiseAutocompleteResultPopup(typedValue);
-
+  await waitForAutocompleteResultAt(1);
   assertState(0, -1, typedValue);
 
   let oneOffs = gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true);
   let resultsPromise =
     BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
                                    "http://mochi.test:8888/?terms=foo.bar");
   EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
   await resultsPromise;
@@ -206,17 +207,17 @@ add_task(async function oneOffClick() {
 // Presses the Return key when a one-off is selected.
 add_task(async function oneOffReturn() {
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
 
   // We are explicitly using something that looks like a url, to make the test
   // stricter. Even if it looks like a url, we should search.
   let typedValue = "foo.bar";
   await promiseAutocompleteResultPopup(typedValue, window, true);
-
+  await waitForAutocompleteResultAt(1);
   assertState(0, -1, typedValue);
 
   // Alt+Down to select the first one-off.
   EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
   assertState(0, 0, typedValue);
 
   let resultsPromise =
     BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -315,10 +315,12 @@ function promiseSpeculativeConnection(ht
 }
 
 async function waitForAutocompleteResultAt(index) {
   let searchString = gURLBar.controller.searchString;
   await BrowserTestUtils.waitForCondition(
     () => gURLBar.popup.richlistbox.children.length > index &&
           gURLBar.popup.richlistbox.children[index].getAttribute("ac-text") == searchString,
     `Waiting for the autocomplete result for "${searchString}" at [${index}] to appear`);
+  // Ensure the addition is complete, for proper mouse events on the entries.
+  await new Promise(resolve => window.requestIdleCallback(resolve, {timeout: 1000}));
   return gURLBar.popup.richlistbox.children[index];
 }
--- a/browser/components/extensions/test/browser/browser_ext_omnibox.js
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -79,31 +79,45 @@ add_task(async function() {
         `Expected "${event}" to have fired with text: "${expected.text}".`);
     }
     if (expected.disposition) {
       is(actual.disposition, expected.disposition,
         `Expected "${event}" to have fired with disposition: "${expected.disposition}".`);
     }
   }
 
-  async function startInputSession() {
+  async function waitForAutocompleteResultAt(index) {
+    let searchString = gURLBar.controller.searchString;
+    await BrowserTestUtils.waitForCondition(
+      () => gURLBar.popup.richlistbox.children.length > index &&
+            gURLBar.popup.richlistbox.children[index].getAttribute("ac-text") == searchString,
+      `Waiting for the autocomplete result for "${searchString}" at [${index}] to appear`);
+    // Ensure the addition is complete, for proper mouse events on the entries.
+    await new Promise(resolve => window.requestIdleCallback(resolve, {timeout: 1000}));
+    return gURLBar.popup.richlistbox.children[index];
+  }
+
+  let inputSessionSerial = 0;
+  async function startInputSession(indexToWaitFor) {
     gURLBar.focus();
     gURLBar.value = keyword;
     EventUtils.synthesizeKey(" ", {});
     await expectEvent("on-input-started-fired");
-    EventUtils.synthesizeKey("t", {});
-    await expectEvent("on-input-changed-fired", {text: "t"});
+    // Always use a different input at every invokation, so that
+    // waitForAutocompleteResultAt can distinguish different cases.
+    let char = ((inputSessionSerial++) % 10).toString();
+    EventUtils.synthesizeKey(char, {});
+
+    await expectEvent("on-input-changed-fired", {text: char});
     // Wait for the autocomplete search. Note that we cannot wait for the search
     // to be complete, since the add-on doesn't communicate when it's done, so
     // just check matches count.
-    await BrowserTestUtils.waitForCondition(
-      () => gURLBar.controller.matchCount >= 2 &&
-            gURLBar.popup.richlistbox.children[1].getAttribute("ac-text") == gURLBar.controller.searchString,
-      "waiting urlbar search to complete");
-    return "t";
+    await waitForAutocompleteResultAt(indexToWaitFor);
+
+    return char;
   }
 
   async function testInputEvents() {
     gURLBar.focus();
 
     // Start an input session by typing in <keyword><space>.
     for (let letter of keyword) {
       EventUtils.synthesizeKey(letter, {});
@@ -169,17 +183,17 @@ add_task(async function() {
       extension.sendMessage("set-default-suggestion", {
         suggestion: {
           description: expectedText,
         },
       });
       await extension.awaitMessage("default-suggestion-set");
     }
 
-    let text = await startInputSession();
+    let text = await startInputSession(0);
 
     let item = gURLBar.popup.richlistbox.children[0];
 
     is(item.getAttribute("title"), expectedText,
       `Expected heuristic result to have title: "${expectedText}".`);
 
     is(item.getAttribute("displayurl"), `${keyword} ${text}`,
       `Expected heuristic result to have displayurl: "${keyword} ${text}".`);
@@ -188,17 +202,17 @@ add_task(async function() {
 
     await expectEvent("on-input-entered-fired", {
       text,
       disposition: "currentTab",
     });
   }
 
   async function testDisposition(suggestionIndex, expectedDisposition, expectedText) {
-    await startInputSession();
+    await startInputSession(suggestionIndex);
 
     // Select the suggestion.
     for (let i = 0; i < suggestionIndex; i++) {
       EventUtils.synthesizeKey("VK_DOWN", {});
     }
 
     let item = gURLBar.popup.richlistbox.children[suggestionIndex];
     if (expectedDisposition == "currentTab") {
@@ -224,17 +238,17 @@ add_task(async function() {
       ok(!!item, "Expected item to exist");
       is(item.getAttribute("title"), description,
         `Expected suggestion to have title: "${description}".`);
 
       is(item.getAttribute("displayurl"), `${keyword} ${content}`,
         `Expected suggestion to have displayurl: "${keyword} ${content}".`);
     }
 
-    let text = await startInputSession();
+    let text = await startInputSession(info.suggestions.length - 1);
 
     extension.sendMessage(info.test);
     await extension.awaitMessage("test-ready");
 
     info.suggestions.forEach(expectSuggestion);
 
     EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
     await expectEvent("on-input-entered-fired", {
@@ -269,22 +283,20 @@ add_task(async function() {
   await testDisposition(3, "newBackgroundTab", suggestions[2].content);
 
   extension.sendMessage("set-suggestions", {suggestions});
   await extension.awaitMessage("suggestions-set");
 
   // Test adding suggestions asynchronously.
   await testSuggestions({
     test: "test-multiple-suggest-calls",
-    skipHeuristic: true,
     suggestions,
   });
   await testSuggestions({
     test: "test-suggestions-after-delay",
-    skipHeuristic: true,
     suggestions,
   });
 
   // Start monitoring the console.
   let waitForConsole = new Promise(resolve => {
     SimpleTest.monitorConsole(resolve, [{
       message: new RegExp(`The keyword provided is already registered: "${keyword}"`),
     }]);
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -77,29 +77,32 @@ const FRECENCY_DEFAULT = 1000;
 
 // Extensions are allowed to add suggestions if they have registered a keyword
 // with the omnibox API. This is the maximum number of suggestions an extension
 // is allowed to add for a given search string.
 // This value includes the heuristic result.
 const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
 
 // After this time, we'll give up waiting for the extension to return matches.
-const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 5000;
+const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 3000;
 
 // A regex that matches "single word" hostnames for whitelisting purposes.
 // The hostname will already have been checked for general validity, so we
 // don't need to be exhaustive here, so allow dashes anywhere.
 const REGEXP_SINGLEWORD_HOST = new RegExp("^[a-z0-9-]+$", "i");
 
 // Regex used to match userContextId.
 const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
 
 // Regex used to match one or more whitespace.
 const REGEXP_SPACES = /\s+/;
 
+// The result is notified on a delay, to avoid rebuilding the panel at every match.
+const NOTIFYRESULT_DELAY_MS = 16;
+
 // Sqlite result row index constants.
 const QUERYINDEX_QUERYTYPE     = 0;
 const QUERYINDEX_URL           = 1;
 const QUERYINDEX_TITLE         = 2;
 const QUERYINDEX_BOOKMARKED    = 3;
 const QUERYINDEX_BOOKMARKTITLE = 4;
 const QUERYINDEX_TAGS          = 5;
 const QUERYINDEX_VISITCOUNT    = 6;
@@ -325,16 +328,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
                                    "@mozilla.org/intl/texttosuburi;1",
                                    "nsITextToSubURI");
 
 function setTimeout(callback, ms) {
   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
+  return timer;
 }
 
 function convertBucketsCharPrefToArray(str) {
   return str.split(",")
             .map(v => {
               let bucket = v.split(":");
               return [ bucket[0].trim().toLowerCase(), Number(bucket[1]) ];
             });
@@ -987,16 +991,19 @@ Search.prototype = {
    * 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() {
     // Avoid multiple calls or re-entrance.
     if (!this.pending)
       return;
+    if (this._notifyTimer)
+      this._notifyTimer.cancel();
+    this._notifyDelaysCount = 0;
     if (this._sleepTimer)
       this._sleepTimer.cancel();
     if (this._sleepResolve) {
       this._sleepResolve();
       this._sleepResolve = null;
     }
     if (this._searchSuggestionController) {
       this._searchSuggestionController.stop();
@@ -1122,17 +1129,16 @@ Search.prototype = {
         searchSuggestionsCompletePromise = this._matchSearchSuggestions(searchString);
         if (this.hasBehavior("restrict")) {
           // Wait for the suggestions to be added.
           await searchSuggestionsCompletePromise;
           this._cleanUpNonCurrentMatches(MATCHTYPE.SUGGESTION);
           // We're done if we're restricting to search suggestions.
           // Notify the result completion then stop the search.
           this._autocompleteSearch.finishSearch(true);
-          this.stop();
           return;
         }
       }
     }
     // In any case, clear previous suggestions.
     searchSuggestionsCompletePromise.then(() => {
       this._cleanUpNonCurrentMatches(MATCHTYPE.SUGGESTION);
     });
@@ -1662,19 +1668,18 @@ Search.prototype = {
     // Remove previous search matches sooner than the maximum timeout, otherwise
     // matches may appear stale for a long time.
     // This is necessary because WebExtensions don't have a method to notify
     // that they are done providing results, so they could be pending forever.
     setTimeout(() => this._cleanUpNonCurrentMatches(MATCHTYPE.EXTENSION), 100);
 
     // Since the extension has no way to signale when it's done pushing
     // results, we add a timeout racing with the addition.
-    let timeoutPromise = new Promise((resolve, reject) => {
-      setTimeout(() => reject(new Error("timeout waiting for the extension to add its results to the location bar")),
-                 MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
+    let timeoutPromise = new Promise(resolve => {
+      setTimeout(resolve, MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
     });
     return Promise.race([timeoutPromise, promise]).catch(Cu.reportError);
   },
 
   async _matchRemoteTabs() {
     let matches = await PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
     for (let {url, title, icon, deviceName} of matches) {
       // It's rare that Sync supplies the icon for the page (but if it does, it
@@ -1904,17 +1909,17 @@ Search.prototype = {
                                match.finalCompleteValue);
     this._currentMatchCount++;
     this._counts[match.type]++;
 
     if (this._currentMatchCount == 1)
       TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, this);
     if (this._currentMatchCount == 6)
       TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, this);
-    this.notifyResults(true);
+    this.notifyResult(true, match.type == MATCHTYPE.HEURISTIC);
   },
 
   _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.
@@ -2004,17 +2009,17 @@ Search.prototype = {
         while (bucket.count > bucket.insertIndex) {
           this._result.removeMatchAt(index);
           changed = true;
           bucket.count--;
         }
       }
     }
     if (changed && notify) {
-      this.notifyResults(true);
+      this.notifyResult(true);
     }
   },
 
   /**
    * If in restrict mode, cleanups non current matches for all the empty types.
    */
   cleanUpRestrictNonCurrentMatches() {
     if (this.hasBehavior("restrict") && this._previousSearchMatchTypes.length > 0) {
@@ -2359,34 +2364,57 @@ Search.prototype = {
       query_type: QUERYTYPE_AUTOFILL_URL,
       searchString,
       revHost
     });
 
     return query;
   },
 
- /**
-   * Notifies the listener about results.
+  // The result is notified to the search listener on a timer, to chunk multiple
+  // match updates together and avoid rebuilding the popup at every new match.
+  _notifyTimer: null,
+
+  /**
+   * Notifies the current result to the listener.
    *
    * @param searchOngoing
-   *        Indicates whether the search is ongoing.
+   *        Indicates whether the search result should be marked as ongoing.
+   * @param skipDelay
+   *        Whether to notify immediately.
    */
-  notifyResults(searchOngoing) {
-    let result = this._result;
-    let resultCode = this._currentMatchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
-    if (searchOngoing) {
-      resultCode += "_ONGOING";
+  _notifyDelaysCount: 0,
+  notifyResult(searchOngoing, skipDelay = false) {
+    let notify = () => {
+      this._notifyDelaysCount = 0;
+      let resultCode = this._currentMatchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+      if (searchOngoing) {
+        resultCode += "_ONGOING";
+      }
+      let result = this._result;
+      result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+      this._listener.onSearchResult(this._autocompleteSearch, result);
+      if (!searchOngoing) {
+        // Break possible cycles.
+        this._listener = null;
+        this._autocompleteSearch = null;
+        this.stop();
+      }
+    };
+    if (this._notifyTimer) {
+      this._notifyTimer.cancel();
     }
-    result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
-    this._listener.onSearchResult(this._autocompleteSearch, result);
-    if (!searchOngoing) {
-      // Break possible cycles.
-      this._listener = null;
-      this._autocompleteSearch = null;
+    // In the worst case, we may get evenly spaced matches that would end up
+    // delaying the UI by N_MATCHES * NOTIFYRESULT_DELAY_MS. Thus, we clamp the
+    // number of times we may delay matches.
+    if (skipDelay || this._notifyDelaysCount > 3) {
+      notify();
+    } else {
+      this._notifyDelaysCount++;
+      this._notifyTimer = setTimeout(notify, NOTIFYRESULT_DELAY_MS);
     }
   },
 };
 
 // UnifiedComplete class
 // component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
 
 function UnifiedComplete() {
@@ -2574,32 +2602,31 @@ UnifiedComplete.prototype = {
     let search = this._currentSearch;
     if (!search)
       return;
     this._lastLowResultsSearchSuggestion = search._lastLowResultsSearchSuggestion;
 
     if (!notify || !search.pending)
       return;
 
-
     // If we are in restrict mode and we reused the previous search results,
     // it's possible we didn't go through all the cleanup methods due to early
     // bailouts. Thus we could still have nonmatching results to remove.
     search.cleanUpRestrictNonCurrentMatches();
 
     // There is a possible race condition here.
     // When a search completes it calls finishSearch that notifies results
     // here.  When the controller gets the last result it fires
     // onSearchComplete.
     // If onSearchComplete immediately starts a new search it will set a new
     // _currentSearch, and on return the execution will continue here, after
-    // notifyResults.
-    // Thus, ensure that notifyResults is the last call in this method,
+    // notifyResult.
+    // Thus, ensure that notifyResult is the last call in this method,
     // otherwise you might be touching the wrong search.
-    search.notifyResults(false);
+    search.notifyResult(false);
   },
 
   // nsIAutoCompleteSearchDescriptor
 
   get searchType() {
     return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
   },
 
--- a/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
@@ -190,17 +190,20 @@ add_task(async function test_removes_sug
     name: extensionName,
     emit(message, text, id) {
       if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
         ExtensionSearchHandler.addSuggestions(keyword, id, [
           {content: "foo", description: "first suggestion"},
           {content: "bar", description: "second suggestion"},
           {content: "baz", description: "third suggestion"},
         ]);
-        controller.stopSearch();
+        // The API doesn't have a way to notify when addition is complete.
+        do_timeout(1000, () => {
+          controller.stopSearch();
+        });
       }
     }
   };
 
   ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
 
   await check_autocomplete({
     search: `${keyword} unmatched`,
@@ -297,18 +300,21 @@ add_task(async function test_setting_the
   let keyword = "test";
   let extensionName = "Omnibox Example";
 
   let mockExtension = {
     name: extensionName,
     emit(message, text, id) {
       if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
         ExtensionSearchHandler.addSuggestions(keyword, id, []);
+        // The API doesn't have a way to notify when addition is complete.
+        do_timeout(1000, () => {
+          controller.stopSearch();
+        });
       }
-      controller.stopSearch();
     }
   };
 
   ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
 
   ExtensionSearchHandler.setDefaultSuggestion(keyword, {
     description: "hello world"
   });
@@ -353,17 +359,20 @@ add_task(async function test_maximum_num
           {content: "d", description: "fourth suggestion"},
           {content: "e", description: "fifth suggestion"},
           {content: "f", description: "sixth suggestion"},
           {content: "g", description: "seventh suggestion"},
           {content: "h", description: "eigth suggestion"},
           {content: "i", description: "ninth suggestion"},
           {content: "j", description: "tenth suggestion"},
         ]);
-        controller.stopSearch();
+        // The API doesn't have a way to notify when addition is complete.
+        do_timeout(1000, () => {
+          controller.stopSearch();
+        });
       }
     }
   };
 
   ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
 
   // Start an input session before testing MSG_INPUT_CHANGED.
   ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});