tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e2c6ac5365305177a042b1962b07ac03f906bc30
parent 3709f413659b907bc94c8ed623015d2a245ed61a
Author: Ted Campbell <tcampbell@mozilla.com>
Date:   Wed,  7 Jan 2026 06:25:57 +0000

Bug 2005839 - Part 3: Refactor FenixApplication inititalization r=mstange,android-reviewers,jonalmeida

This patch adds a lot of documentation about the details and subtlety around
startup. As part of this I adjust the structure of helper functions. The
behaviour and ordering is the same as before.

Differential Revision: https://phabricator.services.mozilla.com/D276813

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt | 229+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
1 file changed, 130 insertions(+), 99 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -130,15 +130,21 @@ private const val BYTES_TO_MEGABYTES_CONVERSION = 1024.0 * 1024.0 @Suppress("Registered", "TooManyFunctions", "LargeClass") open class FenixApplication : LocaleAwareApplication(), Provider { init { - recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical. + // [TIMER] Record startup timestamp as early as reasonable with some degree of consistency. + // + // Measured after: + // - Static class initializers + // - Kotlin companion-object-init blocks + // + // but before: + // - ContentProvider initialization + // - Application.onCreate + // + StartupTimeline.onApplicationInit() } private val logger = Logger("FenixApplication") - internal val isDeviceRamAboveThreshold by lazy { - isDeviceRamAboveThreshold() - } - open val components by lazy { Components(this) } var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null @@ -154,119 +160,150 @@ open class FenixApplication : LocaleAwareApplication(), Provider { * such as Nimbus, Glean and Gecko. Note that Robolectric tests override this with an empty * implementation that skips this initialization. */ + @SuppressLint("NewApi") protected open fun initializeFenixProcess() { - // We measure ourselves to avoid a call into Glean before its loaded. + // [TIMER] Record the start of the [PerfStartup.applicationOnCreate] metric here. Do this + // manually because Glean has not started initializing yet. Note that by this point the + // content providers from Fenix and its libraries have run their initializers already. val start = SystemClock.elapsedRealtimeNanos() - setupInAllProcesses() - - // If the main process crashes before we've reached visual completeness, we consider it to - // be a startup crash and fork into the recovery flow. The activity that is responsible for - // that flow is hosted in a separate process, which means that we avoid the majority of - // initialization work that is done in `setupInMainProcess` - // Please see the README.md in the fenix/startupCrash package for more information. - if (!isMainProcess()) { - // If this is not the main process then do not continue with the initialization here. Everything that - // follows only needs to be done in our app's main process and should not be done in other processes like - // a GeckoView child process or the crash handling process. Most importantly we never want to end up in a - // situation where we create a GeckoRuntime from the Gecko child process. - return + // See Bug 1969818: Crash reporting requires updates to be compatible with + // isolated content process. + if (!android.os.Process.isIsolated()) { + setupCrashReporting() } - // DO NOT ADD ANYTHING ABOVE HERE. - // Note: That the startup crash recovery flow is hosted in a different process, - // so this call will be avoided in that case - setupInMainProcessOnly() - // DO NOT ADD ANYTHING UNDER HERE. - - // DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL. - val stop = SystemClock.elapsedRealtimeNanos() - val durationMillis = TimeUnit.NANOSECONDS.toMillis(stop - start) + // Capture A-C logs to Android logcat. Note that gecko maybe directly post to logcat + // regardless of what we do here. + Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug, AndroidLogSink())) - // We avoid blocking the main thread on startup by calling into Glean on the background thread. - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(IO) { + // While this [initializeFenixProcess] method is run for _all processes_ in the app, we only + // do global initialization for the _main process_ here. This main process initialization + // includes setting up native libraries and global 'components' instances. + // + // Note: If a crash happens during this initialization (before visual completeness), then + // the [StartupCrashActivity] may be launched in a separate process. See the README.md + // in fenix/startupCrash for more information. + // + // Note: Gecko service processes don't use Nimbus or the Kotlin components. They also do + // their own loading of Gecko libraries. + // + // Note: The A-C / Fenix crash service processes are responsible for their own setup and + // should minimize their dependencies to avoid also crashing. + runOnlyInMainProcess { + // Initialization is split into two phases based on if libmegazord is fully initialized. + setupEarlyMain() + setupPostMegazord() + + // [TIMER] Record the end of the `PerfStartup.applicationOnCreate` metric. Note that + // glean will queue this if the backend is still starting up. + val stop = SystemClock.elapsedRealtimeNanos() + val durationMillis = TimeUnit.NANOSECONDS.toMillis(stop - start) PerfStartup.applicationOnCreate.accumulateSamples(listOf(durationMillis)) } } + // Begin initialization of Glean if we have data-upload consent, otherwise we will have to + // wait until we do. Note that Glean initialization is asynchronous any may not be finished + // when this method returns. @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage - private fun initializeGlean() { - val settings = settings() - // We delay the Glean initialization until, we have user consent (After onboarding). + private fun maybeInitializeGlean() { + // We delay the Glean initialization until we have user consent from onboarding. // If onboarding is disabled (when in local builds), continue to initialize Glean. if (components.fenixOnboarding.userHasBeenOnboarded() || !FeatureFlags.onboardingFeatureEnabled) { - initializeGlean(this, logger, settings.isTelemetryEnabled, components.core.client) - } - - // We avoid blocking the main thread on startup by setting startup metrics on the background thread. - val store = components.core.store - GlobalScope.launch(IO) { - setStartupMetrics(store, settings) + initializeGlean(this, logger, settings().isTelemetryEnabled, components.core.client) } } - @SuppressLint("NewApi") - private fun setupInAllProcesses() { - // See Bug 1969818: Crash reporting requires updates to be compatible with - // isolated content process. - if (!android.os.Process.isIsolated()) { - setupCrashReporting() - } - // We want the log messages of all builds to go to Android logcat - Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug, AndroidLogSink())) - } - - private fun setupInMainProcessOnly() { - // ⚠️ DO NOT ADD ANYTHING ABOVE THIS LINE. - // Especially references to the engine/BrowserStore which can alter the app initialization. - // See: https://github.com/mozilla-mobile/fenix/issues/26320 + /** + * This phase of main-process initialization runs before application-services is fully setup + * so care must be taken. This phases begins loading the Nimbus, Glean, Gecko libraries. + * + * By the end of this, application-services, Nimbus and Gecko are initialized. Glean may or may + * not be initialized. + */ + private fun setupEarlyMain() { + // ⚠️ The sequence of CrashReporter / Nimbus / Engine / Glean is particularly subtle due to + // interdependencies among them. + // + // - We want the CrashReporter as soon as reasonable to give the best visibility. Note that + // CrashReporter records Nimbus experiment list when a crash happens so it has a lazy + // dependency on Nimbus. // - // We can initialize Nimbus before Glean because Glean will queue messages - // before it's initialized. + // - Nimbus should be initialized quite early to ensure consistent experiment values are + // applied. In particular, we want to do it before Engine so that we have the right values + // before pages load. See: https://github.com/mozilla-mobile/fenix/issues/26320 + // + // - Glean will queue (most) messages before being started so it is safe for Nimbus to begin + // before Glean does and any metrics will be processed once Glean is ready. + + // Begin application-services initialization. The megazord contains Nimbus, but not Glean. + setupMegazordInitial() + + // Initialize Nimbus and its backend. initializeNimbus() ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register() - run { - // Make sure the engine is initialized and ready to use. - components.core.engine.warmUp() + // Ensure the Engine instance is initialized such that it can receive commands. Note + // that full initialization is typically running off-thread and it may be a while + // before pages can begin to render. + components.core.engine.warmUp() - initializeGlean() + // Kick off initialization of Glean backend off-thread. Glean will continue to queue + // metric samples until the backend is ready. If we don't have data-upload consent then + // this will be a no-op and initialization may be attempted after onboarding. + maybeInitializeGlean() - // Attention: Do not invoke any code from a-s in this scope. - val megazordSetup = finishSetupMegazord() + // Initialize the [BrowserStore] so that [setStartupMetrics] can reference this. + // Note: This is a historical artifact and should be revisited. + val store = components.core.store - setDayNightTheme() - components.strictMode.enableStrictMode(true) - warmBrowsersCache() + // StartupMetrics accesses shared preferences so do this off thread. + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(IO) { + setStartupMetrics(store, settings()) + } - initializeWebExtensionSupport() + // Start setup for concept-fetch networking in megazord. This runs off-thread, but we wait + // before for its completion synchronously. + val megazordDeferred = setupMegazordNetwork() - // Make sure to call this function before registering a storage worker - // (e.g. components.core.historyStorage.registerStorageMaintenanceWorker()) - // as the storage maintenance worker needs a places storage globally when - // it is needed while the app is not running and WorkManager wakes up the app - // for the periodic task. - GlobalPlacesDependencyProvider.initialize(components.core.historyStorage) + setDayNightTheme() + components.strictMode.enableStrictMode(true) + warmBrowsersCache() - GlobalSyncedTabsCommandsProvider.initialize(lazy { components.backgroundServices.syncedTabsCommands }) + initializeWebExtensionSupport() - initializeRemoteSettingsSupport() + // Make sure to call this function before registering a storage worker + // (e.g. components.core.historyStorage.registerStorageMaintenanceWorker()) + // as the storage maintenance worker needs a places storage globally when + // it is needed while the app is not running and WorkManager wakes up the app + // for the periodic task. + GlobalPlacesDependencyProvider.initialize(components.core.historyStorage) - restoreBrowserState() - restoreDownloads() - restoreMessaging() + GlobalSyncedTabsCommandsProvider.initialize(lazy { components.backgroundServices.syncedTabsCommands }) - // Just to make sure it is impossible for any application-services pieces - // to invoke parts of itself that require complete megazord initialization - // before that process completes, we wait here, if necessary. - if (!megazordSetup.isCompleted) { - runBlockingIncrement { megazordSetup.await() } - } - } + initializeRemoteSettingsSupport() + restoreBrowserState() + restoreDownloads() + restoreMessaging() + + // [IMPORTANT] Don't progress further until application-services is actually ready to go. + // This makes it easier to reason about behaviour and avoids issues in the Rust code. + runBlockingIncrement { megazordDeferred.await() } + } + + /** + * The remainder of main-process initialization happens here now that we have ensured the + * application-services initialization is completed. This also queues a bunch of follow-up + * work to the visualCompletenessQueue that will be run after the Activity has started + * rendering. + */ + private fun setupPostMegazord() { setupLeakCanary() + if (components.fenixOnboarding.userHasBeenOnboarded()) { startMetricsIfEnabled( logger = logger, @@ -279,6 +316,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } else { components.distributionIdManager.startAdjustIfSkippingConsentScreen() } + setupPush() GlobalFxSuggestDependencyProvider.initialize(components.fxSuggest.storage) @@ -290,6 +328,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.appStartReasonProvider.registerInAppOnCreate(this) components.startupActivityLog.registerInAppOnCreate(this) components.appLinkIntentLaunchTypeProvider.registerInAppOnCreate(this) + initVisualCompletenessQueueAndQueueTasks() ProcessLifecycleOwner.get().lifecycle.addObservers( @@ -529,8 +568,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } private fun initializeNimbus() { - beginSetupMegazord() - // This lazily constructs the Nimbus object… val nimbus = components.nimbus.sdk // … which we then can populate the feature configuration. @@ -551,7 +588,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { * engine for networking. This should do the minimum work necessary as it is done on the main * thread, early in the app startup sequence. */ - private fun beginSetupMegazord() { + private fun setupMegazordInitial() { // Rust components must be initialized at the very beginning, before any other Rust call, ... AppServicesInitializer.init( AppServicesConfig(components.analytics.crashReporter), @@ -559,7 +596,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage - private fun finishSetupMegazord(): Deferred<Unit> { + private fun setupMegazordNetwork(): Deferred<Unit> { return GlobalScope.async(IO) { if (Config.channel.isDebug) { RustHttpConfig.allowEmulatorLoopback() @@ -900,7 +937,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider { private fun Long.toRoundedMegabytes(): Long = (this / BYTES_TO_MEGABYTES_CONVERSION).roundToLong() - private fun isDeviceRamAboveThreshold() = deviceRamApproxMegabytes() > RAM_THRESHOLD_MEGABYTES + internal val isDeviceRamAboveThreshold by lazy { + deviceRamApproxMegabytes() > RAM_THRESHOLD_MEGABYTES + } @Suppress("CyclomaticComplexMethod") private fun setPreferenceMetrics( @@ -1041,14 +1080,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider { CustomizeHome.contile.set(settings.showContileFeature) } - private fun recordOnInit() { - // This gets called by more than one process. Ideally we'd only run this in the main process - // but the code to check which process we're in crashes because the Context isn't valid yet. - // - // This method is not covered by our internal crash reporting: be very careful when modifying it. - StartupTimeline.onApplicationInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing is critical. - } - override fun onConfigurationChanged(config: android.content.res.Configuration) { // Workaround for androidx appcompat issue where follow system day/night mode config changes // are not triggered when also using createConfigurationContext like we do in LocaleManager