Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy load background tabs at app startup #553

Merged
merged 28 commits into from
Apr 26, 2022
Merged

Conversation

ayoy
Copy link
Collaborator

@ayoy ayoy commented Apr 21, 2022

Task/Issue URL: https://app.asana.com/0/1177771139624306/1201065943414949/f
Tech Design URL:
CC: @mallexxx @tomasstrba

Description:
I added a mechanism that records selected tabs in chronological order and reloads them in that order at app startup.
The logic is as follows:

  1. app launches
  2. if there are no URL tabs or only 1 URL tab and it's currently selected, do nothing, don't even initialize
  3. if current tab is a URL tab, wait until it finishes loading (or fails to load), otherwise proceed immediately
  4. pick up to 3 most recently visited URL tab and reload them in background
  5. as background tabs finish (or fail) loading, repeat previous step (keeping at most 3 concurrent loads) until 20 background tabs are loaded or there are no more URL tabs to load
  6. as the user switches through tabs during lazy loading, record visited tabs and remove them from the lazy loading queue (they were visited manually which triggered a reload)
  7. if 20 tabs were reloaded in background or there are no more not activated URL tabs, report finished work

Additional details:

  • TabLazyLoader is owned by TabCollectionViewModel which serves as its data source
  • When lazy loader reports completion, it gets deallocated
  • Lazy loader does not differentiate between successful and failed loads, it only records website load attempts and completion (regardless of the outcome) - it's therefore not affected by "no internet" scenario
  • the implementation is based on generic types to allow for more thorough unit testing
  • the mechanism is inspired by how Chromium-based browsers work, but magic numbers 3 and 20 mentioned above were picked by Gabriel (still subject to change) - you'll find more information in the Asana task.
  • OSLog.tabLazyLoading was added to help debugging lazy loading (disabled by default)
  • If there are more than 20 tabs at app startup, lazy loading starts with 10 tabs adjacent to the current one, before proceeding to 10 most recently selected.
  • Lazy loading is paused every time the user interacts with the current tab (triggers a navigation).

Steps to test this PR:
You may find this URL helpful when testing: http://tfc.home.pl/dev/dominik/index-delay.php?delay=4
It's a PHP script that loads a simple website after a delay specified in the parameter.
Also enable logging for better insights into what's happening.

Scenario 1:

  1. Open several tabs, try with content-heavy websites such as Asana or news websites
  2. Close the app and reopen
  3. Observe tabs being reloaded in background (tab titles updating) only after the current tab finishes loading

Scenario 2:

  1. Open 1 URL tab and Settings
  2. Activate Settings
  3. Close the app and reopen
  4. Observe that the URL tab gets loaded in background right after the app starts

Scenario 3:

  1. Open >20 URL tabs :)
  2. Close the app and reopen
  3. Observe that only 20 tabs are lazy loaded

Scenario 4:

  1. Open some tabs
  2. On app relaunch, while tabs are loaded, select some of them
  3. Verify in the log that the tabs you selected were not lazy loaded (unless they started loading before being selected)

Scenario 5:

  1. Open Settings, homepage, bookmarks, etc.
  2. Observe that on app relaunch, no lazy loading is performed2.

Scenario 6:

  1. Open some URL tabs, disconnect internet, reload the app
  2. Observe that lazy loading is attempted, completed and the lazy loader is disposed of

...

Testing checklist:

  • Test with Release configuration
  • Test proper deallocation of tabs
  • Make sure committed submodule changes are desired

Internal references:

Software Engineering Expectations
Technical Design Template
When ready for review, remember to post the PR in MM

@tomasstrba tomasstrba self-requested a review April 21, 2022 10:56
@tomasstrba tomasstrba self-assigned this Apr 21, 2022
Copy link
Contributor

@tomasstrba tomasstrba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dominik, this looks good to me! 👍 Very nice code 🎖️ 🏅

Please, I will approve once we discuss suggestions I raised in Asana related to:

  1. Pausing of the lazy loading if currently selected tab starts loading
  2. Disabling of title changes while tab is being lazy loaded

