Reduce duplicates places queries #3695
Conversation
|
And probably something similar can happen for Highlights as well? |
|
@Mardak I’ve simulated a delayed TopSitesFeed startup by doing Does this flow match the one in the issue? I'm trying to reproduce it so that maybe we can fix it some other way that doesn't involve multiple calls to |
|
@k88hudson I wonder if this is the right place/time to just get rid of refreshing sites/highlights on NEW_TAB_LOAD especially if both are refreshing based on last updated time. Probably do it on SYSTEM_TICK? In terms of avoiding double Highlights places call on INIT and TOP_SITES_UPDATED, instead of awaiting for top sites from INIT, maybe just return early and remember that the next TOP_SITES_UPDATED should broadcast. Although if TopSitesFeed always broadcasts… should Highlights getting TOP_SITES_UPDATED also broadcast? |
|
Actually nevermind the last question. I would think SYSTEM_TICK does /not/ broadcast and just updates the store for the next rehydration. So similarly highlights shouldn't broadcast on TOP_SITES_UPDATED unless a previous broadcast was skipped because there was no top sites data. |
This would fix the extra calls on startup for both
Highlights should return early if Topsites are not ready and broadcast if its store state is not initialized (this should only happen for initialization). |
|
Oh, checking for Highlights's section initialized value should work. I just realized Highlights probably shouldn't refresh on any actions that TopSitesFeed is also watching; otherwise, we end up with the double updates as we do now when TOP_SITES_UPDATED comes in. Although I can't think of a good way to maintain that dependency right now… Or maybe something like: constructor() {
this.broadcastNextUpdate = true;
}
onAction() {
case MIGRATION:
case PLACES_*:
this.broadcastNextUpdate = true;
break;
case TOP_SITES_UPDATED:
this.fetchHighlights();
} |
| @@ -54,6 +55,12 @@ this.HighlightsFeed = class HighlightsFeed { | |||
| } | |||
|
|
|||
| async fetchHighlights(broadcast = false) { | |||
| if (this._updateInProgress) { | |||
k88hudson
Oct 13, 2017
•
Member
The only issue I've seen with this approach is if an update is in progress where an actual change happened (e.g. something was edited, blocked) the old data is returned instead of the more recent data, which is ok in some cases, but not so great in others.
The only issue I've seen with this approach is if an update is in progress where an actual change happened (e.g. something was edited, blocked) the old data is returned instead of the more recent data, which is ok in some cases, but not so great in others.
piatra
Oct 13, 2017
Author
Collaborator
@k88hudson We could add && !broadcast if that's the only issue. What about removing refresh entirely from new tab load and defer to SYSTEM_TICK?
@k88hudson We could add && !broadcast if that's the only issue. What about removing refresh entirely from new tab load and defer to SYSTEM_TICK?
That's what I was trying to do with this check, before calling on TOP_SITES_UPDATED, HighlightsFeed should only call fetchHighlights if we know some items will get deduped (removed). There is the case where you unpin something and it falls out of your Topsites (which shouldn't even happen #3676) case in which maybe a Highlight should show up. But having that show up at the next refresh seems totally resonable (to simplify the logic). |
|
One drawback of So a different approach that could make it in the worst case 1 fetch would perhaps be better. |
|
Instead of checking TopSites.rows and Highlights.rows for matching URLs I could directly call dedupe and see if the length of the result is different. |
|
One thing I just noticed from my own browsing is that because we currently broadcast fetchHighlights on a NEW_TAB_LOAD, often times I end up seeing highlights without images because it only just realized there are new highlights and should fetch an image. But by doing it some time other than TAB_LOAD, there's a higher chance that the image will already be fetched. |
| this.fetchHighlights(true); | ||
| } else if (Date.now() - this.highlightsLastUpdated >= HIGHLIGHTS_UPDATE_TIME) { | ||
| case at.SYSTEM_TICK: | ||
| if (Date.now() - this.highlightsLastUpdated >= HIGHLIGHTS_UPDATE_TIME) { |
Mardak
Oct 14, 2017
Member
There's 3 different "timers" that are being considered here (and for top sites):
- system tick interval (5 minutes)
- feed last updated (15 minutes)
- links cache expiration (5 minutes)
Does it make sense to still have all 3? Or maybe a related question is what's each of their purpose now?
Another issue with similar but slightly different timers is that if they're a little bit off, an expected 5 minute refresh gets skipped and is only actually refreshed after 10 minutes. E.g., due to async stuff, last update was at T+5.01 and the next system tick at T+10.0 is only 4.99 minutes after the last update.
There's 3 different "timers" that are being considered here (and for top sites):
- system tick interval (5 minutes)
- feed last updated (15 minutes)
- links cache expiration (5 minutes)
Does it make sense to still have all 3? Or maybe a related question is what's each of their purpose now?
Another issue with similar but slightly different timers is that if they're a little bit off, an expected 5 minute refresh gets skipped and is only actually refreshed after 10 minutes. E.g., due to async stuff, last update was at T+5.01 and the next system tick at T+10.0 is only 4.99 minutes after the last update.
piatra
Oct 16, 2017
Author
Collaborator
I think that HIGHLIGHTS_UPDATE_TIME is the only one not needed anymore. Looks like a work around to avoid frequent updates on every page load.
-
system tick interval is needed because this is the new event we react to, independent of page loads. This should also fix the problem of refreshing highlights before having the image ready as described in comment
-
links cache expiration ensures we get new highlights regardless of special events (like BOOKMARKS_REMOVED/ADDED) that invalidate the cache or our different invalidation tactics (every LAST_FEED_UPDATED minutes).
I'm not sure the timing issue is very important (maybe I don't get it) but we force update on special events for blocking and bookmarking where we need to ensure consistent information. If we don't update the highlights with visits once in a while it should be ok.
I think that HIGHLIGHTS_UPDATE_TIME is the only one not needed anymore. Looks like a work around to avoid frequent updates on every page load.
-
system tick interval is needed because this is the new event we react to, independent of page loads. This should also fix the problem of refreshing highlights before having the image ready as described in comment
-
links cache expiration ensures we get new highlights regardless of special events (like BOOKMARKS_REMOVED/ADDED) that invalidate the cache or our different invalidation tactics (every LAST_FEED_UPDATED minutes).
I'm not sure the timing issue is very important (maybe I don't get it) but we force update on special events for blocking and bookmarking where we need to ensure consistent information. If we don't update the highlights with visits once in a while it should be ok.
Mardak
Oct 16, 2017
Member
The "don't update the highlights with visits once in a while" is actually "every other time" if the timing is just off. It's just being clear that the code might say "refresh every 5 minutes" but turns out to be "refresh every 10 minutes." That may be acceptable, but if 5 minutes was picked for a particular reason, it shouldn't just accidentally be twice as long.
The "don't update the highlights with visits once in a while" is actually "every other time" if the timing is just off. It's just being clear that the code might say "refresh every 5 minutes" but turns out to be "refresh every 10 minutes." That may be acceptable, but if 5 minutes was picked for a particular reason, it shouldn't just accidentally be twice as long.
piatra
Oct 17, 2017
•
Author
Collaborator
Could we move
this.highlightsLastUpdated = Date.now();
At the top of the fetchHighlights method? I've tried it and it seems to consistently trigger updates at the right moment.
Or maybe change HIGHLIGHTS_UPDATE_TIME to be 3 * SYSTEM_TICK. Instead of counting on timestamp difference we could count modulo 3 and trigger refresh.
Could we move
this.highlightsLastUpdated = Date.now();
At the top of the fetchHighlights method? I've tried it and it seems to consistently trigger updates at the right moment.
Or maybe change HIGHLIGHTS_UPDATE_TIME to be 3 * SYSTEM_TICK. Instead of counting on timestamp difference we could count modulo 3 and trigger refresh.
Mardak
Oct 17, 2017
Member
I thought you were suggesting HIGHLIGHTS_UPDATE_TIME wouldn't be necessary, so there shouldn't be a need to track highlightsLastUpdated ? We could probably just set LinksCache update time to be 10% less than SYSTEM_TICK, i.e., 4.5 minutes, if we're sure we just want to update at least once every 5 minutes.
I thought you were suggesting HIGHLIGHTS_UPDATE_TIME wouldn't be necessary, so there shouldn't be a need to track highlightsLastUpdated ? We could probably just set LinksCache update time to be 10% less than SYSTEM_TICK, i.e., 4.5 minutes, if we're sure we just want to update at least once every 5 minutes.
|
|
||
| dispatchInitLoadEvent() { |
Mardak
Oct 14, 2017
•
Member
Nice that this separated so easily. I would name this something else as there's already too many "dispatch" and "init"s. Probably something along the lines of simulateNewTabEvents or simulateMessagesForExistingTabs ?
Nice that this separated so easily. I would name this something else as there's already too many "dispatch" and "init"s. Probably something along the lines of simulateNewTabEvents or simulateMessagesForExistingTabs ?
| const highlightsIndex = SectionsManager.sections.get(SECTION_ID).order; | ||
| const rows = this.store.getState().Sections[highlightsIndex].rows; | ||
| // No data available, we need to fetch and dedupe results. | ||
| if (!rows.length) { |
Mardak
Oct 14, 2017
Member
This special case seems to do the exact opposite of what the method says it should be doing. If there's no highlights, there's actually nothing to be deduped, i.e., highlights will not be deduped.
This special case seems to do the exact opposite of what the method says it should be doing. If there's no highlights, there's actually nothing to be deduped, i.e., highlights will not be deduped.
piatra
Oct 16, 2017
Author
Collaborator
Problem is I kept adding logic to it as new edge cases emerged. Logic should be fine, maybe rename it to something like shouldFetchHighlights similar to shouldUpdate in React when it receives new props.
Problem is I kept adding logic to it as new edge cases emerged. Logic should be fine, maybe rename it to something like shouldFetchHighlights similar to shouldUpdate in React when it receives new props.
| this.refresh(action.meta.fromTarget); | ||
| case at.SYSTEM_TICK: | ||
| if (Date.now() - this.lastUpdated >= UPDATE_TIME) { | ||
| this.refresh(); |
Mardak
Oct 14, 2017
Member
I believe @k88hudson has mentioned that we want to avoid "random" updates to already opened pages. Looks like we probably want some way to refresh without dispatching to content now and only update chrome… maybe just SendToMain?
I believe @k88hudson has mentioned that we want to avoid "random" updates to already opened pages. Looks like we probably want some way to refresh without dispatching to content now and only update chrome… maybe just SendToMain?
| url: "about:sheep", | ||
| loaded: true, | ||
| portID: "loaded" | ||
| describe("#dispatchInitLoadEvent", () => { |
Mardak
Oct 14, 2017
Member
This should be at the same level as #createChannel and would avoid indenting everything.
This should be at the same level as #createChannel and would avoid indenting everything.
|
Let's remove the feed update time stuff and shorten LinksCache to be less than SYSTEM_TICK. Also would be good to clean up the bool arg APIs without defaults. With those, this should be good to land |
| @@ -55,23 +55,18 @@ this.HighlightsFeed = class HighlightsFeed { | |||
| } | |||
|
|
|||
| async fetchHighlights(broadcast = false) { | |||
| // We need TopSites to have been initialised for deduping. | |||
| // We can return early because we will get another chance to initialize | |||
| // on TOP_SITES_UPDATED. | |||
Mardak
Oct 17, 2017
Member
nit: odd wrapping and probably can just be shorter while still clear with the code below it:
// We need TopSites for deduping, so just wait for TOP_SITES_UPDATED
nit: odd wrapping and probably can just be shorter while still clear with the code below it:
// We need TopSites for deduping, so just wait for TOP_SITES_UPDATED
| // If the last time we refreshed the data is greater than 15 minutes, fetch again. | ||
| this.fetchHighlights(false); | ||
| this.fetchHighlights(true); |
Mardak
Oct 17, 2017
Member
We don't want to broadcast on SYSTEM_TICK and don't want feed update time logic.
We don't want to broadcast on SYSTEM_TICK and don't want feed update time logic.
| @@ -162,7 +154,8 @@ this.HighlightsFeed = class HighlightsFeed { | |||
| this.fetchHighlights(false); | |||
| break; | |||
| case at.TOP_SITES_UPDATED: | |||
| this.fetchHighlights(false); | |||
| // Only broadcast on the first TOP_SITES_UPDATED call. | |||
| this.fetchHighlights(false || this.highlightsLastUpdated === 0); | |||
Mardak
Oct 17, 2017
Member
The false || is useless with the other condition. But looking at this closer, wouldn't we want to broadcast the first fetch independent of which action triggered as opposed to only for TOP_SITES_UPDATED?
The false || is useless with the other condition. But looking at this closer, wouldn't we want to broadcast the first fetch independent of which action triggered as opposed to only for TOP_SITES_UPDATED?
piatra
Oct 17, 2017
Author
Collaborator
How about checking this.store.getState().Sections[Highlights].initialized here and dispatch when false.
Maybe wrap everything in a if highlights.length > 0 as highlights can be empty for fresh profiles.
How about checking this.store.getState().Sections[Highlights].initialized here and dispatch when false.
Maybe wrap everything in a if highlights.length > 0 as highlights can be empty for fresh profiles.
Mardak
Oct 17, 2017
Member
That seems reasonable to switch to broadcasting if not previously initialized. Also noticed that highlightsLength should be unused now that we don't force fetch if fewer than 9 highlights, so that can be removed.
Not entirely sure what you're suggesting for checking length > 0? Don't call updateSection if there's nothing?
That seems reasonable to switch to broadcasting if not previously initialized. Also noticed that highlightsLength should be unused now that we don't force fetch if fewer than 9 highlights, so that can be removed.
Not entirely sure what you're suggesting for checking length > 0? Don't call updateSection if there's nothing?
piatra
Oct 17, 2017
•
Author
Collaborator
Yes, don't broadcast if there's nothing. But then it will not initialize & never show the empty state as that is initialized && !rows.length so nvm.
Yes, don't broadcast if there's nothing. But then it will not initialize & never show the empty state as that is initialized && !rows.length so nvm.
| @@ -138,6 +138,8 @@ this.Store = class Store { | |||
| if (initAction) { | |||
| this.dispatch(initAction); | |||
| } | |||
|
|
|||
| this._messageChannel.simulateMessagesForExistingTabs(); | |||
Mardak
Oct 17, 2017
Member
A comment for why this is here relative to the other code
A comment for why this is here relative to the other code
| */ | ||
| async refresh(target = null) { | ||
| async refresh(broadcast = true) { |
Mardak
Oct 17, 2017
Member
It's a bit confusing that this is default true, but HighlightsFeed has it default false although it looks like all fetchHighlights calls are explicitly passing in a value instead of relying on defaults. Maybe we should just remove the defaults from both places.
It's a bit confusing that this is default true, but HighlightsFeed has it default false although it looks like all fetchHighlights calls are explicitly passing in a value instead of relying on defaults. Maybe we should just remove the defaults from both places.
Mardak
Oct 17, 2017
Member
I suppose if we're touching the callers, might as well clean up the API to not pass in a plain boolean arg, so something more like:
refresh(false) -> refresh({broadcast: false})
("refresh... not?" vs "refresh without broadcast")
I suppose if we're touching the callers, might as well clean up the API to not pass in a plain boolean arg, so something more like:
refresh(false) -> refresh({broadcast: false})
("refresh... not?" vs "refresh without broadcast")
| this.refresh(action.meta.fromTarget); | ||
| case at.SYSTEM_TICK: | ||
| if (Date.now() - this.lastUpdated >= UPDATE_TIME) { | ||
| // No update target, no broadcast. |
Mardak
Oct 17, 2017
Member
Like Highlights, remove the update time stuff. Also, comment is outdated and probably won't be necessary if we have refresh({broadcast: false})
Like Highlights, remove the update time stuff. Also, comment is outdated and probably won't be necessary if we have refresh({broadcast: false})
|
One thing I noticed when testing the PR, but this probably should be its own issue/PR. #3675 added expiring the highlights links cache on BOOKMARK_ADDED. A new profile causes adds of several bookmarks causing the cache to be expired and fetched from places multiple times on startup. |
|
whew! Thanks! |
| // We broadcast when we want to force an update, so get fresh links | ||
| if (broadcast) { | ||
| this.linksCache.expire(); | ||
| async fetchHighlights(options = {}) { |
Mardak
Oct 17, 2017
Member
Let's describe the option we expect and for TopSites too.
Let's describe the option we expect and for TopSites too.
| this.highlightsLastUpdated = Date.now(); | ||
| this.highlightsLength = highlights.length; | ||
| const sectionIndex = SectionsManager.sections.get(SECTION_ID).order; | ||
| const initialized = this.store.getState().Sections[sectionIndex].initialized; |
Mardak
Oct 17, 2017
Member
nit: const {initialized} = …
nit: const {initialized} = …
| this.highlightsLength = highlights.length; | ||
| const sectionIndex = SectionsManager.sections.get(SECTION_ID).order; | ||
| const initialized = this.store.getState().Sections[sectionIndex].initialized; | ||
| // Broadcast when required or if it is the first fetch. |
Mardak
Oct 17, 2017
Member
nit: "first fetch" -> "first update" as INIT is most likely the first fetch.
nit: "first fetch" -> "first update" as INIT is most likely the first fetch.
| @@ -5,7 +5,10 @@ | |||
|
|
|||
| this.EXPORTED_SYMBOLS = ["LinksCache"]; | |||
|
|
|||
| const EXPIRATION_TIME = 5 * 60 * 1000; // 5 minutes | |||
| // This should be slightly less than SYSTEM_TICK_INTERVAL as timers are | |||
| // not precise enough causing skips in the refresh interval. | |||
Mardak
Oct 17, 2017
Member
nit: It's because timer comparisons are too exact ;) and the underlying issue is that async/await functionality will make the last recorded times a little bit later. Might be useful to include the 10%, e.g., "slightly less (10%)"
nit: It's because timer comparisons are too exact ;) and the underlying issue is that async/await functionality will make the last recorded times a little bit later. Might be useful to include the 10%, e.g., "slightly less (10%)"
|
|
||
| assert.calledOnce(feed.linksCache.expire); | ||
| }); | ||
| it("should not call fetchHighlights with broadcast on initialization", () => { |
Mardak
Oct 17, 2017
Member
nit: this doesn't seem to describe the test
nit: this doesn't seem to describe the test
| sinon.stub(feed, "refresh"); | ||
| feed.onAction(newTabAction); | ||
| assert.notCalled(feed.refresh); | ||
| it("should call refresh on SYSTEM_TICK with correct params", () => { |
Mardak
Oct 17, 2017
Member
This seems to do the exact same thing as the previous test.
This seems to do the exact same thing as the previous test.
Closes #3638 and fix #3697
fetchHiglightscall when there are less than 9 highlights cardsINITandNEW_TAB_LOADactions from triggering two update callslastUpdatedfield will prevent requests from happeningTOP_SITES_UPDATEDcheck that Topsites and Highlights have items in common before triggeringfetchHighlightsfetchHiglightsensures deduping but we can skip it if there is nothing to dedupe