Bug 1645521 - Part 2 - Allow for multiple heuristic providers and enable ProviderHeuristicFallback. r=adw

Differential Revision: https://phabricator.services.mozilla.com/D80293
This commit is contained in:
Harry Twyford 2020-07-09 05:02:49 +00:00
Родитель 382aaa0d5a
Коммит 7e31642215
10 изменённых файлов: 181 добавлений и 71 удалений

Просмотреть файл

@ -188,7 +188,7 @@ add_task(async function test_registerProvider() {
}); });
// Adds a single active provider that returns many kinds of results. This also // Adds a single active provider that returns many kinds of results. This also
// checks that the heuristic result from the built-in UnifiedComplete provider // checks that the heuristic result from the built-in HeuristicFallback provider
// is included. // is included.
add_task(async function test_onProviderResultsRequested() { add_task(async function test_onProviderResultsRequested() {
let ext = ExtensionTestUtils.loadExtension({ let ext = ExtensionTestUtils.loadExtension({
@ -276,7 +276,7 @@ add_task(async function test_onProviderResultsRequested() {
// Check the results. // Check the results.
let expectedResults = [ let expectedResults = [
// The first result should be a search result returned by UnifiedComplete. // The first result should be a search result returned by HeuristicFallback.
{ {
type: UrlbarUtils.RESULT_TYPE.SEARCH, type: UrlbarUtils.RESULT_TYPE.SEARCH,
source: UrlbarUtils.RESULT_SOURCE.SEARCH, source: UrlbarUtils.RESULT_SOURCE.SEARCH,

Просмотреть файл

@ -41,6 +41,15 @@ function groupFromResult(result) {
} }
} }
// Breaks ties among heuristic results. Providers higher up the list are higher
// priority.
const heuristicOrder = [
// Test providers are handled in sort(),
// Extension providers are handled in sort(),
"UrlbarProviderSearchTips",
"UnifiedComplete",
"HeuristicFallback",
];
/** /**
* Class used to create a muxer. * Class used to create a muxer.
* The muxer receives and sorts results in a UrlbarQueryContext. * The muxer receives and sorts results in a UrlbarQueryContext.
@ -54,11 +63,19 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
return "UnifiedComplete"; return "UnifiedComplete";
} }
/* eslint-disable complexity */
/** /**
* Sorts results in the given UrlbarQueryContext. * Sorts results in the given UrlbarQueryContext.
* *
* sort() is not suitable to be broken up into smaller functions or to rely
* on more convenience functions. It exists to efficiently group many
* conditions into just three loops. As a result, we must disable complexity
* linting.
*
* @param {UrlbarQueryContext} context * @param {UrlbarQueryContext} context
* The query context. * The query context.
* @returns {boolean} If results were successfully sorted. This return value
* is a stopgap and can be removed when bug 1648468 lands.
*/ */
sort(context) { sort(context) {
// This method is called multiple times per keystroke, so it should be as // This method is called multiple times per keystroke, so it should be as
@ -70,19 +87,42 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
// Capture information about the heuristic result to dedupe results from the // Capture information about the heuristic result to dedupe results from the
// heuristic more quickly. // heuristic more quickly.
let topHeuristicRank = Infinity;
for (let result of context.allHeuristicResults) {
// Determine the highest-ranking heuristic result.
if (!result.heuristic) {
continue;
}
// + 2 to reserve the highest-priority slots for test and extension
// providers.
let heuristicRank = heuristicOrder.indexOf(result.providerName) + 2;
// Extension and test provider names vary widely and aren't suitable
// for a static safelist like heuristicOrder.
if (result.providerType == UrlbarUtils.PROVIDER_TYPE.EXTENSION) {
heuristicRank = 1;
} else if (result.providerName.startsWith("TestProvider")) {
heuristicRank = 0;
} else if (heuristicRank - 2 == -1) {
throw new Error(
`Heuristic result returned by unexpected provider: ${result.providerName}`
);
}
// Replace in case of ties, which would occur if a provider sent two
// heuristic results.
if (heuristicRank <= topHeuristicRank) {
topHeuristicRank = heuristicRank;
context.heuristicResult = result;
}
}
let heuristicResultQuery; let heuristicResultQuery;
let heuristicResultOmniboxContent;
if (context.heuristicResult) { if (context.heuristicResult) {
if ( if (
context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.SEARCH && context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
context.heuristicResult.payload.query context.heuristicResult.payload.query
) { ) {
heuristicResultQuery = context.heuristicResult.payload.query.toLocaleLowerCase(); heuristicResultQuery = context.heuristicResult.payload.query.toLocaleLowerCase();
} else if (
context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.OMNIBOX &&
context.heuristicResult.payload.content
) {
heuristicResultOmniboxContent = context.heuristicResult.payload.content.toLocaleLowerCase();
} }
} }
@ -95,10 +135,17 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
UrlbarPrefs.get("maxHistoricalSearchSuggestions"), UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
context.maxResults context.maxResults
); );
let hasUnifiedComplete = false;
// Do the first pass through the results. We only collect info for the // Do the first pass through the results. We only collect info for the
// second pass here. // second pass here.
for (let result of context.results) { for (let result of context.results) {
// Keep track of whether UnifiedComplete has returned results. We will
// quit early if it hasn't.
if (result.providerName == "UnifiedComplete") {
hasUnifiedComplete = true;
}
// The "Search in a Private Window" result should only be shown when there // The "Search in a Private Window" result should only be shown when there
// are other results and all of them are searches. It should not be shown // are other results and all of them are searches. It should not be shown
// if the user typed an alias because that's an explicit engine choice. // if the user typed an alias because that's an explicit engine choice.
@ -139,9 +186,24 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
} }
} }
// Quit early if we're still waiting on UnifiedComplete. If it turns out
// UnifiedComplete doesn't need to return any results, it will call sort()
// regardless to unblock showing results.
if (
!hasUnifiedComplete &&
context.pendingHeuristicProviders.has("UnifiedComplete")
) {
return false;
}
// Do the second pass through results to build the list of unsorted results. // Do the second pass through results to build the list of unsorted results.
let unsortedResults = []; let unsortedResults = [];
for (let result of context.results) { for (let result of context.results) {
// Exclude low-ranked heuristic results.
if (result.heuristic && result != context.heuristicResult) {
continue;
}
// Exclude "Search in a Private Window" as determined in the first pass. // Exclude "Search in a Private Window" as determined in the first pass.
if ( if (
result.type == UrlbarUtils.RESULT_TYPE.SEARCH && result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
@ -214,15 +276,6 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
} }
} }
// Exclude omnibox results that dupe the heuristic.
if (
!result.heuristic &&
result.type == UrlbarUtils.RESULT_TYPE.OMNIBOX &&
result.payload.content == heuristicResultOmniboxContent
) {
continue;
}
// Include this result. // Include this result.
unsortedResults.push(result); unsortedResults.push(result);
} }
@ -272,6 +325,7 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
} }
context.results = sortedResults; context.results = sortedResults;
return true;
} }
/** /**

Просмотреть файл

@ -63,7 +63,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
* @returns {boolean} Whether this provider should be invoked for the search. * @returns {boolean} Whether this provider should be invoked for the search.
*/ */
isActive(queryContext) { isActive(queryContext) {
return false; // TODO: Set to true in future part of this patch. return true;
} }
/** /**

Просмотреть файл

@ -90,8 +90,17 @@ class ProviderUnifiedComplete extends UrlbarProvider {
acResult, acResult,
urls urls
); );
for (let result of results) { // The Muxer gives UnifiedComplete special status and waits for it to
addCallback(this, result); // return results before sorting them. We need to know that
// UnifiedComplete has finished even if it isn't returning results, so we
// call addCallback with an empty result here, which is something only
// UnifiedComplete is allowed to do.
if (!results.length) {
addCallback(this, null);
} else {
for (let result of results) {
addCallback(this, result);
}
} }
}); });
this.queries.delete(queryContext); this.queries.delete(queryContext);

Просмотреть файл

@ -366,6 +366,7 @@ class Query {
let queryPromises = []; let queryPromises = [];
for (let provider of activeProviders) { for (let provider of activeProviders) {
if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
this.context.pendingHeuristicProviders.add(provider.name);
queryPromises.push( queryPromises.push(
provider.tryMethod("startQuery", this.context, this.add.bind(this)) provider.tryMethod("startQuery", this.context, this.add.bind(this))
); );
@ -436,13 +437,29 @@ class Query {
if (!(provider instanceof UrlbarProvider)) { if (!(provider instanceof UrlbarProvider)) {
throw new Error("Invalid provider passed to the add callback"); throw new Error("Invalid provider passed to the add callback");
} }
// When this set is empty, we can display heuristic results early. We remove
// the provider from the list without checking result.heuristic since
// heuristic providers don't necessarily have to return heuristic results.
// We expect a provider with type HEURISTIC will return its heuristic
// result(s) first.
this.context.pendingHeuristicProviders.delete(provider.name);
// Stop returning results as soon as we've been canceled. // Stop returning results as soon as we've been canceled.
if (this.canceled) { if (this.canceled) {
return; return;
} }
let addResult = true;
if (!result) {
addResult = false;
}
// Check if the result source should be filtered out. Pay attention to the // Check if the result source should be filtered out. Pay attention to the
// heuristic result though, that is supposed to be added regardless. // heuristic result though, that is supposed to be added regardless.
if ( if (
addResult &&
!this.acceptableSources.includes(result.source) && !this.acceptableSources.includes(result.source) &&
!result.heuristic && !result.heuristic &&
// Treat form history as searches for the purpose of acceptableSources. // Treat form history as searches for the purpose of acceptableSources.
@ -450,35 +467,60 @@ class Query {
result.source != UrlbarUtils.RESULT_SOURCE.HISTORY || result.source != UrlbarUtils.RESULT_SOURCE.HISTORY ||
!this.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH)) !this.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH))
) { ) {
return; addResult = false;
} }
// Filter out javascript results for safety. The provider is supposed to do // Filter out javascript results for safety. The provider is supposed to do
// it, but we don't want to risk leaking these out. // it, but we don't want to risk leaking these out.
if ( if (
addResult &&
result.type != UrlbarUtils.RESULT_TYPE.KEYWORD && result.type != UrlbarUtils.RESULT_TYPE.KEYWORD &&
result.payload.url && result.payload.url &&
result.payload.url.startsWith("javascript:") && result.payload.url.startsWith("javascript:") &&
!this.context.searchString.startsWith("javascript:") && !this.context.searchString.startsWith("javascript:") &&
UrlbarPrefs.get("filter.javascript") UrlbarPrefs.get("filter.javascript")
) { ) {
return; addResult = false;
} }
// We wait on UnifiedComplete to return a heuristic result. If it has no
// heuristic result to return, we still need to sort and display results, so
// we follow the usual codepath to muxer.sort. We only offer this for
// UnifiedComplete, so we check the provider's name here. This is a stopgap
// measure until bug 1648468 lands.
if (!addResult) {
if (provider.name == "UnifiedComplete") {
this._notifyResultsFromProvider(provider);
}
return;
}
result.providerName = provider.name; result.providerName = provider.name;
result.providerType = provider.type;
this.context.results.push(result); this.context.results.push(result);
if (result.heuristic) { if (result.heuristic) {
this.context.heuristicResult = result; this.context.allHeuristicResults.push(result);
} }
this._notifyResultsFromProvider(provider); this._notifyResultsFromProvider(provider);
} }
_notifyResultsFromProvider(provider) { _notifyResultsFromProvider(provider) {
// If the provider is not of heuristic type, chunk results, to improve the // We create two chunking timers: one for heuristic results, and one for
// dataflow and reduce UI flicker. // other results. We expect heuristic providers to return their heuristic
// results before other results/providers in most cases. When all heuristic
// providers have returned some results, we fire the heuristic timer early.
// If the timer fires first, we stop waiting on the remaining heuristic
// providers.
// Both timers are used to reduce UI flicker.
if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
this._notifyResults(); if (!this._heuristicProviderTimer) {
this._heuristicProviderTimer = new SkippableTimer({
name: "Heuristic provider timer",
callback: () => this._notifyResults(),
time: CHUNK_RESULTS_DELAY_MS,
logger,
});
}
} else if (!this._chunkTimer) { } else if (!this._chunkTimer) {
this._chunkTimer = new SkippableTimer({ this._chunkTimer = new SkippableTimer({
name: "Query chunk timer", name: "Query chunk timer",
@ -487,16 +529,33 @@ class Query {
logger, logger,
}); });
} }
// If all active heuristic providers have returned results, we can skip the
// heuristic results timer and start showing results immediately.
if (
this._heuristicProviderTimer &&
!this.context.pendingHeuristicProviders.size
) {
this._heuristicProviderTimer.fire().catch(Cu.reportError);
}
} }
_notifyResults() { _notifyResults() {
this.muxer.sort(this.context); let sorted = this.muxer.sort(this.context);
if (this._heuristicProviderTimer) {
this._heuristicProviderTimer.cancel().catch(Cu.reportError);
this._heuristicProviderTimer = null;
}
if (this._chunkTimer) { if (this._chunkTimer) {
this._chunkTimer.cancel().catch(Cu.reportError); this._chunkTimer.cancel().catch(Cu.reportError);
this._chunkTimer = null; this._chunkTimer = null;
} }
if (!sorted) {
return;
}
// Before the muxer.sort call above, this.context.results should never be // Before the muxer.sort call above, this.context.results should never be
// empty since this method is called when results are added. But the muxer // empty since this method is called when results are added. But the muxer
// may have excluded one or more results from the final sorted list. For // may have excluded one or more results from the final sorted list. For

Просмотреть файл

@ -651,7 +651,7 @@ var UrlbarUtils = {
"usercontextid" "usercontextid"
), ),
allowSearchSuggestions: false, allowSearchSuggestions: false,
providers: ["UnifiedComplete"], providers: ["UnifiedComplete", "HeuristicFallback"],
}); });
await UrlbarProvidersManager.startQuery(context); await UrlbarProvidersManager.startQuery(context);
if (!context.heuristicResult) { if (!context.heuristicResult) {
@ -1000,6 +1000,8 @@ class UrlbarQueryContext {
} }
this.lastResultCount = 0; this.lastResultCount = 0;
this.allHeuristicResults = [];
this.pendingHeuristicProviders = new Set();
this.userContextId = this.userContextId =
options.userContextId || options.userContextId ||
Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;

Просмотреть файл

@ -598,7 +598,7 @@ class TestProvider extends UrlbarProvider {
*/ */
constructor({ constructor({
results, results,
name = "TestProvider" + Math.floor(Math.random() * 100000), name = Math.floor(Math.random() * 100000),
type = UrlbarUtils.PROVIDER_TYPE.PROFILE, type = UrlbarUtils.PROVIDER_TYPE.PROFILE,
priority = 0, priority = 0,
addTimeout = 0, addTimeout = 0,
@ -613,7 +613,7 @@ class TestProvider extends UrlbarProvider {
this._onCancel = onCancel; this._onCancel = onCancel;
} }
get name() { get name() {
return this._name; return "TestProvider" + this._name;
} }
get type() { get type() {
return this._type; return this._type;

Просмотреть файл

@ -21,19 +21,19 @@ add_task(async function urlToTip() {
]); ]);
// Add a provider that returns a tip result when the search string is "testx". // Add a provider that returns a tip result when the search string is "testx".
let tipResult = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.TIP,
UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
{
text: "This is a test tip.",
buttonText: "OK",
helpUrl: "http://example.com/",
type: "test",
}
);
tipResult.suggestedIndex = 1;
let provider = new UrlbarTestUtils.TestProvider({ let provider = new UrlbarTestUtils.TestProvider({
results: [ results: [tipResult],
new UrlbarResult(
UrlbarUtils.RESULT_TYPE.TIP,
UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
{
text: "This is a test tip.",
buttonText: "OK",
helpUrl: "http://example.com/",
type: "test",
}
),
],
}); });
provider.isActive = context => context.searchString == "testx"; provider.isActive = context => context.searchString == "testx";
UrlbarProvidersManager.registerProvider(provider); UrlbarProvidersManager.registerProvider(provider);
@ -122,19 +122,19 @@ add_task(async function tipToURL() {
// Add a provider that returns a tip result when the search string is "test" // Add a provider that returns a tip result when the search string is "test"
// or "testxx". // or "testxx".
let tipResult = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.TIP,
UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
{
text: "This is a test tip.",
buttonText: "OK",
helpUrl: "http://example.com/",
type: "test",
}
);
tipResult.suggestedIndex = 1;
let provider = new UrlbarTestUtils.TestProvider({ let provider = new UrlbarTestUtils.TestProvider({
results: [ results: [tipResult],
new UrlbarResult(
UrlbarUtils.RESULT_TYPE.TIP,
UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
{
text: "This is a test tip.",
buttonText: "OK",
helpUrl: "http://example.com/",
type: "test",
}
),
],
}); });
provider.isActive = context => provider.isActive = context =>
["test", "testxx"].includes(context.searchString); ["test", "testxx"].includes(context.searchString);

Просмотреть файл

@ -359,30 +359,16 @@ add_task(async function test_filter_priority() {
/** /**
* A test provider. * A test provider.
*/ */
class TestProvider extends UrlbarProvider { class TestProvider extends UrlbarTestUtils.TestProvider {
constructor(priority, shouldBeInvoked, namePart = "") { constructor(priority, shouldBeInvoked, namePart = "") {
super(); super();
this._priority = priority; this._priority = priority;
this._name = `Provider-${priority}` + namePart; this._name = `${priority}` + namePart;
this._shouldBeInvoked = shouldBeInvoked; this._shouldBeInvoked = shouldBeInvoked;
} }
get name() {
return this._name;
}
get type() {
return UrlbarUtils.PROVIDER_TYPE.PROFILE;
}
isActive(context) {
return true;
}
getPriority(context) {
return this._priority;
}
async startQuery(context, add) { async startQuery(context, add) {
Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`); Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`);
} }
cancelQuery(context) {}
pickResult(result) {}
} }
// Test all possible orderings of the providers to make sure the logic that // Test all possible orderings of the providers to make sure the logic that

Просмотреть файл

@ -122,7 +122,7 @@ class TipProvider extends UrlbarProvider {
this._results = results; this._results = results;
} }
get name() { get name() {
return "TestTipProvider"; return "TestProviderTip";
} }
get type() { get type() {
return UrlbarUtils.PROVIDER_TYPE.PROFILE; return UrlbarUtils.PROVIDER_TYPE.PROFILE;