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

Waiting for ContentBlockingRules to be applied before navigation #402

Merged
merged 40 commits into from
Feb 22, 2022

Conversation

mallexxx
Copy link
Collaborator

@mallexxx mallexxx commented Jan 21, 2022

Task/Issue URL: https://app.asana.com/0/72649045549333/1201190116754047
Tech Design URL: https://app.asana.com/0/inbox/1199237043628108/1201676989048031/1201672261001531
CC: @tomasstrba

Description:

  • Converts ContentBlockingRules + UserScripts into a single dependency (ContentBlockingAssets) which is injected into UserContentController
  • Navigation is paused until ContentBlockingAssets become available

see duckduckgo/BrowserServicesKit#54

Steps to test this PR:
0. Remove ~/Library/WebKit/com.duckduckgo.macos.browser[.debug]/ContentRuleLists/*

  1. Open a browser with a restored nytimes.com Tab
  2. Validate there's no ads on the restored Tab
  3. Open privacy dashboard, turn protection off - validate protection is off for the reloaded webpage
  4. Open privacy dashboard, turn protection on - validate it turns on instantly and works on reloaded webpage

Testing checklist:

  • Test with Release configuration

Internal references:

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

@mallexxx mallexxx changed the title ContentBlockingRules sync loading ContentBlockingRules sync loading (WIP) Jan 24, 2022
@mallexxx mallexxx force-pushed the alex/content-blocking-rules branch from 5f75f05 to 266ef22 Compare January 26, 2022 07:21

if navigationAction.isTargetingMainFrame && !url.isDuckDuckGo {
RulesCompilationMonitor.shared.tabWillWaitForRulesCompilation(self)
let waitTime = await webView.configuration.userContentController.awaitContentBlockingRulesInstalled()
Copy link
Contributor

@tomasstrba tomasstrba Jan 26, 2022

Choose a reason for hiding this comment

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

Maybe it is me but in this particular case await is making this section pretty hard to read.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tomasstrba you mean the whole method or this particular place?
Do you think getting read of isTargetingMainFrame, isDuckDuckGo check and moving waitTime to inout args will make it better?
like this:

RulesCompilationMonitor.shared.tabWillWaitForRulesCompilation(self)
var waitTime: TimeInterval?
await webView.configuration.userContentController.awaitContentBlockingRulesInstalled(waitTime: &waitTime)
RulesCompilationMonitor.shared.tab(self, didFinishWaitingForRulesWithWaitTime: waitTime)

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.

Alex, sorry for jumping in. I will leave the main review to Brindy or Bartek, I just looked to also understand the change. Here are few comments I did while looking at this

}

if navigationAction.isTargetingMainFrame && !url.isDuckDuckGo {
RulesCompilationMonitor.shared.tabWillWaitForRulesCompilation(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

In case rules are already compiled and ready, suspending until awaitContentBlockingAssetsInstalled is finished makes the loading unnecessary slower.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, can we inject RulesCompilationMonitor.shared? It makes it easier to create unit tests of Tab class + it is less spaghetti.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tomasstrba, are you sure it's waiting when rules are already compiled? I'll check it but the intent here is it should complete instantly when the rules are in place
Good not about RulesCompilationMonitor.shared

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I understand the intent, but to me it looks like there is a cost of the async/await even if it completes immediately. Since this is the critical section of the loading, I would refactor to speed this as much as possible

@@ -27,4 +27,13 @@ extension Publisher where Failure == Never {
}
}

/// Buffers latest published value and replays it on subscription
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the @published property doing the same? This seems a little over-engineered

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Basically yes, it's mostly the same except for optionality: @published requires property to be present from the start, or making it Optional, ShareReplay is a concept from rxSwift missing in Combine for some reason, it basically waits for a published value and then buffers it for a next subscriptor
Why do you think it lacks clarity? For me it's more complex to have a @published value populated from elsewhere to be connected from elsewhere, shareReplay in contrary allows to have this processing be grouped in one place

}
}

private let gpcEnabledUpdatesSubject = PassthroughSubject<Bool, Never>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, is there any specific reason why @Published isn't suitable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't found a good way to combine it with UserDefaultsWrapper, but would be better of course..

@@ -58,7 +58,9 @@ final class Pixel {
guard !dryRun else {
os_log(.debug, log: .pixel, "%@", pixelName.replacingOccurrences(of: "_", with: "."))

onComplete(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, what is the reason for this delay?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To simulate a server request, it's a "dryrun" path


import Foundation

final class RulesCompilationMonitor: NSObject {
Copy link
Contributor

Choose a reason for hiding this comment

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

Names of methods of this class are describing events triggering them. Looking at this class without looking elsewhere makes it pretty hard to understand. Do you think methods should describe their purpose?

Copy link
Contributor

Choose a reason for hiding this comment

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

Making unit tests to cover this class should help to:

  1. Verify the functionality
  2. Improve the naming

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

reportWaitTimeForTab(_, waitedForRulesFor: timeInterval) - should be good instead of tab(didFinish).. other methods seem find
I'll add some tests and comments for the class; Considering also renaming it to ContentBlockingRulesCompilationTimeReporter

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks 👍 :)

@@ -48,16 +48,21 @@ final class TabCollection: NSObject {
}

saveLastRemovedTab(at: index)
tabs[index].tabWillClose()
Copy link
Contributor

Choose a reason for hiding this comment

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

Just thinking. Is there a better way to do this so we don't forget to call tabWillClose() after adding more remove methods? What is on my mind is subclassing collection (array) and overriding remove.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TabCollection is basically the collection subclass :)
Maybe adding an "assert" to Tab.dealloc if "tabWillClose" (we may set tab content to .none to indicate this) was called would be enough?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah why not, anything that would help us not to miss tabWillClose() in the future

@mallexxx mallexxx requested a review from tomasstrba February 1, 2022 06:35
@mallexxx mallexxx changed the title ContentBlockingRules sync loading (WIP) Waiting for ContentBlockingRules to be applied before navigation Feb 2, 2022
Copy link
Collaborator

@bwaresiak bwaresiak left a comment

Choose a reason for hiding this comment

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

Great job so far - I've been testing rule compilation/recompilation and cache gives awesome boost of performance for already precompiled lists.

There are few bugs I've noticed (find in page, no trackers reported) but I supposed that these are because of the UserScripts reuse issue (mentioned in the comment) - once that is resolved I'll give this another test.

// report only once
isFinished = true

Pixel.fire(.compileRulesWait(onboardingShown: self.onboardingShown, waitTime: waitTime, result: result), onComplete: completionHandler)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have exact time as a parameter?

contentBlockingRulesSubject.eraseToAnyPublisher()
init(rulesUpdate: ContentBlockerRulesManager.UpdateEvent) {
self.rulesUpdate = rulesUpdate
self.userScripts = UserScripts(with: DefaultScriptSourceProvider())
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't seem like a correct approach: here we create set of user scripts and sent these to all tabs - so whatever tab is created last will become the sole delegate of the scripts. Instead scripts should be created for every Tab separately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh, how could I miss it 🤦‍♂️

Copy link
Collaborator

Choose a reason for hiding this comment

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

No worries - I've read that code like 5 times and didn't notice the problem until I started the testing 🙃

@mallexxx
Copy link
Collaborator Author

should be good to go now

Copy link
Collaborator

@bwaresiak bwaresiak left a comment

Choose a reason for hiding this comment

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

Great, scripts seems to work well now, I've left one comment related to performance inline.

One thing that bothers me is that I randomly get assertion from UserContentController.installUserScripts:

        guard let delegate = delegate else {
            assertionFailure("UserContentController delegate not set")
            return
        }

I tried to figure out a way to reproduce that but it seems random - I just leave browser working in background for a while, and sometimes when I come back and trigger protection on/off it causes assertion to fire.

DuckDuckGo/ContentBlocker/ContentBlocking.swift Outdated Show resolved Hide resolved
@mallexxx mallexxx assigned mallexxx and unassigned bwaresiak Feb 11, 2022
@mallexxx
Copy link
Collaborator Author

@bwaresiak I can swear I've caught this assertion once thinking I understand where it comes from, but no luck to reproduce it since then..
So, I added some #if DEBUG leak protection and validation that the delegate is set after init instead this assert, hope it works now

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self,
weak cc=webView.configuration.userContentController] in
assert(self == nil)
assert(cc == nil)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't that fire randomly in some cases e.g when we call faviconManagement.handleFaviconLinks and have to wait for response, but tab is closed? We probably have more async blocks that retain the Tab instance temporarily that could cause this to misfire - but prolly we should change all these blocks to use weak self anyway.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll just remove this to let it pass now and make another TechDebt task for this to think about

Copy link
Collaborator

@bwaresiak bwaresiak left a comment

Choose a reason for hiding this comment

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

Great job @mallexxx!

@mallexxx mallexxx merged commit 7509c21 into develop Feb 22, 2022
@mallexxx mallexxx deleted the alex/content-blocking-rules branch February 22, 2022 14:11
samsymons added a commit that referenced this pull request Feb 22, 2022
* develop:
  Waiting for ContentBlockingRules to be applied before navigation (#402)
  Cookie prompt management (#312)
  Pass config data to Autofill UserScript (#418)
samsymons added a commit that referenced this pull request Feb 22, 2022
* develop:
  Waiting for ContentBlockingRules to be applied before navigation (#402)
  Cookie prompt management (#312)
  Pass config data to Autofill UserScript (#418)
  Fix video fullscreen exit crash (#295)
samsymons added a commit that referenced this pull request Feb 23, 2022
…ntication

# By Alexey Martemyanov (1) and others
* sam/logins-filtering-and-sorting:
  Disable the search field when editing.
  Waiting for ContentBlockingRules to be applied before navigation (#402)
  Cookie prompt management (#312)
  Pass config data to Autofill UserScript (#418)

# Conflicts:
#	DuckDuckGo.xcodeproj/project.pbxproj
#	DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
#	DuckDuckGo/Preferences/Model/PreferenceSections.swift
#	DuckDuckGo/Preferences/View/PrivacySecurityPreferencesTableCellView.xib
jonathanKingston pushed a commit that referenced this pull request Feb 25, 2022
* rm WebViewConfiguration typealias

* async decidePolicyForNavigationAction + wait for content rules

* Rules compilation monitor and reporting

* Inject UserScripts+ContentBlockingRules as a single ContentBlockingAssets

* PR review fixes

* fix tests

* fix proj

* ContentBlockingUpdatingTests and fixes

* cleanup

* cleanup

* fixing tests

* fixing tests

* Report no wait time on rules loading; Report contentBlockingCompilationTime

* fix tests

* still fixing tests

* and more

* linter, please

* please..

* the tests

* c‘mon!

* get rid of it

* what you say about no runloop waiting?

* figuring out what's wrong

* at last

* bump BSK

* UserScripts generated per-tab

* fix unexpected Pixel during tests

* compileRulesWait Pixel +waitTime argument

* fixing PR issues

* leaks prevention

* fix tests

* cleanup

* set BSK to v.10.0.0

* final adjustment
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.

3 participants