Bug 891439 - Standardize the sheduleSearch/performSearch methods, r=past
authorVictor Porof <vporof@mozilla.com>
Fri, 13 Sep 2013 16:23:13 +0300
changeset 159975 c5b199404e8a8757396baedc511496c806d1796c
parent 159974 ba5df19635daf8f3e6cf16b9d70c629d098f3e4a
child 159976 ead76fb1a864c7b411fc4fb7e162c28352079a6b
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs891439
milestone26.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 891439 - Standardize the sheduleSearch/performSearch methods, r=past
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/netmonitor/netmonitor-controller.js
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/ViewHelpers.jsm
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -921,17 +921,16 @@ function SourceScripts() {
   this._onNewSource = this._onNewSource.bind(this);
   this._onSourcesAdded = this._onSourcesAdded.bind(this);
   this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
 }
 
 SourceScripts.prototype = {
   get activeThread() DebuggerController.activeThread,
   get debuggerClient() DebuggerController.client,
-  _newSourceTimeout: null,
   _cache: new Map(),
 
   /**
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("SourceScripts is connecting...");
     this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
@@ -943,31 +942,32 @@ SourceScripts.prototype = {
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("SourceScripts is disconnecting...");
-    window.clearTimeout(this._newSourceTimeout);
     this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.removeListener("newSource", this._onNewSource);
     this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   _handleTabNavigation: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("Handling tab navigation in the SourceScripts");
-    window.clearTimeout(this._newSourceTimeout);
+
+    // Don't expect the old sources to matter after the tab navigated.
+    clearNamedTimeout("new-source");
 
     // Retrieve the list of script sources known to the server from before
     // the client was ready to handle "newSource" notifications.
     this._cache.clear();
     this.activeThread.getSources(this._onSourcesAdded);
   },
 
   /**
@@ -992,24 +992,23 @@ SourceScripts.prototype = {
 
     // Select this source if it's the preferred one.
     let preferredValue = DebuggerView.Sources.preferredValue;
     if (aPacket.source.url == preferredValue) {
       DebuggerView.Sources.selectedValue = preferredValue;
     }
     // ..or the first entry if there's none selected yet after a while
     else {
-      window.clearTimeout(this._newSourceTimeout);
-      this._newSourceTimeout = window.setTimeout(() => {
+      setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
         // If after a certain delay the preferred source still wasn't received,
         // just give up on waiting and display the first entry.
         if (!DebuggerView.Sources.selectedValue) {
           DebuggerView.Sources.selectedIndex = 0;
         }
-      }, NEW_SOURCE_DISPLAY_DELAY);
+      });
     }
 
     // If there are any stored breakpoints for this source, display them again,
     // both in the editor and the breakpoints pane.
     DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that a new source has been added.
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1463,18 +1463,16 @@ WatchExpressionsView.prototype = Heritag
 });
 
 /**
  * Functions handling the global search UI.
  */
 function GlobalSearchView() {
   dumpn("GlobalSearchView was instantiated");
 
-  this._startSearch = this._startSearch.bind(this);
-  this._performGlobalSearch = this._performGlobalSearch.bind(this);
   this._createItemView = this._createItemView.bind(this);
   this._onHeaderClick = this._onHeaderClick.bind(this);
   this._onLineClick = this._onLineClick.bind(this);
   this._onMatchClick = this._onMatchClick.bind(this);
 }
 
 GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
   /**
@@ -1553,83 +1551,57 @@ GlobalSearchView.prototype = Heritage.ex
       this._currentlyFocusedMatch = totalLineResults - 1;
     }
     this._onMatchClick({
       target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
     });
   },
 
   /**
-   * Allows searches to be scheduled and delayed to avoid redundant calls.
-   */
-  delayedSearch: true,
-
-  /**
    * Schedules searching for a string in all of the sources.
    *
-   * @param string aQuery
-   *        The string to search for.
-   */
-  scheduleSearch: function(aQuery) {
-    if (!this.delayedSearch) {
-      this.performSearch(aQuery);
-      return;
-    }
-    let delay = Math.max(GLOBAL_SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
-
-    window.clearTimeout(this._searchTimeout);
-    this._searchFunction = this._startSearch.bind(this, aQuery);
-    this._searchTimeout = window.setTimeout(this._searchFunction, delay);
-  },
-
-  /**
-   * Immediately searches for a string in all of the sources.
-   *
-   * @param string aQuery
+   * @param string aToken
    *        The string to search for.
+   * @param number aWait
+   *        The amount of milliseconds to wait until draining.
    */
-  performSearch: function(aQuery) {
-    window.clearTimeout(this._searchTimeout);
-    this._searchFunction = null;
-    this._startSearch(aQuery);
-  },
+  scheduleSearch: function(aToken, aWait) {
+    // The amount of time to wait for the requests to settle.
+    let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
+    let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
 
-  /**
-   * Starts searching for a string in all of the sources.
-   *
-   * @param string aQuery
-   *        The string to search for.
-   */
-  _startSearch: function(aQuery) {
-    this._searchedToken = aQuery;
-
-    // Start fetching as many sources as possible, then perform the search.
-    DebuggerController.SourceScripts
-      .getTextForSources(DebuggerView.Sources.values)
-      .then(this._performGlobalSearch);
+    // Allow requests to settle down first.
+    setNamedTimeout("global-search", delay, () => {
+      // Start fetching as many sources as possible, then perform the search.
+      let urls = DebuggerView.Sources.values;
+      let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls);
+      sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
+    });
   },
 
   /**
    * Finds string matches in all the sources stored in the controller's cache,
-   * and groups them by location and line number.
+   * and groups them by url and line number.
+   *
+   * @param string aToken
+   *        The string to search for.
+   * @param array aSources
+   *        An array of [url, text] tuples for each source.
    */
-  _performGlobalSearch: function(aSources) {
-    // Get the currently searched token from the filtering input.
-    let token = this._searchedToken;
-
-    // Make sure we're actually searching for something.
-    if (!token) {
+  _doSearch: function(aToken, aSources) {
+    // Don't continue filtering if the searched token is an empty string.
+    if (!aToken) {
       this.clearView();
       window.dispatchEvent(document, "Debugger:GlobalSearch:TokenEmpty");
       return;
     }
 
     // Search is not case sensitive, prepare the actual searched token.
-    let lowerCaseToken = token.toLowerCase();
-    let tokenLength = token.length;
+    let lowerCaseToken = aToken.toLowerCase();
+    let tokenLength = aToken.length;
 
     // Create a Map containing search details for each source.
     let globalResults = new GlobalResults();
 
     // Search for the specified token in each source's text.
     for (let [url, text] of aSources) {
       // Verify that the search token is found anywhere in the source.
       if (!text.toLowerCase().contains(lowerCaseToken)) {
@@ -1663,17 +1635,17 @@ GlobalSearchView.prototype = Heritage.ex
           // get the actual matched text from the original line's text.
           if (aIndex != aArray.length - 1) {
             let matched = aString.substr(prevLength + currLength, tokenLength);
             let range = { start: prevLength + currLength, length: matched.length };
             lineResults.add(matched, range, true);
           }
 
           // Continue with the next sub-region in this line's text.
-          return aPrev + token + aCurr;
+          return aPrev + aToken + aCurr;
         }, "");
 
         if (lineResults.matchCount) {
           sourceResults.add(lineResults);
         }
       });
 
       if (sourceResults.matchCount) {
@@ -1819,20 +1791,17 @@ GlobalSearchView.prototype = Heritage.ex
       });
       aMatch.setAttribute("focused", "");
     }}, 0);
     aMatch.setAttribute("focusing", "");
   },
 
   _splitter: null,
   _currentlyFocusedMatch: -1,
