Skip to content

Conversation

@CrisBarreiro
Copy link
Contributor

@CrisBarreiro CrisBarreiro commented Nov 10, 2025

Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211897095283266?focus=true

Description

Steps to test this PR

Update override fun isEnabled(): Boolean in FeatureToggles adding this snippet at the beginning of the method

        val threadName = Thread.currentThread().name
        if (threadName == "main" || threadName == "Main") {
            val stackTrace = Thread.currentThread().stackTrace
                .drop(1) // Skip getStackTrace itself
                .take(10) // Limit to first 10 frames to avoid too much output
                .joinToString("\n  ") { "at ${it.className}.${it.methodName}(${it.fileName}:${it.lineNumber})" }
            System.err.println("FeatureToggles: isEnabled() called on main thread for feature: ${featureName()}\nCall stack:\n  $stackTrace")
        }

Feature 1

  • Smoke test app

Feature 2

  • Fresh install app
  • Wait for a WebView to open
  • Filter logcat for level:warn and check there's no occurrences for FeatureToggles: isEnabled() called on main thread containing BrowserTabFragment or BrowserTabViewModel
  • Load a page
  • Kill the app
  • Open the app again and wait for the tab to restore
  • Filter logcat for level:warn and check there's no occurrences for FeatureToggles: isEnabled() called on main thread containing BrowserTabFragment or BrowserTabViewModel init flow. You'll still see
    • clientBrandHint in RealClientBrandHintProvider.calculateBrandingChange. This is also called from shouldOverrideUrlLoading, and therefore it's not easy to migrate
    • aiChatQueryDetectionFeature in SpecialUrlDetectorImpl.determineType. This is also called from shouldOverrideUrlLoading, and therefore it's not easy to migrate
    • siteErrorHandlerKillSwitch in BrowserTabViewModel.recordErrorCode. Not part of critical path

UI changes

n/a

Copy link
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@CrisBarreiro CrisBarreiro marked this pull request as ready for review November 10, 2025 15:54
configureNavigationBar()
configureOmnibar()

configureObservers()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to call configureObservers after configuring the views as, otherwise, the app would crash because webView, popupMenu, omnibar, etc. aren't initialized yet. It used to work because we were blocking the main thread, and therefore no values were emitted before they could be processed

@CrisBarreiro CrisBarreiro force-pushed the feature/cris/on-activity-created-non-blocking branch from fe9135f to a69da00 Compare November 12, 2025 09:29
@CrisBarreiro CrisBarreiro force-pushed the feature/cris/on-activity-created-non-blocking branch from 96dcdd2 to 76e8e7c Compare November 12, 2025 10:00
@CDRussell CDRussell requested a review from Copilot November 12, 2025 11:57
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR removes main thread checks from BrowserTabFragment and BrowserTabViewModel by moving Remote Config (feature toggle) checks off the main thread using coroutines and dispatchers.

  • Wraps Remote Config isEnabled() calls in withContext(dispatchers.io()) to execute them on background threads
  • Updates CustomHeadersProvider.getCustomHeaders() to be a suspend function that runs on IO dispatcher
  • Refactors initialization code in BrowserTabFragment.onActivityCreated() to run within a coroutine scope

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
CustomHeadersProvider.kt Changed getCustomHeaders() to suspend function with IO dispatcher execution
OmnibarLayoutViewModel.kt Moved feature toggle check for serpEasterEggLogosToggles to IO thread within coroutine
OmnibarLayout.kt Wrapped feature toggle check in coroutine with IO dispatcher
LegacyOmnibarLayout.kt Wrapped feature toggle check in coroutine with IO dispatcher
BrowserTabViewModel.kt Added IO dispatcher wrapping for multiple feature toggle checks and updated calls to getUrlHeaders()
BrowserTabFragment.kt Wrapped initialization logic in coroutine and updated multiple methods to suspend functions
BrowserTabViewModelTest.kt Updated test fake to match new suspend function signature

