Bug 891439 - Standardize the sheduleSearch/performSearch methods, r=past
authorVictor Porof <vporof@mozilla.com>
Fri, 13 Sep 2013 16:23:13 +0300
changeset 147068 c5b199404e8a8757396baedc511496c806d1796c
parent 147067 ba5df19635daf8f3e6cf16b9d70c629d098f3e4a
child 147069 ead76fb1a864c7b411fc4fb7e162c28352079a6b
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspast
bugs891439
milestone26.0a1
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.