-  _forceExpandResults: false,
-  _searchTimeout: null,
-  _searchFunction: null,
-  _searchedToken: ""
+  _forceExpandResults: false
 });
 
 /**
  * An object containing all source results, grouped by source location.
  * Iterable via "for (let [location, sourceResults] in globalResults) { }".
  */
 function GlobalResults() {
   this._store = [];
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -630,18 +630,18 @@ StackFramesView.prototype = Heritage.ext
   /**
    * The scroll listener for the stackframes container.
    */
   _onScroll: function() {
     // Update the stackframes container only if we have to.
     if (!this.dirty) {
       return;
     }
-    window.clearTimeout(this._scrollTimeout);
-    this._scrollTimeout = window.setTimeout(this._afterScroll, STACK_FRAMES_SCROLL_DELAY);
+    // Allow requests to settle down first.
+    setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll);
   },
 
   /**
    * Requests the addition of more frames from the controller.
    */
   _afterScroll: function() {
     // TODO: Accessing private widget properties. Figure out what's the best
     // way to expose such things. Bug 876271.
@@ -657,17 +657,16 @@ StackFramesView.prototype = Heritage.ext
 
       // Loads more stack frames from the debugger server cache.
       DebuggerController.StackFrames.addMoreFrames();
     }
   },
 
   _commandset: null,
   _menupopup: null,
