Bug 1464454 - Expire adaptive history after 90 days and limit the number of top adaptive history matches in the Address Bar. r=adw
Reduce adaptive history domination of the Address Bar results by expiring unused
entries sooner and limiting the number of top adaptive results.
MozReview-Commit-ID: EGOs6rVYGj6
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -55,16 +55,17 @@ const PREF_OTHER_DEFAULTS = new Map([
["keyword.enabled", true],
]);
// AutoComplete query type constants.
// Describes the various types of queries that we can process rows for.
const QUERYTYPE_FILTERED = 0;
const QUERYTYPE_AUTOFILL_ORIGIN = 1;
const QUERYTYPE_AUTOFILL_URL = 2;
+const QUERYTYPE_ADAPTIVE = 3;
// This separator is used as an RTL-friendly way to split the title and tags.
// It can also be used by an nsIAutoCompleteResult consumer to re-split the
// "comment" back into the title and the tag.
const TITLE_TAGS_SEPARATOR = " \u2013 ";
// Telemetry probes.
const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
@@ -925,16 +926,20 @@ function Search(searchString, searchPara
this._previousSearchMatchTypes.push(MATCHTYPE.SUGGESTION);
} else if (style.includes("extension")) {
this._previousSearchMatchTypes.push(MATCHTYPE.EXTENSION);
} else {
this._previousSearchMatchTypes.push(MATCHTYPE.GENERAL);
}
}
+ // Used to limit the number of adaptive results.
+ this._adaptiveCount = 0;
+ this._extraAdaptiveRows = [];
+
// This is a replacement for this._result.matchCount, to be used when you need
// to check how many "current" matches have been inserted.
// Indeed this._result.matchCount may include matches from the previous search.
this._currentMatchCount = 0;
// These are used to avoid adding duplicate entries to the results.
this._usedURLs = [];
this._usedPlaceIds = new Set();
@@ -1206,16 +1211,22 @@ Search.prototype = {
// Finally run all the other queries.
for (let [query, params] of queries) {
await conn.executeCached(query, params, this._onResultRow.bind(this));
if (!this.pending)
return;
}
+ // If we have some unused adaptive matches, add them now.
+ while (this._extraAdaptiveRows.length &&
+ this._currentMatchCount < Prefs.get("maxRichResults")) {
+ this._addFilteredQueryMatch(this._extraAdaptiveRows.shift());
+ }
+
// Ideally we should wait until MATCH_BOUNDARY_ANYWHERE, but that query
// may be really slow and we may end up showing old results for too long.
this._cleanUpNonCurrentMatches(MATCHTYPE.GENERAL);
// If we do not have enough results, and our match type is
// MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
// results.
let count = this._counts[MATCHTYPE.GENERAL] + this._counts[MATCHTYPE.HEURISTIC];
@@ -1801,16 +1812,19 @@ Search.prototype = {
case QUERYTYPE_AUTOFILL_ORIGIN:
this._result.setDefaultIndex(0);
this._addOriginAutofillMatch(row);
break;
case QUERYTYPE_AUTOFILL_URL:
this._result.setDefaultIndex(0);
this._addURLAutofillMatch(row);
break;
+ case QUERYTYPE_ADAPTIVE:
+ this._addAdaptiveQueryMatch(row);
+ break;
case QUERYTYPE_FILTERED:
this._addFilteredQueryMatch(row);
break;
}
// If the search has been canceled by the user or by _addMatch, or we
// fetched enough results, we can stop the underlying Sqlite query.
let count = this._counts[MATCHTYPE.GENERAL] + this._counts[MATCHTYPE.HEURISTIC];
if (!this.pending || count >= Prefs.get("maxRichResults")) {
@@ -2111,16 +2125,32 @@ Search.prototype = {
finalCompleteValue,
comment,
frecency,
style: ["autofill"].concat(extraStyles).join(" "),
icon: iconHelper(finalCompleteValue),
});
},
+ // This is the same as _addFilteredQueryMatch, but it only returns a few
+ // results, caching the others. If at the end we don't find other results, we
+ // can add these.
+ _addAdaptiveQueryMatch(row) {
+ // Allow one quarter of the results to be adaptive results.
+ // Note: ideally adaptive results should have their own provider and the
+ // results muxer should decide what to show. But that's too complex to
+ // support in the current code, so that's left for a future refactoring.
+ if (this._adaptiveCount < Math.ceil(Prefs.get("maxRichResults") / 4)) {
+ this._addFilteredQueryMatch(row);
+ } else {
+ this._extraAdaptiveRows.push(row);
+ }
+ this._adaptiveCount++;
+ },
+
_addFilteredQueryMatch(row) {
let match = {};
match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
let bookmarkTitle = bookmarked ?
@@ -2273,17 +2303,17 @@ Search.prototype = {
* database with and an object containing the params to bound.
*/
get _adaptiveQuery() {
return [
SQL_ADAPTIVE_QUERY,
{
parent: PlacesUtils.tagsFolderId,
search_string: this._searchString,
- query_type: QUERYTYPE_FILTERED,
+ query_type: QUERYTYPE_ADAPTIVE,
matchBehavior: this._matchBehavior,
searchBehavior: this._behavior,
userContextId: this._userContextId,
maxResults: Prefs.get("maxRichResults")
}
];
},
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -105,16 +105,18 @@ using namespace mozilla::places;
#define PREF_FREC_UNVISITED_TYPED_BONUS "places.frecency.unvisitedTypedBonus"
#define PREF_FREC_UNVISITED_TYPED_BONUS_DEF 200
#define PREF_FREC_RELOAD_VISIT_BONUS "places.frecency.reloadVisitBonus"
#define PREF_FREC_RELOAD_VISIT_BONUS_DEF 0
// This is a 'hidden' pref for the purposes of unit tests.
#define PREF_FREC_DECAY_RATE "places.frecency.decayRate"
#define PREF_FREC_DECAY_RATE_DEF 0.975f
+// An adaptive history entry is removed if unused for these many days.
+#define ADAPTIVE_HISTORY_EXPIRE_DAYS 90
// In order to avoid calling PR_now() too often we use a cached "now" value
// for repeating stuff. These are milliseconds between "now" cache refreshes.
#define RENEW_CACHED_NOW_TIMEOUT ((int32_t)3 * PR_MSEC_PER_SEC)
// character-set annotation
#define CHARSET_ANNO NS_LITERAL_CSTRING("URIProperties/characterSet")
@@ -2516,60 +2518,70 @@ public:
};
nsresult
nsNavHistory::DecayFrecency()
{
nsresult rv = FixInvalidFrecencies();
NS_ENSURE_SUCCESS(rv, rv);
- float decayRate = Preferences::GetFloat(PREF_FREC_DECAY_RATE, PREF_FREC_DECAY_RATE_DEF);
+ float decayRate = Preferences::GetFloat(PREF_FREC_DECAY_RATE,
+ PREF_FREC_DECAY_RATE_DEF);
+ if (decayRate > 1.0f) {
+ MOZ_ASSERT(false, "The frecency decay rate should not be greater than 1.0");
+ decayRate = PREF_FREC_DECAY_RATE_DEF;
+ }
// Globally decay places frecency rankings to estimate reduced frecency
// values of pages that haven't been visited for a while, i.e., they do
// not get an updated frecency. A scaling factor of .975 results in .5 the
// original value after 28 days.
// When changing the scaling factor, ensure that the barrier in
// moz_places_afterupdate_frecency_trigger still ignores these changes.
nsCOMPtr<mozIStorageAsyncStatement> decayFrecency = mDB->GetAsyncStatement(
"UPDATE moz_places SET frecency = ROUND(frecency * :decay_rate) "
"WHERE frecency > 0"
);
NS_ENSURE_STATE(decayFrecency);
-
rv = decayFrecency->BindDoubleByName(NS_LITERAL_CSTRING("decay_rate"),
static_cast<double>(decayRate));
NS_ENSURE_SUCCESS(rv, rv);
// Decay potentially unused adaptive entries (e.g. those that are at 1)
// to allow better chances for new entries that will start at 1.
nsCOMPtr<mozIStorageAsyncStatement> decayAdaptive = mDB->GetAsyncStatement(
- "UPDATE moz_inputhistory SET use_count = use_count * .975"
+ "UPDATE moz_inputhistory SET use_count = use_count * :decay_rate"
);
NS_ENSURE_STATE(decayAdaptive);
+ rv = decayAdaptive->BindDoubleByName(NS_LITERAL_CSTRING("decay_rate"),
+ static_cast<double>(decayRate));
+ NS_ENSURE_SUCCESS(rv, rv);
// Delete any adaptive entries that won't help in ordering anymore.
nsCOMPtr<mozIStorageAsyncStatement> deleteAdaptive = mDB->GetAsyncStatement(
- "DELETE FROM moz_inputhistory WHERE use_count < .01"
+ "DELETE FROM moz_inputhistory WHERE use_count < :use_count"
);
NS_ENSURE_STATE(deleteAdaptive);
+ rv = deleteAdaptive->BindDoubleByName(NS_LITERAL_CSTRING("use_count"),
+ std::pow(static_cast<double>(decayRate),
+ ADAPTIVE_HISTORY_EXPIRE_DAYS));
+ NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
if (!conn) {
return NS_ERROR_UNEXPECTED;
}
mozIStorageBaseStatement *stmts[] = {
decayFrecency.get(),
decayAdaptive.get(),
deleteAdaptive.get()
};
nsCOMPtr<mozIStoragePendingStatement> ps;
RefPtr<PlacesDecayFrecencyCallback> cb = new PlacesDecayFrecencyCallback();
- rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb,
- getter_AddRefs(ps));
+ rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb, getter_AddRefs(ps));
NS_ENSURE_SUCCESS(rv, rv);
mDecayFrecencyPendingCount++;
return NS_OK;
}
void
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -487,8 +487,33 @@ add_task(async function ensure_search_en
for (let engine of Services.search.getEngines()) {
Services.search.removeEngine(engine);
}
Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
"http://s.example.com/search");
let engine = Services.search.getEngineByName("MozSearch");
Services.search.currentEngine = engine;
});
+
+/**
+ * Add a adaptive result for a given (url, string) tuple.
+ * @param {string} aUrl
+ * The url to add an adaptive result for.
+ * @param {string} aSearch
+ * The string to add an adaptive result for.
+ * @resolves When the operation is complete.
+ */
+function addAdaptiveFeedback(aUrl, aSearch) {
+ let promise = TestUtils.topicObserved("places-autocomplete-feedback-updated");
+ let thing = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() { return thing; },
+ get controller() { return thing; },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: () => aUrl,
+ searchString: aSearch
+ };
+ Services.obs.notifyObservers(thing, "autocomplete-will-enter-text");
+ return promise;
+}
rename from toolkit/components/places/tests/unit/test_adaptive.js
rename to toolkit/components/places/tests/unifiedcomplete/test_adaptive.js
--- a/toolkit/components/places/tests/unit/test_adaptive.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive.js
@@ -13,80 +13,35 @@
*
* This also tests bug 395735 for the instrumentation feedback mechanism.
*
* Bug 411293 is tested to make sure the drop down strongly prefers previously
* typed pages that have been selected and are moved to the top with adaptive
* learning.
*/
-function AutoCompleteInput(aSearches) {
- this.searches = aSearches;
-}
-AutoCompleteInput.prototype = {
- constructor: AutoCompleteInput,
-
- get minResultsForPopup() {
- return 0;
- },
- get timeout() {
- return 10;
- },
- get searchParam() {
- return "";
- },
- get textValue() {
- return "";
- },
- get disableAutoComplete() {
- return false;
- },
- get completeDefaultIndex() {
- return false;
- },
-
- get searchCount() {
- return this.searches.length;
- },
- getSearchAt(aIndex) {
- return this.searches[aIndex];
- },
-
- onSearchBegin() {},
- onSearchComplete() {},
-
- get popupOpen() {
- return false;
- },
- popup: {
- set selectedIndex(aIndex) {},
- invalidate() {},
- QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompletePopup])
- },
-
- QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput])
-};
-
/**
* Checks that autocomplete results are ordered correctly.
*/
function ensure_results(expected, searchTerm, callback) {
let controller = Cc["@mozilla.org/autocomplete/controller;1"].
getService(Ci.nsIAutoCompleteController);
// Make an AutoCompleteInput that uses our searches
// and confirms results on search complete.
let input = new AutoCompleteInput(["unifiedcomplete"]);
controller.input = input;
input.onSearchComplete = function() {
Assert.equal(controller.searchStatus,
- Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
- Assert.equal(controller.matchCount, expected.length);
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH,
+ "The search should be complete");
+ Assert.equal(controller.matchCount, expected.length,
+ "All the expected results should have been found");
for (let i = 0; i < controller.matchCount; i++) {
print("Testing for '" + expected[i].uri.spec + "' got '" + controller.getValueAt(i) + "'");
Assert.equal(controller.getValueAt(i), expected[i].uri.spec);
Assert.equal(controller.getStyleAt(i), expected[i].style);
}
callback();
};
@@ -363,25 +318,45 @@ var tests = [
makeResult(uri1, "tag"),
makeResult(uri2),
];
observer.search = s0;
observer.runCount = c1 + c2;
await task_setCountRank(uri1, c1, c1, s2, "tag");
await task_setCountRank(uri2, c1, c2, s2);
},
+ // Test that many results are all shown if no other results are available.
+ async function() {
+ print("Test 14 - many results");
+ let n = 10;
+ observer.results = Array(n).fill(0).map(
+ (e, i) => makeResult(Services.io.newURI("http://site.tld/" + i))
+ );
+ observer.search = s2;
+ observer.runCount = n * (n + 1) / 2;
+ let c = n;
+ for (let result of observer.results) {
+ task_setCountRank(result.uri, c, c, s2);
+ c--;
+ }
+ },
];
/**
* Test adaptive autocomplete.
*/
add_task(async function test_adaptive() {
// Disable autoFill for this test.
Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
- registerCleanupFunction(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
for (let test of tests) {
// Cleanup.
await PlacesUtils.bookmarks.eraseEverything();
let types = ["history", "bookmark", "openpage"];
for (let type of types) {
Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
}
rename from toolkit/components/places/tests/unit/test_adaptive_bug527311.js
rename to toolkit/components/places/tests/unifiedcomplete/test_adaptive_behaviors.js
--- a/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive_behaviors.js
@@ -1,137 +1,40 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Test for Bug 527311
+// Addressbar suggests adaptive results regardless of the requested behavior.
+
const TEST_URL = "http://adapt.mozilla.org/";
const SEARCH_STRING = "adapt";
const SUGGEST_TYPES = ["history", "bookmark", "openpage"];
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-const PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC =
- "places-autocomplete-feedback-updated";
-
-function cleanup() {
- for (let type of SUGGEST_TYPES) {
- Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
- }
-}
-
-function AutoCompleteInput(aSearches) {
- this.searches = aSearches;
-}
-AutoCompleteInput.prototype = {
- constructor: AutoCompleteInput,
- searches: null,
- minResultsForPopup: 0,
- timeout: 10,
- searchParam: "",
- textValue: "",
- disableAutoComplete: false,
- completeDefaultIndex: false,
-
- get searchCount() {
- return this.searches.length;
- },
-
- getSearchAt: function ACI_getSearchAt(aIndex) {
- return this.searches[aIndex];
- },
-
- onSearchComplete: function ACI_onSearchComplete() {},
-
- popupOpen: false,
-
- popup: {
- setSelectedIndex() {},
- invalidate() {},
-
- QueryInterface: ChromeUtils.generateQI(["nsIAutoCompletePopup"])
- },
-
- onSearchBegin() {},
-
- QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteInput"])
-};
-
+add_task(async function test_adaptive_search_specific() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
-function check_results() {
- return new Promise(resolve => {
- let controller = Cc["@mozilla.org/autocomplete/controller;1"].
- getService(Ci.nsIAutoCompleteController);
- let input = new AutoCompleteInput(["unifiedcomplete"]);
- controller.input = input;
-
- input.onSearchComplete = function() {
- Assert.equal(controller.searchStatus,
- Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
- Assert.equal(controller.matchCount, 0);
-
- PlacesUtils.bookmarks.eraseEverything().then(() => {
- cleanup();
- resolve();
- });
- };
-
- controller.startSearch(SEARCH_STRING);
- });
-}
-
-
-function addAdaptiveFeedback(aUrl, aSearch) {
- return new Promise(resolve => {
- let observer = {
- observe(aSubject, aTopic, aData) {
- Services.obs.removeObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
- do_timeout(0, resolve);
- }
- };
- Services.obs.addObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
-
- let thing = {
- QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput,
- Ci.nsIAutoCompletePopup,
- Ci.nsIAutoCompleteController]),
- get popup() { return thing; },
- get controller() { return thing; },
- popupOpen: true,
- selectedIndex: 0,
- getValueAt: () => aUrl,
- searchString: aSearch
- };
-
- Services.obs.notifyObservers(thing, "autocomplete-will-enter-text");
- });
-}
-
-
-add_task(function init() {
- Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
- registerCleanupFunction(() => {
- Services.prefs.clearUserPref("browser.urlbar.autoFill");
- });
-});
-
-add_task(async function test_adaptive_search_specific() {
// Add a bookmark to our url.
await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: "test_book",
url: TEST_URL,
});
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
// We want to search only history.
for (let type of SUGGEST_TYPES) {
type == "history" ? Services.prefs.setBoolPref("browser.urlbar.suggest." + type, true)
: Services.prefs.setBoolPref("browser.urlbar.suggest." + type, false);
}
// Add an adaptive entry.
await addAdaptiveFeedback(TEST_URL, SEARCH_STRING);
- await check_results();
-
- await PlacesTestUtils.promiseAsyncUpdates();
+ await check_autocomplete({
+ search: SEARCH_STRING,
+ matches: [],
+ });
});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive_limited.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that top adaptive results are limited, remaining ones are enqueued.
+
+add_task(async function() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ let n = 10;
+ let uris = Array(n).fill(0).map((e, i) => "http://site.tld/" + i);
+
+ // Add a bookmark to one url.
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_book",
+ url: uris.shift(),
+ });
+
+ // Make remaining ones adaptive results.
+ for (let uri of uris) {
+ await PlacesTestUtils.addVisits(uri);
+ await addAdaptiveFeedback(uri, "book");
+ }
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
+ let matches = uris.map(uri => ({ uri: Services.io.newURI(uri),
+ title: "test visit for " + uri }));
+ let book_index = Math.ceil(Services.prefs.getIntPref("browser.urlbar.maxRichResults") / 4);
+ matches.splice(book_index, 0, { uri: Services.io.newURI(bm.url.href),
+ title: "test_book", "style": ["bookmark"] });
+
+ await check_autocomplete({
+ search: "book",
+ matches,
+ checkSorting: true,
+ });
+});
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -8,16 +8,19 @@ support-files =
!/toolkit/components/places/tests/favicons/favicon-normal16.png
autofill_tasks.js
[test_416211.js]
[test_416214.js]
[test_417798.js]
[test_418257.js]
[test_422277.js]
+[test_adaptive.js]
+[test_adaptive_behaviors.js]
+[test_adaptive_limited.js]
[test_autocomplete_functional.js]
[test_autocomplete_stopSearch_no_throw.js]
[test_autofill_origins.js]
[test_autofill_search_engines.js]
[test_autofill_urls.js]
[test_avoid_middle_complete.js]
[test_avoid_stripping_to_empty_tokens.js]
[test_casing.js]
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -37,18 +37,16 @@ skip-if = os == "linux"
[test_454977.js]
[test_463863.js]
[test_485442_crash_bug_nsNavHistoryQuery_GetUri.js]
[test_486978_sort_by_date_queries.js]
[test_536081.js]
[test_1085291.js]
[test_1105208.js]
[test_1105866.js]
-[test_adaptive.js]
-[test_adaptive_bug527311.js]
[test_annotations.js]
[test_asyncExecuteLegacyQueries.js]
[test_async_transactions.js]
[test_bookmarks_json.js]
[test_bookmarks_json_corrupt.js]
[test_bookmarks_html.js]
[test_bookmarks_html_corrupt.js]
[test_bookmarks_html_escape_entities.js]