Comment on lines 1044 to 1045
lifecycleScope.launch {
omnibar = Omnibar(
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Wrapping the entire onActivityCreated initialization in lifecycleScope.launch introduces a race condition. The function returns immediately before initialization completes, but observers and other components may expect the view to be fully configured. Consider using runBlocking for critical initialization or restructuring to ensure proper sequencing.

Copilot uses AI. Check for mistakes.
configureNavigationBar()
configureOmnibar()

configureObservers()
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Moving configureObservers() after the omnibar and webview configuration changes the initialization order. Observers should be configured before other setup to ensure they can respond to state changes during initialization. This was previously called earlier in the sequence.

Copilot uses AI. Check for mistakes.
Comment on lines +640 to +641
viewModelScope.launch {
if (withContext(dispatcherProvider.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) {
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Launching a new coroutine within onOmnibarViewStateUpdated() breaks synchronous UI update expectations. The calling code may expect state updates to complete immediately, but now they happen asynchronously, potentially causing UI inconsistencies or timing issues.

Copilot uses AI. Check for mistakes.
Comment on lines +1142 to +1144
viewModelScope.launch {
command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate))
}
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Wrapping navigation commands in new coroutines introduces timing issues. The navigation command may execute after subsequent code that expects it to have already fired, potentially breaking navigation flow or causing race conditions with other navigation logic.

Copilot uses AI. Check for mistakes.

it.settings.apply {
clientBrandHintProvider.setDefault(this)
withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) }
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Calling clientBrandHintProvider.setDefault() on IO dispatcher while configuring WebView settings is problematic. WebView configuration must happen on the main thread, and switching contexts mid-configuration may cause threading violations or incomplete setup.

Suggested change
withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) }
clientBrandHintProvider.setDefault(this@apply)

Copilot uses AI. Check for mistakes.
Copy link
Member

@CDRussell CDRussell left a comment

Choose a reason for hiding this comment

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

Still reviewing, but releasing pending comments along with some things you should consider before we merge this (if you haven't already), since it's a riskier change with no ability to FF:

  1. ensure you've tested on both play and internal variants
  2. run as many of the Maestro UI tests against this change as you can

Comment on lines +590 to +592
if (siteErrorHandlerKillSwitch.self().isEnabled()) {
siteErrorHandler.assignErrorsAndClearCache(value)
siteHttpErrorHandler.assignErrorsAndClearCache(value)
Copy link
Member

Choose a reason for hiding this comment

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

is it just the FF check that needs to be done on io / does calling the other methods on io change existing behaviour?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The FF check needs to happen in io, and I thought it would be nice to have the other ones execute on io as well. It's not a super complex logic, so it might not hurt to have it in main, but I think io is better

    override fun assignErrorsAndClearCache(site: Site?) {
        if (site != null) {
            cache[site.url]?.forEach {
                assignError(site, it)
            }
        }
        cache.clear()
    }
    ```


it.settings.apply {
clientBrandHintProvider.setDefault(this)
withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) }
Copy link
Member

@CDRussell CDRussell Nov 12, 2025

Choose a reason for hiding this comment

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

is it safe to do all of clientBrandHintProvider.setDefault on the io thread now, whereas it was all main before?

  • if you're fully confident it's a safe change, what you have is fine.
  • but if you have any doubt or want to de-risk further, can consider: should it be suspend instead, and inside you could just do the FF check on io leaving all the other logic to run as is?

webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript)

private fun configureWebViewForAutofill(it: DuckDuckGoWebView) {
private suspend fun configureWebViewForAutofill(it: DuckDuckGoWebView) {
Copy link
Member

Choose a reason for hiding this comment

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

is adding suspend necessary, nothing else seems to use it?

)
webViewContainer = binding.webViewContainer
viewModel.registerWebViewListener(webViewClient, webChromeClient)
configureWebView()
Copy link
Member

Choose a reason for hiding this comment

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

nit: in here it calls swipingTabsFeature.isEnabled which might sometimes check the FF (first time). it's possible it's already cached at this point (BrowserActivity maybe) but if configureWebView is suspend you could also add a withContext(dispatchers.io()) around swipingTabsFeature.isEnabled check; might help to prevent it appearing as a subtle issue later if the thing that is causing it to already be cached at this point is changed.

@CDRussell
Copy link
Member

@CrisBarreiro , note I see a few other cases where FFs are checked on main thread

Screenshot 2025-11-12 at 12 21 40

@CrisBarreiro
Copy link
Contributor Author

@CrisBarreiro , note I see a few other cases where FFs are checked on main thread

Screenshot 2025-11-12 at 12 21 40

Discussed on MM. These are not necessarily happening in BrowserTabFragment or BrowserTabViewModel, and the intent of this PR is not to eliminate all RC checks in main, just address the ones in these classes for now

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