-  _scrollTimeout: null,
   _prevBlackBoxedUrl: null
 });
 
 /**
  * Utility functions for handling stackframes.
  */
 let StackFrameUtils = {
   /**
@@ -1149,43 +1148,43 @@ FilterView.prototype = {
       }
       this._prevSearchedFile = file;
       return;
     }
 
     // Perform a global search based on the specified operator.
     if (isGlobal) {
       if (isReturnKey && (isDifferentToken || DebuggerView.GlobalSearch.hidden)) {
-        DebuggerView.GlobalSearch.performSearch(token);
+        DebuggerView.GlobalSearch.scheduleSearch(token, 0);
       } else {
         DebuggerView.GlobalSearch[["selectNext", "selectPrev"][action]]();
       }
       this._prevSearchedToken = token;
       return;
     }
 
     // Perform a function search based on the specified operator.
     if (isFunction) {
       if (isReturnKey && (isDifferentToken || DebuggerView.FilteredFunctions.hidden)) {
-        DebuggerView.FilteredFunctions.performSearch(token);
+        DebuggerView.FilteredFunctions.scheduleSearch(token, 0);
       } else if (!isReturnKey) {
         DebuggerView.FilteredFunctions[["selectNext", "selectPrev"][action]]();
       } else {
         DebuggerView.FilteredFunctions.clearView();
         DebuggerView.editor.focus();
         this.clearSearch();
       }
       this._prevSearchedToken = token;
       return;
     }
 
     // Perform a variable search based on the specified operator.
     if (isVariable) {
       if (isReturnKey && isDifferentToken) {
-        DebuggerView.Variables.performSearch(token);
+        DebuggerView.Variables.scheduleSearch(token, 0);
       } else {
         DebuggerView.Variables.expandFirstSearchResults();
       }
       this._prevSearchedToken = token;
       return;
     }
 
     // Increment or decrement the specified line.
@@ -1209,17 +1208,17 @@ FilterView.prototype = {
 
   /**
    * The blur listener for the search container.
    */
   _onBlur: function() {
     DebuggerView.GlobalSearch.clearView();
     DebuggerView.FilteredSources.clearView();
     DebuggerView.FilteredFunctions.clearView();
-    DebuggerView.Variables.performSearch(null);
+    DebuggerView.Variables.scheduleSearch(null, 0);
     this._searchboxHelpPanel.hidePopup();
   },
 
   /**
    * Called when a filtering key sequence was pressed.
    *
    * @param string aOperator
    *        The operator to use for filtering.
@@ -1269,17 +1268,17 @@ FilterView.prototype = {
     this._doSearch(SEARCH_LINE_FLAG);
     this._searchboxHelpPanel.hidePopup();
   },
 
   /**
    * Called when the variable search filter key sequence was pressed.
    */
   _doVariableSearch: function() {
-    DebuggerView.Variables.performSearch("");
+    DebuggerView.Variables.scheduleSearch("", 0);
     this._doSearch(SEARCH_VARIABLE_FLAG);
     this._searchboxHelpPanel.hidePopup();
   },
 
   /**
    * Called when the variables focus key sequence was pressed.
    */
   _doVariablesFocus: function() {
@@ -1407,17 +1406,16 @@ FilteredSourcesView.prototype = Heritage
 });
 
 /**
  * Functions handling the function search UI.
  */
 function FilteredFunctionsView() {
   dumpn("FilteredFunctionsView was instantiated");
 
-  this._performFunctionSearch = this._performFunctionSearch.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onSelect = this._onSelect.bind(this);
 }
 
 FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, {
   /**
    * Initialization function, called when the debugger is started.
    */
@@ -1436,93 +1434,69 @@ FilteredFunctionsView.prototype = Herita
     dumpn("Destroying the FilteredFunctionsView");
 
     this.widget.removeEventListener("select", this._onSelect, false);
     this.widget.removeEventListener("click", this._onClick, false);
     this.anchor = null;
   },
 
   /**
-   * Allows searches to be scheduled and delayed to avoid redundant calls.
-   */
-  delayedSearch: true,
-
-  /**
    * Schedules searching for a function in all of the sources.
    *
-   * @param string aQuery
-   *        The function to search for.
-   */
-  scheduleSearch: function(aQuery) {
-    if (!this.delayedSearch) {
-      this.performSearch(aQuery);
-      return;
-    }
-    let delay = Math.max(FUNCTION_SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
-
-    window.clearTimeout(this._searchTimeout);
-    this._searchFunction = this._startSearch.bind(this, aQuery);
-    this._searchTimeout = window.setTimeout(this._searchFunction, delay);
-  },
-
-  /**
-   * Immediately searches for a function in all of the sources.
-   *
-   * @param string aQuery
+   * @param string aToken
    *        The function to search for.
+   * @param number aWait
+   *        The amount of milliseconds to wait until draining.
    */
-  performSearch: function(aQuery) {
-    window.clearTimeout(this._searchTimeout);
-    this._searchFunction = null;
-    this._startSearch(aQuery);
-  },
+  scheduleSearch: function(aToken, aWait) {
+    let maxDelay = FUNCTION_SEARCH_ACTION_MAX_DELAY;
+    let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
 
-  /**
-   * Starts searching for a function in all of the sources.
-   *
-   * @param string aQuery
-   *        The function to search for.
-   */
-  _startSearch: function(aQuery) {
-    this._searchedToken = aQuery;
-
-    // Start fetching as many sources as possible, then perform the search.
-    DebuggerController.SourceScripts
-      .getTextForSources(DebuggerView.Sources.values)
-      .then(this._performFunctionSearch);
+    // Allow requests to settle down first.
+    setNamedTimeout("function-search", delay, () => {
+      // Start fetching as many sources as possible, then perform the search.
+      let urls = DebuggerView.Sources.values;
+      let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls);
+      sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
+    });
   },
 
   /**
    * Finds function matches in all the sources stored in the cache, and groups
    * them by location and line number.
+   *
+   * @param string aToken
+   *        The string to search for.
+   * @param array aSources
+   *        An array of [url, text] tuples for each source.
    */
-  _performFunctionSearch: function(aSources) {
+  _doSearch: function(aToken, aSources) {
     // Get the currently searched token from the filtering input.
     // Continue parsing even if the searched token is an empty string, to
     // cache the syntax tree nodes generated by the reflection API.
-    let token = this._searchedToken;
 
     // Make sure the currently displayed source is parsed first. Once the
     // maximum allowed number of resutls are found, parsing will be halted.
     let currentUrl = DebuggerView.Sources.selectedValue;
     let currentSource = aSources.filter(([sourceUrl]) => sourceUrl == currentUrl)[0];
     aSources.splice(aSources.indexOf(currentSource), 1);
     aSources.unshift(currentSource);
 
-    // If not searching for a specific function, only parse the displayed source.
-    if (!token) {
+    // If not searching for a specific function, only parse the displayed source,
+    // which is now the first item in the sources array.
+    if (!aToken) {
       aSources.splice(1);
     }
 
     // Prepare the results array, containing search details for each source.
     let searchResults = [];
 
     for (let [location, contents] of aSources) {
       let parserMethods = DebuggerController.Parser.get(location, contents);
-      let sourceResults = parserMethods.getNamedFunctionDefinitions(token);
+      let sourceResults = parserMethods.getNamedFunctionDefinitions(aToken);
 
       for (let scriptResult of sourceResults) {
         for (let parseResult of scriptResult.parseResults) {
           searchResults.push({
             sourceUrl: scriptResult.sourceUrl,
             scriptOffset: scriptResult.scriptOffset,
             functionName: parseResult.functionName,
             functionLocation: parseResult.functionLocation,
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -293,20 +293,16 @@ TargetEventsHandler.prototype = {
   _onTabNavigated: function(aType, aPacket) {
     switch (aType) {
       case "will-navigate": {
         // Reset UI.
         NetMonitorView.RequestsMenu.reset();
         NetMonitorView.Sidebar.reset();
         NetMonitorView.NetworkDetails.reset();
 
-        // Reset global helpers cache.
-        nsIURL.store.clear();
-        drain.store.clear();
-
         window.emit(EVENTS.TARGET_WILL_NAVIGATE);
         break;
       }
       case "navigate": {
         window.emit(EVENTS.TARGET_DID_NAVIGATE);
         break;
       }
     }
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -718,17 +718,18 @@ RequestsMenuView.prototype = Heritage.ex
     }
     this._updateQueue.push([aId, aData]);
 
     // Lazy updating is disabled in some tests.
     if (!this.lazyUpdate) {
       return void this._flushRequests();
     }
     // Allow requests to settle down first.
-    drain("update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
+    setNamedTimeout(
+      "update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
   },
 
   /**
    * Starts adding all queued additional information about network requests.
    */
   _flushRequests: function() {
     // For each queued additional information packet, get the corresponding
     // request item in the view and update it based on the specified data.
@@ -1189,17 +1190,18 @@ RequestsMenuView.prototype = Heritage.ex
     }
   },
 
   /**
    * The resize listener for this container's window.
    */
   _onResize: function(e) {
     // Allow requests to settle down first.
-    drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
+    setNamedTimeout(
+      "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
   /**
    * Handle the context menu opening. Hide items if no request is selected.
    */
   _onContextShowing: function() {
     let resendElement = $("#request-menu-context-resend");
     resendElement.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom;
@@ -2149,25 +2151,15 @@ function writeQueryText(aParams) {
  * @return string
  *         Query string that can be appended to a url.
  */
 function writeQueryString(aParams) {
   return [(name + "=" + value) for ({name, value} of aParams)].join("&");
 }
 
 /**
- * Helper for draining a rapid succession of events and invoking a callback
- * once everything settles down.
- */
-function drain(aId, aWait, aCallback, aStore = drain.store) {
-  window.clearTimeout(aStore.get(aId));
-  aStore.set(aId, window.setTimeout(aCallback, aWait));
-}
-drain.store = new Map();
-
-/**
  * Preliminary setup for the NetMonitorView object.
  */
 NetMonitorView.Toolbar = new ToolbarView();
 NetMonitorView.RequestsMenu = new RequestsMenuView();
 NetMonitorView.Sidebar = new SidebarView();
 NetMonitorView.CustomRequest = new CustomRequestView();
 NetMonitorView.NetworkDetails = new NetworkDetailsView();
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -173,19 +173,17 @@ VariablesView.prototype = {
    */
   _emptySoon: function(aTimeout) {
     let prevList = this._list;
     let currList = this._list = this.document.createElement("scrollbox");
 
     this._store.length = 0;
     this._itemsByElement.clear();
 
-    this._emptyTimeout = this.window.setTimeout(() => {
-      this._emptyTimeout = null;
-
+    this.window.setTimeout(() => {
       prevList.removeEventListener("keypress", this._onViewKeyPress, false);
       currList.addEventListener("keypress", this._onViewKeyPress, false);
       currList.setAttribute("orient", "vertical");
 
       this._parent.removeChild(prevList);
       this._parent.appendChild(currList);
       this._boxObject = currList.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
 
@@ -452,17 +450,17 @@ VariablesView.prototype = {
     }
     this._searchboxContainer.hidden = !aVisibleFlag;
   },
 
   /**
    * Listener handling the searchbox input event.
    */
   _onSearchboxInput: function() {
-    this.performSearch(this._searchboxNode.value);
+    this._doSearch(this._searchboxNode.value);
   },
 
   /**
    * Listener handling the searchbox key press event.
    */
   _onSearchboxKeyPress: function(e) {
     switch(e.keyCode) {
       case e.DOM_VK_RETURN:
@@ -472,77 +470,53 @@ VariablesView.prototype = {
       case e.DOM_VK_ESCAPE:
         this._searchboxNode.value = "";
         this._onSearchboxInput();
         return;
     }
   },
 
   /**
-   * Allows searches to be scheduled and delayed to avoid redundant calls.
-   */
-  delayedSearch: true,
-
-  /**
    * Schedules searching for variables or properties matching the query.
    *
-   * @param string aQuery
+   * @param string aToken
    *        The variable or property to search for.
+   * @param number aWait
+   *        The amount of milliseconds to wait until draining.
    */
-  scheduleSearch: function(aQuery) {
-    if (!this.delayedSearch) {
-      this.performSearch(aQuery);
-      return;
-    }
-    let delay = Math.max(SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
-
-    this.window.clearTimeout(this._searchTimeout);
-    this._searchFunction = this._startSearch.bind(this, aQuery);
-    this._searchTimeout = this.window.setTimeout(this._searchFunction, delay);
-  },
-
-  /**
-   * Immediately searches for variables or properties matching the query.
-   *
-   * @param string aQuery
-   *        The variable or property to search for.
-   */
-  performSearch: function(aQuery) {
-    this.window.clearTimeout(this._searchTimeout);
-    this._searchFunction = null;
-    this._startSearch(aQuery);
+  scheduleSearch: function(aToken, aWait) {
+    let maxDelay = SEARCH_ACTION_MAX_DELAY;
+    let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+    // Allow requests to settle down first.
+    setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
   },
 
   /**
    * Performs a case insensitive search for variables or properties matching
    * the query, and hides non-matched items.
    *
-   * If aQuery is empty string, then all the scopes are unhidden and expanded,
+   * If aToken is falsy, then all the scopes are unhidden and expanded,
    * while the available variables and properties inside those scopes are
    * just unhidden.
    *
-   * If aQuery is null or undefined, then all the scopes are just unhidden,
-   * and the available variables and properties inside those scopes are also
-   * just unhidden.
-   *
-   * @param string aQuery
+   * @param string aToken
    *        The variable or property to search for.
    */
-  _startSearch: function(aQuery) {
+  _doSearch: function(aToken) {
     for (let scope of this._store) {
-      switch (aQuery) {
+      switch (aToken) {
         case "":
-          scope.expand();
-          // fall through
         case null:
         case undefined:
+          scope.expand();
           scope._performSearch("");
           break;
         default:
-          scope._performSearch(aQuery.toLowerCase());
+          scope._performSearch(aToken.toLowerCase());
           break;
       }
     }
   },
 
   /**
    * Expands the first search results in this container.
    */
@@ -901,19 +875,16 @@ VariablesView.prototype = {
   _document: null,
   _window: null,
 
   _store: null,
   _prevHierarchy: null,
   _currHierarchy: null,
   _enumVisible: true,
   _nonEnumVisible: true,
-  _emptyTimeout: null,
-  _searchTimeout: null,
-  _searchFunction: null,
   _parent: null,
   _list: null,
   _boxObject: null,
   _searchboxNode: null,
   _searchboxContainer: null,
   _searchboxPlaceholder: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -10,18 +10,22 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const PANE_APPEARANCE_DELAY = 50;
 const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
-this.EXPORTED_SYMBOLS = ["Heritage", "ViewHelpers", "WidgetMethods"];
+this.EXPORTED_SYMBOLS = [
+  "Heritage", "ViewHelpers", "WidgetMethods",
+  "setNamedTimeout", "clearNamedTimeout"
+];
 
 /**
  * Inheritance helpers from the addon SDK's core/heritage.
  * Remove these when all devtools are loadered.
  */
 this.Heritage = {
   /**
    * @see extend in sdk/core/heritage.
@@ -37,16 +41,51 @@ this.Heritage = {
     return Object.getOwnPropertyNames(aObject).reduce((aDescriptor, aName) => {
       aDescriptor[aName] = Object.getOwnPropertyDescriptor(aObject, aName);
       return aDescriptor;
     }, {});
   }
 };
 
 /**
+ * Helper for draining a rapid succession of events and invoking a callback
+ * once everything settles down.
+ *
+ * @param string aId
+ *        A string identifier for the named timeout.
+ * @param number aWait
+ *        The amount of milliseconds to wait after no more events are fired.
+ * @param function aCallback
+ *        Invoked when no more events are fired after the specified time.
+ */
+this.setNamedTimeout = function(aId, aWait, aCallback) {
+  clearNamedTimeout(aId);
+
+  namedTimeoutsStore.set(aId, setTimeout(() =>
+    namedTimeoutsStore.delete(aId) && aCallback(), aWait));
+};
+
+/**
+ * Clears a named timeout.
+ * @see setNamedTimeout
+ *
+ * @param string aId
+ *        A string identifier for the named timeout.
+ */
+this.clearNamedTimeout = function(aId) {
+  if (!namedTimeoutsStore) {
+    return;
+  }
+  clearTimeout(namedTimeoutsStore.get(aId));
+  namedTimeoutsStore.delete(aId);
+};
+
+XPCOMUtils.defineLazyGetter(this, "namedTimeoutsStore", () => new Map());
+
+/**
  * Helpers for creating and messaging between UI components.
  */
 this.ViewHelpers = {
   /**
    * Convenience method, dispatching a custom event.
    *
    * @param nsIDOMNode aTarget
    *        A custom target element to dispatch the event from.