@@ -1091,6 +1097,7 @@ extension Tab: WKNavigationDelegate {
// https://app.asana.com/0/1199230911884351/1200381133504356/f
// hasError = true

webViewDidFailNavigationPublisher.send()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍

@@ -59,6 +59,7 @@ final class TabCollectionViewModel: NSObject {

// In a special occasion, we want to select the "parent" tab after closing the currently selected tab
private var selectParentOnRemoval = false
private var tabLazyLoader: TabLazyLoader<TabCollectionViewModel>?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍

import Combine
import os

final class TabLazyLoader<DataSource: TabLazyLoaderDataSource> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, just to understand, what is the advantage of data source being generic parameter here?

Copy link
Collaborator Author

@ayoy ayoy Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for introducing generics to this code was increasing testability. It starts with Tab class which is hard to work with in unit tests, because it has an assert in deinit requiring to call tabWillClose() before deinitializing. It also contains a web view, etc.

But the lazy loader works on tabs via TabCollectionViewModel, and even if we created a data source protocol that TabCollectionViewModel would implement, that protocol would still make use of the concrete Tab type. And after all, the lazy loader only needs a few properties and functions from Tab, and we definitely don't want to deal with a real webView in unit tests.

Hence the LazyLoadable protocol and the use of associatedtype in the data source protocol. And that associated type makes it impossible to use TabLazyLoaderDataSource as a standalone type, producing the infamous error:

Protocol 'TabLazyLoaderDataSource' can only be used as a generic constraint because it has Self or associated type requirements

So for the lazy loader to define a data source of type TabLazyLoaderDataSource, it needs to become a generic class.

Bottom line - it was not my intention to make the code look sophisticated through the use of generics, but rather it was the only way to get the code to compile after abstracting the data source :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 🙏 More clear to me now :)


var url: URL? { content.url }

var loadingFinishedPublisher: AnyPublisher<Tab, Never> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍

@tomasstrba tomasstrba assigned ayoy and unassigned tomasstrba Apr 21, 2022
… to current

Load 10 adjacent tabs, based on index diff vs current tab: 1, -1, 2, -2, 3, etc.
After that, load 10 recently selected tabs.
@ayoy ayoy added the draft label Apr 21, 2022
@ayoy
Copy link
Collaborator Author

ayoy commented Apr 21, 2022

I added loading adjacent tabs when there are more than 20 tabs, but this PR will keep evolving as I'm adding new features requested in the Asana task comments :) marking as draft for now.

@ayoy ayoy removed the draft label Apr 22, 2022
@ayoy
Copy link
Collaborator Author

ayoy commented Apr 22, 2022

@tomasstrba the PR is ready for review and contains the following changes since last time you've checked it:

  • when there are more than 20 tabs, start with loading 10 tabs adjacent to the tab that was selected at app startup, before proceeding to loading 10 last selected tabs
  • tab title is not refreshed when it's lazy loaded - this limits the number of refreshes but does not eliminate them completely, since title can be updated by JS code after the tab has "fully" loaded according to webView
  • when current tab is reloaded, lazy loading is paused (new tabs are not selected for lazy loading until current tab stops reloading).

I will work on "Reopen Last Closed Window" in a separate pull request as I believe it is fully independent of these changes.

@ayoy ayoy assigned tomasstrba and unassigned ayoy Apr 22, 2022
@tomasstrba
Copy link
Contributor

Awesome! 👏 Will take a look at it soon. (Probably won't be part of the release today, but that is ok)

Copy link
Contributor

@tomasstrba tomasstrba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Nice work! 💯 I only have found last blocker that I would like to discuss. We currently trigger lazy loading for each window. Does it make sense to limit it to the main window?

Hypothetically, someone with 10 windows restored could have really slow start. WDYT?

import Combine
import os

final class TabLazyLoader<DataSource: TabLazyLoaderDataSource> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 🙏 More clear to me now :)

@tomasstrba tomasstrba assigned ayoy and unassigned tomasstrba Apr 24, 2022
@ayoy
Copy link
Collaborator Author

ayoy commented Apr 25, 2022

LGTM! Nice work! 💯 I only have found last blocker that I would like to discuss. We currently trigger lazy loading for each window. Does it make sense to limit it to the main window?

Hypothetically, someone with 10 windows restored could have really slow start. WDYT?

That's a very fair point Tom! I adjusted the code so that lazy loading is requested by WindowsManager only for the new key window while restoring state. Also added a guard that prevents scheduling lazy loading more than once for a TabCollectionViewModel instance.

Going forward, lazy loading can be expanded to cover multiple windows as a separate project, someday.

@ayoy ayoy assigned tomasstrba and unassigned ayoy Apr 25, 2022
@ayoy ayoy requested a review from tomasstrba April 25, 2022 07:57
@tomasstrba
Copy link
Contributor

Jumping on this for a final review

Copy link
Contributor

@tomasstrba tomasstrba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👍

@mallexxx had a pretty good point in Asana, that if there is no connection after the start, tabs show "Oooops" title. I think it is fine to move on with this PR but do you mind scoping a follow-up task solving this issue?

@tomasstrba tomasstrba assigned ayoy and unassigned tomasstrba Apr 26, 2022
@ayoy
Copy link
Collaborator Author

ayoy commented Apr 26, 2022

Thank you very much Tom! Yes, I will add a follow-up task according to the comment from @mallexxx.

@ayoy ayoy merged commit a750777 into develop Apr 26, 2022
@ayoy ayoy deleted the dominik/lazy-load-tabs branch April 26, 2022 15:15
samsymons added a commit that referenced this pull request Apr 27, 2022
# By Alexey Martemyanov (3) and others
# Via GitHub
* develop:
  Lazy load background tabs at app startup (#553)
  Update the Fireproof checkmark in the Save Credentials view controller (#555)
  Support config v2 (#528)
  Fullscreen video fixing (#541)
  Add data import failure pixels (#552)
  Update BSK to fix autofill on Catalina (#551)
  fix contrast bug on Catalina / Big Sur (#546)
  Disable download reload on page tab reactivation/session restoration (#516)
  Add "New Window" item to App Dock menu (#544)

# Conflicts:
#	DuckDuckGo.xcodeproj/project.pbxproj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants