-
Notifications
You must be signed in to change notification settings - Fork 1.3k
For #11660: added prefetch for topsites and update in onCreateView() #11668
For #11660: added prefetch for topsites and update in onCreateView() #11668
Conversation
Codecov Report
@@ Coverage Diff @@
## master #11668 +/- ##
=========================================
Coverage 21.77% 21.78%
Complexity 716 716
=========================================
Files 375 376 +1
Lines 15066 15073 +7
Branches 1957 1957
=========================================
+ Hits 3281 3284 +3
- Misses 11503 11505 +2
- Partials 282 284 +2
Continue to review full report at Codecov.
|
7be628c
to
82d5296
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about two things:
- (I think we should address before landing) On launch, the homescreen flashes on my GS5 as it does an additional layout pass.
- (we could land and fast follow-up on this one) We're not blocking for the data the first time so we have non-deterministic behavior which 1) could make instability harder to track down, 2) could confuse our results in FNPRMS, and 3) creates different user experiences that make it harder to trust our experiments (e.g. maybe the difference in behavior changes the outcome of the experiment but it's not something we're measuring).
I tested this on my P2 and GS5 (without measurements) and confirmed that it anecdotally feels faster and the top sites appear at the same time as the rest of the content.
I'd also like to get @csadilek 's feedback because I'm not expert on Fenix code but I want to get these numbers on fnprms to see how they affect startup so I think we should land without the review if we come to a solution for that first issue.
/** | ||
* Observe a LiveData once and unregister from it as soon as the live data returns a value | ||
*/ | ||
fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: observeForeverOnce
? It makes it clear it isn't bound to any lifecycle though it's a bit confusing, lol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the name isn't very indicative. I could put observeOnceWithoutLifecycleOwner
longer name ,but more self explanatory?
/** | ||
* Observe a LiveData once and unregister from it as soon as the live data returns a value | ||
*/ | ||
fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this observer
name is redundant to the anonymous observer we create. Maybe callback
or simple function
?
// This has to be called separately from the consumeFrom block since the coroutine doesn't | ||
// allow our UI to be updated right away and delays our start up. The init block allows us to | ||
// force our UI to render with the data available in our store at fragment creation time. | ||
sessionControlView?.update(homeFragmentStore.state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move next to consumeFrom
block you're referecing.
Also, it might be helpful to encapsulate them both in a helper function so the grouping is clear:
fun updateSessionControlView() { // maybe a better name
fun update(state) {
sessionControlView?.update(state)
}
// explain update now
update(homeFragmentStore.state)
// explain update later
addObserver(::update)
}
Two best practices in play here:
- when I'm adding code with comments, I always think if it'd be more appropriate to put the code in a function to encapsulate the complexity
- when I'm adding code to lifecycle methods, I always think about using a helper to keep the lifecycle methods short and discussing things at a high level (this doesn't seem to be common on Fenix but I think it greatly helps clarity)
@@ -215,6 +215,12 @@ class HomeFragment : Fragment() { | |||
sessionControlInteractor, | |||
homeViewModel | |||
) | |||
|
|||
// This has to be called separately from the consumeFrom block since the coroutine doesn't |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this is unclear. Which consumeFrom block (see my comment below)? What coroutine? What do you mean by "delays our start up"? How? What init block? How does it force our UI to render?
// This has to be called separately from the consumeFrom block since the coroutine doesn't | ||
// allow our UI to be updated right away and delays our start up. The init block allows us to | ||
// force our UI to render with the data available in our store at fragment creation time. | ||
sessionControlView?.update(homeFragmentStore.state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big issue: what happens if the data isn't available when this is called? We might want to consider blocking on it to make this more deterministic (e.g. if our fnprms starts reporting two drastically different numbers randomly, we'll be confused).
I confirmed this isn't deterministic as is: when running debug builds on Pixel 2, I see the top sites animation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The list should be empty and will just update itself as its doing now. we could block on it too if needed
@@ -86,4 +87,10 @@ class TopSiteStorage(private val context: Context) { | |||
context.settings().defaultTopSitesAdded = true | |||
} | |||
} | |||
|
|||
fun prefetch() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think we need to explain why we do this. We should probably only do so once in the code so maybe say to reference some other method (or the other explanation should reference here?)
We should probably also explain why this is innocuous.
// This has to be called separately from the consumeFrom block since the coroutine doesn't | ||
// allow our UI to be updated right away and delays our start up. The init block allows us to | ||
// force our UI to render with the data available in our store at fragment creation time. | ||
sessionControlView?.update(homeFragmentStore.state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big issue: I don't love that we're "setting top sites twice" but only dispatching the change once but it seems innocuous on P2. However, on my GS5, the whole homescreen flashes, presumably because it relays out the RV after this line: https://searchfox.org/mozilla-mobile/rev/d81a3c3fce58d3a6c3ebd3257ad1566c9bd462ef/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt#147 The current behavior does this too but it doesn't look as bad because top sites gets added instead of just replaced.
I'm not sure what we should do about that... Let's discuss
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I don't see it on my G5. We could do a quick "if this is the first call from the consumeFrom, ignore"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is fixed if we remove that line. The line was added in #5591 and was trying to fix #2874, #5131, and #5375. The first two relate to problems with tabs updating on the homescreen – no longer a problem – and I'm unable to reproduce the 3rd issue (maybe also tabs related)? I tried reproducing similar issues with collections:
- open homescreen, open tabs tray, click 3-dot to add to collection, see homescreen
- open history/bookmarks, add collection, open homescreen
But I can't reproduce. I think this is safe to remove but we should check with ekager.
@@ -229,6 +230,13 @@ open class FenixApplication : LocaleAwareApplication() { | |||
// no-op, LeakCanary is disabled by default | |||
} | |||
|
|||
// This is for issue https://github.com/mozilla-mobile/fenix/issues/11660. We prefetch our info for startup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's confusing that we say, "our info" but really mean "top sites" so I'd rather say "We prefetch our top sites for startup" and it can be changed if the code becomes more general later.
StrictMode.allowThreadDiskReads().resetPoliciesAfter { | ||
components.core.topSiteStorage.prefetch() | ||
} | ||
} | ||
private fun setupPush() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: whitespace above
// This is for issue https://github.com/mozilla-mobile/fenix/issues/11660. We prefetch our info for startup | ||
// so that we're sure that we have all the data available as our fragment is launched. | ||
private fun prefetchForHomeFragment() { | ||
StrictMode.allowThreadDiskReads().resetPoliciesAfter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary? I don't see anywhere that would cause us to read on the main thread but perhaps I misunderstand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why, but our strictmodes complained when I didn't add it and the CI tests failed ( some of them). I could try to remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should land as is in order to get these numbers from fnprms and see we're at. I think we should also get retroactive reviews from csadilek and ekager and follow-up with a few PRs:
- Make it deterministic
- Address the nits
* The [SessionControlView] is forced to update with our current state when we call [HomeFragment.onCreateView] | ||
* in order to be able to draw everything at once with the current data in our store. The [View.consumeFrom] coroutine dispatch | ||
* doesn't get run right away which means that we won't draw on the first layout pass. | ||
* */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: */
, not * */
@@ -240,6 +234,19 @@ class HomeFragment : Fragment() { | |||
return view | |||
} | |||
|
|||
/** | |||
* The [SessionControlView] is forced to update with our current state when we call [HomeFragment.onCreateView] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why do we need to draw everything at once? Make it clear this is a perf issue
* doesn't get run right away which means that we won't draw on the first layout pass. | ||
* */ | ||
fun updateSessionControlView(view: View) { | ||
sessionControlView?.update(homeFragmentStore.state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I like the inner function version I suggested because it makes it clear that the code is repeated and prevents someone from missing that if they decide to refactor this. But I can go either way
@@ -216,16 +216,10 @@ class HomeFragment : Fragment() { | |||
homeViewModel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: In the extended commit summary, I would have included information on why it's okay to remove this code (though I guess it's fine if it's just in the PR text? maybe you can link to what's in the PR).
06956bc
to
d07021d
Compare
TopSites will be prefetched with observerOnce (wrapper around observerForever). Also, the SessionControlView.update() is called right away instead of waiting from consumeFrom in the HomeFragment.onCreateView() which will allow the UI to render all at once on its first perform traversal
d07021d
to
b78ae24
Compare
…illa-mobile#11821)" This reverts commit bd7a537.
…illa-mobile#11821)" This reverts commit bd7a537.
Took the timing for this fix on a g5+
With changes:
and without the changes:
I do see a net gain with these changes. It might be worthwhile to implement the same
prefetch
for collections since it uses the samelivedata
mechanism 😄FYI I also did a VIEW (the new app-link name) test with FNPRMS and my results are the same as they are currently (so ~2.9 seconds). This change shouldn't affect it.
Pull Request checklist
After merge
To download an APK when reviewing a PR: