Bug 1389125 - Refactor ActivityStreamProvider in preparation for Highlights. r=ursula

Add helpers for shared adjusting limit, bookmarkGuid sub-SELECT, WHERE and params. More efficiently select https and correctly select bookmarks. Remove _addETLD, getHistorySize and getBookmarksSize. Allow for activity stream caller to customize more options.

MozReview-Commit-ID: Lj9AhoFJar

--HG--
extra : rebase_source : fb4bb13969b47c28c1a137075304efb23c254182
This commit is contained in:
Ed Lee 2017-09-02 13:13:52 -07:00
Родитель 2ec05bb44f
Коммит 85b1aa47dd
2 изменённых файлов: 125 добавлений и 110 удалений

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

@ -53,11 +53,11 @@ const LINKS_GET_LINKS_LIMIT = 100;
// The gather telemetry topic.
const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
// The number of top sites to display on Activity Stream page
const TOP_SITES_LENGTH = 6;
// Some default frecency threshold for Activity Stream requests
const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150;
// Use double the number to allow for immediate display when blocking sites
const TOP_SITES_LIMIT = TOP_SITES_LENGTH * 2;
// Some default query limit for Activity Stream requests
const ACTIVITY_STREAM_DEFAULT_LIMIT = 12;
/**
* Calculate the MD5 hash for a string.
@ -824,19 +824,56 @@ var PlacesProvider = {
* history changes.
*/
var ActivityStreamProvider = {
/**
* Shared adjustment for selecting potentially blocked links.
*/
_adjustLimitForBlocked({ignoreBlocked, numItems}) {
// Just use the usual number if blocked links won't be filtered out
if (ignoreBlocked) {
return numItems;
}
// Additionally select the number of blocked links in case they're removed
return Object.keys(BlockedLinks.links).length + numItems;
},
/**
* Process links after getting them from the database.
*
* @param {Array} aLinks
* an array containing link objects
*
* @returns {Array} an array of checked links with favicons and eTLDs added
* Shared sub-SELECT to get the guid of a bookmark of the current url while
* avoiding LEFT JOINs on moz_bookmarks. This avoids gettings tags. The guid
* could be one of multiple possible guids. Assumes `moz_places h` is in FROM.
*/
_processLinks(aLinks) {
let links_ = aLinks.filter(link => LinkChecker.checkLoadURI(link.url));
links_ = this._faviconBytesToDataURI(links_);
return this._addETLD(links_);
_commonBookmarkGuidSelect: `(
SELECT guid
FROM moz_bookmarks b
WHERE fk = h.id
AND type = :bookmarkType
AND (
SELECT id
FROM moz_bookmarks p
WHERE p.id = b.parent
AND p.parent <> :tagsFolderId
) NOTNULL
) AS bookmarkGuid`,
/**
* Shared WHERE expression filtering out undesired pages, e.g., hidden,
* unvisited, and non-http/s urls. Assumes moz_places is in FROM / JOIN.
*/
_commonPlacesWhere: `
AND hidden = 0
AND last_visit_date > 0
AND (SUBSTR(url, 1, 6) == "https:"
OR SUBSTR(url, 1, 5) == "http:")
`,
/**
* Shared parameters for getting correct bookmarks and LIMITed queries.
*/
_getCommonParams(aOptions, aParams = {}) {
return Object.assign({
bookmarkType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
limit: this._adjustLimitForBlocked(aOptions),
tagsFolderId: PlacesUtils.tagsFolderId
}, aParams);
},
/**
@ -905,58 +942,45 @@ var ActivityStreamProvider = {
return aLinks;
},
/**
* Add the eTLD to each link in the array of links.
*
* @param {Array} aLinks
* an array containing objects with urls
*
* @returns {Array} an array of links with eTLDs added
*/
_addETLD(aLinks) {
return aLinks.map(link => {
try {
link.eTLD = Services.eTLD.getPublicSuffix(Services.io.newURI(link.url));
} catch (e) {
link.eTLD = "";
}
return link;
});
},
/*
* Gets the top frecent sites for Activity Stream.
*
* @param {Object} aOptions
* options.ignoreBlocked: Do not filter out blocked links .
* {bool} ignoreBlocked: Do not filter out blocked links.
* {int} numItems: Maximum number of items to return.
* {int} topsiteFrecency: Minimum amount of frecency for a site.
*
* @returns {Promise} Returns a promise with the array of links as payload.
*/
async getTopFrecentSites(aOptions = {}) {
let {ignoreBlocked} = aOptions;
async getTopFrecentSites(aOptions) {
const options = Object.assign({
ignoreBlocked: false,
numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
topsiteFrecency: ACTIVITY_STREAM_DEFAULT_FRECENCY
}, aOptions || {});
// Get extra links in case they're blocked and double the usual limit in
// case the host is deduped between with www or not-www (i.e., 2 hosts) as
// well as an extra buffer for multiple pages from the same exact host.
const limit = Object.keys(BlockedLinks.links).length + TOP_SITES_LIMIT * 2 * 10;
// Double the item count in case the host is deduped between with www or
// not-www (i.e., 2 hosts) and an extra buffer for multiple pages per host.
const origNumItems = options.numItems;
options.numItems *= 2 * 10;
// Keep this query fast with frecency-indexed lookups (even with excess
// rows) and shift the more complex logic to post-processing afterwards
const sqlQuery = `SELECT
moz_bookmarks.guid AS bookmarkGuid,
frecency,
moz_places.guid,
last_visit_date / 1000 AS lastVisitDate,
rev_host,
moz_places.title,
url
FROM moz_places
LEFT JOIN moz_bookmarks
ON moz_places.id = moz_bookmarks.fk
WHERE hidden = 0 AND last_visit_date NOTNULL
AND (SUBSTR(moz_places.url, 1, 6) == "https:" OR SUBSTR(moz_places.url, 1, 5) == "http:")
ORDER BY frecency DESC
LIMIT ${limit}`;
const sqlQuery = `
SELECT
${this._commonBookmarkGuidSelect},
frecency,
guid,
last_visit_date / 1000 AS lastVisitDate,
rev_host,
title,
url
FROM moz_places h
WHERE frecency >= :frecencyThreshold
${this._commonPlacesWhere}
ORDER BY frecency DESC
LIMIT :limit
`;
let links = await this.executePlacesQuery(sqlQuery, {
columns: [
@ -966,7 +990,10 @@ var ActivityStreamProvider = {
"lastVisitDate",
"title",
"url"
]
],
params: this._getCommonParams(options, {
frecencyThreshold: options.topsiteFrecency
})
});
// Determine if the other link is "better" (larger frecency, more recent,
@ -993,7 +1020,7 @@ var ActivityStreamProvider = {
// Clean up the returned links by removing blocked, deduping, etc.
const exactHosts = new Map();
for (const link of links) {
if (!ignoreBlocked && BlockedLinks.isBlocked(link)) {
if (!options.ignoreBlocked && BlockedLinks.isBlocked(link)) {
continue;
}
@ -1014,10 +1041,10 @@ var ActivityStreamProvider = {
}
// Pick out the top links using the same comparer as before
links = [...hosts.values()].sort(isOtherBetter).slice(0, TOP_SITES_LIMIT);
links = [...hosts.values()].sort(isOtherBetter).slice(0, origNumItems);
links = await this._addFavicons(links);
return this._processLinks(links);
// Get the favicons as data URI for now (until we use the favicon protocol)
return this._faviconBytesToDataURI(await this._addFavicons(links));
},
/**
@ -1039,31 +1066,6 @@ var ActivityStreamProvider = {
return result;
},
/**
* Gets History size
*
* @returns {Promise} Returns a promise with the count of moz_places records
*/
async getHistorySize() {
let sqlQuery = `SELECT count(*) FROM moz_places
WHERE hidden = 0 AND last_visit_date NOT NULL`;
let result = await this.executePlacesQuery(sqlQuery);
return result;
},
/**
* Gets Bookmarks count
*
* @returns {Promise} Returns a promise with the count of bookmarks
*/
async getBookmarksSize() {
let sqlQuery = `SELECT count(*) FROM moz_bookmarks WHERE type = :type`;
let result = await this.executePlacesQuery(sqlQuery, {params: {type: PlacesUtils.bookmarks.TYPE_BOOKMARK}});
return result;
},
/**
* Executes arbitrary query against places database
*

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

@ -3,6 +3,17 @@
// See also browser/base/content/test/newtab/.
function getBookmarksSize() {
return NewTabUtils.activityStreamProvider.executePlacesQuery(
"SELECT count(*) FROM moz_bookmarks WHERE type = :type",
{params: {type: PlacesUtils.bookmarks.TYPE_BOOKMARK}});
}
function getHistorySize() {
return NewTabUtils.activityStreamProvider.executePlacesQuery(
"SELECT count(*) FROM moz_places WHERE hidden = 0 AND last_visit_date NOT NULL");
}
add_task(async function validCacheMidPopulation() {
let expectedLinks = makeLinks(0, 3, 1);
@ -357,7 +368,7 @@ add_task(async function getTopFrecentSites() {
await setUpActivityStreamTest();
let provider = NewTabUtils.activityStreamLinks;
let links = await provider.getTopSites();
let links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 0, "empty history yields empty links");
// add a visit
@ -365,9 +376,11 @@ add_task(async function getTopFrecentSites() {
await PlacesTestUtils.addVisits(testURI);
links = await provider.getTopSites();
Assert.equal(links.length, 0, "adding a single visit doesn't exceed default threshold");
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "adding a visit yields a link");
Assert.equal(links[0].url, testURI, "added visit corresponds to added url");
Assert.equal(links[0].eTLD, "com", "added visit mozilla.com has 'com' eTLD");
});
add_task(async function getTopFrecentSites_dedupeWWW() {
@ -375,7 +388,7 @@ add_task(async function getTopFrecentSites_dedupeWWW() {
let provider = NewTabUtils.activityStreamLinks;
let links = await provider.getTopSites();
let links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 0, "empty history yields empty links");
// add a visit without www
@ -387,7 +400,7 @@ add_task(async function getTopFrecentSites_dedupeWWW() {
await PlacesTestUtils.addVisits(testURI);
// Test combined frecency score
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
Assert.equal(links[0].frecency, 200, "frecency scores are combined");
@ -396,13 +409,13 @@ add_task(async function getTopFrecentSites_dedupeWWW() {
await PlacesTestUtils.addVisits(noWWW);
let withWWW = "http://www.mozilla.com/page";
await PlacesTestUtils.addVisits(withWWW);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
Assert.equal(links[0].frecency, 200, "frecency scores are combined ignoring extra pages");
// add another visit with www
await PlacesTestUtils.addVisits(withWWW);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "still yields one link");
Assert.equal(links[0].url, withWWW, "more frecent www link is used");
Assert.equal(links[0].frecency, 300, "frecency scores are combined ignoring extra pages");
@ -410,7 +423,7 @@ add_task(async function getTopFrecentSites_dedupeWWW() {
// add a couple more visits to the no-www page
await PlacesTestUtils.addVisits(noWWW);
await PlacesTestUtils.addVisits(noWWW);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "still yields one link");
Assert.equal(links[0].url, noWWW, "now more frecent no-www link is used");
Assert.equal(links[0].frecency, 500, "frecency scores are combined ignoring extra pages");
@ -428,7 +441,7 @@ add_task(async function getTopFrencentSites_maxLimit() {
await PlacesTestUtils.addVisits(testURI);
}
let links = await provider.getTopSites();
let links = await provider.getTopSites({topsiteFrecency: 100});
Assert.ok(links.length < MANY_LINKS, "query default limited to less than many");
Assert.ok(links.length > 6, "query default to more than visible count");
});
@ -442,28 +455,28 @@ add_task(async function getTopFrencentSites_allowedProtocols() {
let testURI = "file:///some/file/path.png";
await PlacesTestUtils.addVisits(testURI);
let links = await provider.getTopSites();
let links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 0, "don't get sites with the file:// protocol");
// now add a site with an allowed protocol
testURI = "http://www.mozilla.com";
await PlacesTestUtils.addVisits(testURI);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "http:// is an allowed protocol");
// and just to be sure, add a visit to a site with ftp:// protocol
testURI = "ftp://bad/example";
await PlacesTestUtils.addVisits(testURI);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 1, "we still only accept http:// and https:// for top sites");
// add a different allowed protocol
testURI = "https://https";
await PlacesTestUtils.addVisits(testURI);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 100});
Assert.equal(links.length, 2, "we now accept both http:// and https:// for top sites");
});
@ -487,7 +500,7 @@ add_task(async function getTopFrecentSites_order() {
{uri: "https://mozilla4.com/3", visitDate: timeLater}
];
let links = await provider.getTopSites();
let links = await provider.getTopSites({topsiteFrecency: 0});
Assert.equal(links.length, 0, "empty history yields empty links");
let base64URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
@ -500,7 +513,7 @@ add_task(async function getTopFrecentSites_order() {
await PlacesTestUtils.addVisits(visits);
await PlacesTestUtils.addFavicons(faviconData);
links = await provider.getTopSites();
links = await provider.getTopSites({topsiteFrecency: 0});
Assert.equal(links.length, visits.length, "number of links added is the same as obtain by getTopFrecentSites");
// first link doesn't have a favicon
@ -538,12 +551,12 @@ add_task(async function activitySteamProvider_deleteHistoryLink() {
{uri: "https://mozilla2.com/1", visitDate: timeDaysAgo(0)}
];
let size = await NewTabUtils.activityStreamProvider.getHistorySize();
let size = await getHistorySize();
Assert.equal(size, 0, "empty history has size 0");
await PlacesTestUtils.addVisits(visits);
size = await NewTabUtils.activityStreamProvider.getHistorySize();
size = await getHistorySize();
Assert.equal(size, 2, "expected history size");
// delete a link
@ -551,7 +564,7 @@ add_task(async function activitySteamProvider_deleteHistoryLink() {
Assert.equal(deleted, true, "link is deleted");
// ensure that there's only one link left
size = await NewTabUtils.activityStreamProvider.getHistorySize();
size = await getHistorySize();
Assert.equal(size, 1, "expected history size");
// pin the link and delete it
@ -564,7 +577,7 @@ add_task(async function activitySteamProvider_deleteHistoryLink() {
// delete the pinned link and ensure it was both deleted from history and unpinned
deleted = await provider.deleteHistoryEntry("https://mozilla1.com/0");
size = await NewTabUtils.activityStreamProvider.getHistorySize();
size = await getHistorySize();
Assert.equal(deleted, true, "link is deleted");
Assert.equal(size, 0, "expected history size");
Assert.equal(NewTabUtils.pinnedLinks.links.length, 0, "unpinned the deleted link");
@ -575,18 +588,18 @@ add_task(async function activityStream_deleteBookmark() {
let provider = NewTabUtils.activityStreamLinks;
let bookmarks = [
{url: "https://mozilla1.com/0", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK},
{url: "https://mozilla1.com/1", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK}
{url: "https://mozilla1.com/0", parentGuid: PlacesUtils.bookmarks.unfiledGuid},
{url: "https://mozilla1.com/1", parentGuid: PlacesUtils.bookmarks.unfiledGuid}
];
let bookmarksSize = await NewTabUtils.activityStreamProvider.getBookmarksSize();
let bookmarksSize = await getBookmarksSize();
Assert.equal(bookmarksSize, 0, "empty bookmarks yields 0 size");
for (let placeInfo of bookmarks) {
await PlacesUtils.bookmarks.insert(placeInfo);
}
bookmarksSize = await NewTabUtils.activityStreamProvider.getBookmarksSize();
bookmarksSize = await getBookmarksSize();
Assert.equal(bookmarksSize, 2, "size 2 for 2 bookmarks added");
let bookmarkGuid = await new Promise(resolve => PlacesUtils.bookmarks.fetch(
@ -594,7 +607,7 @@ add_task(async function activityStream_deleteBookmark() {
let deleted = await provider.deleteBookmark(bookmarkGuid);
Assert.equal(deleted.guid, bookmarkGuid, "the correct bookmark was deleted");
bookmarksSize = await NewTabUtils.activityStreamProvider.getBookmarksSize();
bookmarksSize = await getBookmarksSize();
Assert.equal(bookmarksSize, 1, "size 1 after deleting");
});
@ -616,12 +629,12 @@ add_task(async function activityStream_blockedURLs() {
{uri: "https://example4.com/", visitDate: timeEarlier, transition: TRANSITION_TYPED}
];
await PlacesTestUtils.addVisits(visits);
await PlacesUtils.bookmarks.insert({url: "https://example5.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK});
await PlacesUtils.bookmarks.insert({url: "https://example5.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid});
let sizeQueryResult;
// bookmarks
sizeQueryResult = await NewTabUtils.activityStreamProvider.getBookmarksSize();
sizeQueryResult = await getBookmarksSize();
Assert.equal(sizeQueryResult, 1, "got the correct bookmark size");
});