Bug 429732 - Make Finder.jsm iterate over matches asynchronously in small batches so it does not block the UI thread. r=mikedeboer

This commit is contained in:
Tomasz Kołodziejski 2014-09-18 10:23:00 +02:00
parent 4254d0a334
commit 962246cb69
3 changed files with 103 additions and 101 deletions

View File

@ -392,24 +392,6 @@
testClipboardSearchString(SEARCH_TEXT);
}
// Perform an async function in serial on each of the list items.
function asyncForEach(list, async, callback) {
let i = 0;
let len = list.length;
if (!len)
return callback();
async(list[i], function handler() {
i++;
if (i < len) {
async(list[i], handler, i);
} else {
callback();
}
}, i);
}
function testFindCountUI(callback) {
clearFocus();
document.getElementById("cmd_find").doCommand();
@ -444,46 +426,45 @@
let timeout = gFindBar._matchesCountTimeoutLength + 20;
function assertMatches(aTest, aMatches) {
window.opener.wrappedJSObject.SimpleTest.is(aTest.current, aMatches[1],
window.opener.wrappedJSObject.SimpleTest.is(aMatches[1], aTest.current,
"Currently highlighted match should be at " + aTest.current);
window.opener.wrappedJSObject.SimpleTest.is(aTest.total, aMatches[2],
window.opener.wrappedJSObject.SimpleTest.is(aMatches[2], aTest.total,
"Total amount of matches should be " + aTest.total);
}
function testString(aTest, aNext) {
gFindBar.clear();
enterStringIntoFindField(aTest.text);
setTimeout(function() {
function* generatorTest() {
for (let test of tests) {
gFindBar.clear();
yield;
enterStringIntoFindField(test.text);
yield;
let matches = foundMatches.value.match(regex);
if (!aTest.total) {
if (!test.total) {
ok(!matches, "No message should be shown when 0 matches are expected");
aNext();
} else {
assertMatches(aTest, matches);
let cycleTests = [];
let cycles = aTest.total;
while (--cycles) {
aTest.current++;
if (aTest.current > aTest.total)
aTest.current = 1;
cycleTests.push({
current: aTest.current,
total: aTest.total
});
}
asyncForEach(cycleTests, function(aCycleTest, aNextCycle) {
assertMatches(test, matches);
for (let i = 1; i < test.total; i++) {
gFindBar.onFindAgainCommand();
setTimeout(function() {
assertMatches(aCycleTest, foundMatches.value.match(regex));
aNextCycle();
}, timeout);
}, aNext);
yield;
// test.current + 1, test.current + 2, ..., test.total, 1, ..., test.current
let current = (test.current + i - 1) % test.total + 1;
assertMatches({
current: current,
total: test.total
}, foundMatches.value.match(regex));
}
}
}, timeout);
}
callback();
}
asyncForEach(tests, testString, callback);
let test = generatorTest();
let resultListener = {
onMatchesCountResult: function() {
test.next();
}
};
gFindBar.browser.finder.addResultListener(resultListener);
test.next();
}
function testClipboardSearchString(aExpected) {

View File

@ -448,40 +448,24 @@
]]></getter>
</property>
<method name="_updateMatchesCountWorker">
<parameter name="aRes"/>
<body><![CDATA[
let word = this._findField.value;
if (aRes == this.nsITypeAheadFind.FIND_NOTFOUND || !word) {
this._foundMatches.hidden = true;
this._foundMatches.value = "";
} else {
let matchesCount = this.browser.finder.requestMatchesCount(
word, this._matchesCountLimit, this._findMode == this.FIND_LINKS);
window.clearTimeout(this._updateMatchesCountTimeout);
this._updateMatchesCountTimeout = null;
}
]]></body>
</method>
<!--
- Updates the search match count after each find operation on a new string.
- @param aRes
- the result of the find operation
-->
<method name="_updateMatchesCount">
<parameter name="aRes"/>
<body><![CDATA[
if (this._matchesCountLimit == 0 || !this._dispatchFindEvent("matchescount"))
return;
if (this._updateMatchesCountTimeout) {
window.clearTimeout(this._updateMatchesCountTimeout);
this._updateMatchesCountTimeout = null;
}
this._updateMatchesCountTimeout =
window.setTimeout(() => this._updateMatchesCountWorker(aRes),
this._matchesCountTimeoutLength);
window.setTimeout(() => {
this.browser.finder.requestMatchesCount(this._findField.value, this._matchesCountLimit,
this._findMode == this.FIND_LINKS);
}, this._matchesCountTimeoutLength);
]]></body>
</method>

View File

@ -11,6 +11,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Geometry.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService",
"@mozilla.org/intl/texttosuburi;1",
@ -22,6 +23,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
const kHighlightIterationSizeMax = 100;
function Finder(docShell) {
this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
this._fastFind.init(docShell);
@ -127,6 +130,8 @@ Finder.prototype = {
this._fastFind.caseSensitive = aSensitive;
},
_lastFindResult: null,
/**
* Used for normal search operations, highlights the first match.
*
@ -135,9 +140,9 @@ Finder.prototype = {
* @param aDrawOutline Puts an outline around matched links.
*/
fastFind: function (aSearchString, aLinksOnly, aDrawOutline) {
let result = this._fastFind.find(aSearchString, aLinksOnly);
this._lastFindResult = this._fastFind.find(aSearchString, aLinksOnly);
let searchString = this._fastFind.searchString;
this._notify(searchString, result, false, aDrawOutline);
this._notify(searchString, this._lastFindResult, false, aDrawOutline);
},
/**
@ -150,9 +155,9 @@ Finder.prototype = {
* @param aDrawOutline Puts an outline around matched links.
*/
findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
this._lastFindResult = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
let searchString = this._fastFind.searchString;
this._notify(searchString, result, aFindBackwards, aDrawOutline);
this._notify(searchString, this._lastFindResult, aFindBackwards, aDrawOutline);
},
/**
@ -174,14 +179,18 @@ Finder.prototype = {
return searchString;
},
highlight: function (aHighlight, aWord) {
let found = this._highlight(aHighlight, aWord, null);
highlight: Task.async(function* (aHighlight, aWord) {
if (this._abortHighlight) {
this._abortHighlight();
}
let found = yield this._highlight(aHighlight, aWord, null);
if (aHighlight) {
let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
: Ci.nsITypeAheadFind.FIND_NOTFOUND;
this._notify(aWord, result, false, false, false);
}
},
}),
enableSelection: function() {
this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON);
@ -262,7 +271,22 @@ Finder.prototype = {
}
},
_notifyMatchesCount: function(result) {
for (let l of this._listeners) {
try {
l.onMatchesCountResult(result);
} catch (ex) {}
}
},
requestMatchesCount: function(aWord, aMatchLimit, aLinksOnly) {
if (this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
this.searchString == "") {
return this._notifyMatchesCount({
total: 0,
current: 0
});
}
let window = this._getWindow();
let result = this._countMatchesInWindow(aWord, aMatchLimit, aLinksOnly, window);
@ -279,11 +303,7 @@ Finder.prototype = {
delete result._currentFound;
delete result._framesToCount;
for (let l of this._listeners) {
try {
l.onMatchesCountResult(result);
} catch (ex) {}
}
this._notifyMatchesCount(result);
},
/**
@ -322,41 +342,37 @@ Finder.prototype = {
let foundRange = this._fastFind.getFoundRange();
this._findIterator(aWord, aWindow, aRange => {
if (!aLinksOnly || this._rangeStartsInLink(aRange)) {
for(let range of this._findIterator(aWord, aWindow)) {
if (!aLinksOnly || this._rangeStartsInLink(range)) {
++aStats.total;
if (!aStats._currentFound) {
++aStats.current;
aStats._currentFound = (foundRange &&
aRange.startContainer == foundRange.startContainer &&
aRange.startOffset == foundRange.startOffset &&
aRange.endContainer == foundRange.endContainer &&
aRange.endOffset == foundRange.endOffset);
range.startContainer == foundRange.startContainer &&
range.startOffset == foundRange.startOffset &&
range.endContainer == foundRange.endContainer &&
range.endOffset == foundRange.endOffset);
}
}
if (aStats.total == aMatchLimit) {
aStats.total = -1;
return false;
break;
}
});
};
return aStats;
},
/**
* Basic wrapper around nsIFind that provides invoking a callback `aOnFind`
* each time an occurence of `aWord` string is found.
* Basic wrapper around nsIFind that provides a generator yielding
* a range each time an occurence of `aWord` string is found.
*
* @param aWord
* the word to search for.
* @param aWindow
* the window to search in.
* @param aOnFind
* the Function to invoke when a word is found. if Boolean `false` is
* returned, the find operation will be stopped and the Function will
* not be invoked again.
*/
_findIterator: function(aWord, aWindow, aOnFind) {
_findIterator: function* (aWord, aWindow) {
let doc = aWindow.document;
let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
doc.body : doc.documentElement;
@ -381,13 +397,34 @@ Finder.prototype = {
finder.caseSensitive = this._fastFind.caseSensitive;
while ((retRange = finder.Find(aWord, searchRange, startPt, endPt))) {
if (aOnFind(retRange) === false)
break;
yield retRange;
startPt = retRange.cloneRange();
startPt.collapse(false);
}
},
_highlightIterator: Task.async(function* (aWord, aWindow, aOnFind) {
let count = 0;
for (let range of this._findIterator(aWord, aWindow)) {
aOnFind(range);
if (++count >= kHighlightIterationSizeMax) {
count = 0;
yield this._highlightSleep(0);
}
}
}),
_abortHighlight: null,
_highlightSleep: function(delay) {
return new Promise((resolve, reject) => {
this._abortHighlight = () => {
this._abortHighlight = null;
reject();
};
this._getWindow().setTimeout(resolve, delay);
});
},
/**
* Helper method for `_countMatchesInWindow` that recursively collects all
* visible (i)frames inside a window.
@ -521,12 +558,12 @@ Finder.prototype = {
}
},
_highlight: function (aHighlight, aWord, aWindow) {
_highlight: Task.async(function* (aHighlight, aWord, aWindow) {
let win = aWindow || this._getWindow();
let found = false;
for (let i = 0; win.frames && i < win.frames.length; i++) {
if (this._highlight(aHighlight, aWord, win.frames[i]))
if (yield this._highlight(aHighlight, aWord, win.frames[i]))
found = true;
}
@ -539,7 +576,7 @@ Finder.prototype = {
}
if (aHighlight) {
this._findIterator(aWord, win, aRange => {
yield this._highlightIterator(aWord, win, aRange => {
this._highlightRange(aRange, controller);
found = true;
});
@ -567,7 +604,7 @@ Finder.prototype = {
}
return found;
},
}),
_highlightRange: function(aRange, aController) {
let node = aRange.startContainer;