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:
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