From dfb3c4c9bf8f8e853430faee93f86d23a0b2abab Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Thu, 1 Apr 2021 14:08:11 +0200 Subject: [PATCH] Introduce process lifecycle observer to collect metrics about tabs when app goes to foreground/background. --- app/metrics.yaml | 40 ++++++ .../org/mozilla/fenix/FenixApplication.kt | 4 + .../telemetry/TelemetryLifecycleObserver.kt | 76 +++++++++++ .../TelemetryLifecycleObserverTest.kt | 124 ++++++++++++++++++ .../telemetry/TelemetryMiddlewareTest.kt | 2 +- docs/metrics.md | 1 + 6 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt create mode 100644 app/src/test/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserverTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 93bf0d437..58b387e5d 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -4965,6 +4965,46 @@ engine_tab: - fenix-core@mozilla.com - skaspari@mozilla.com expires: "2021-12-31" + foreground_metrics: + type: event + description: | + Event collecting data about the state of tabs when the app comes back to + the foreground. + bugs: + - https://github.com/mozilla-mobile/android-components/issues/9997 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + - skaspari@mozilla.com + expires: "2021-12-31" + extra_keys: + background_active_tabs: + description: | + Number of active tabs (with an engine session assigned) when the app + went to the background. + background_crashed_tabs: + description: | + Number of tabs marked as crashed when the app went to the background. + background_total_tabs: + description: | + Number of total tabs when the app went to the background. + foreground_active_tabs: + description: | + Number of active tabs (with an engine session assigned) when the + app came back to the foreground. + foreground_crashed_tabs: + description: | + Number of tabs marked as crashed when the app came back to the + foreground. + foreground_total_tabs: + description: | + Number of total tabs when the app came back to the foreground. + time_in_background: + description: | + Time (in milliseconds) the app was in the background. synced_tabs: synced_tabs_suggestion_clicked: diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 22657333f..a4e802678 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -13,6 +13,7 @@ import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.getSystemService +import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration.Builder import androidx.work.Configuration.Provider import kotlinx.coroutines.Deferred @@ -59,6 +60,7 @@ import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.WebPushEngineIntegration import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.VisibilityLifecycleCallback +import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver import org.mozilla.fenix.utils.BrowsersCache import java.util.concurrent.TimeUnit @@ -194,6 +196,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.startupActivityStateProvider.registerInAppOnCreate(this) initVisualCompletenessQueueAndQueueTasks() + ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store)) + components.appStartupTelemetry.onFenixApplicationOnCreate() } } diff --git a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt new file mode 100644 index 000000000..ec86ae2f4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.telemetry + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.base.android.Clock +import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics +import org.mozilla.fenix.GleanMetrics.EngineTab.foregroundMetricsKeys as MetricsKeys + +/** + * [LifecycleObserver] to used on the process lifecycle to measure the amount of tabs getting killed + * while the app is in the background. + * + * See: + * - https://github.com/mozilla-mobile/android-components/issues/9624 + * - https://github.com/mozilla-mobile/android-components/issues/9997 + */ +class TelemetryLifecycleObserver( + private val store: BrowserStore +) : LifecycleObserver { + private var pausedState: TabState? = null + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() { + pausedState = createTabState() + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() { + val lastState = pausedState ?: return + val currentState = createTabState() + + EngineMetrics.foregroundMetrics.record(mapOf( + MetricsKeys.backgroundActiveTabs to lastState.activeEngineTabs.toString(), + MetricsKeys.backgroundCrashedTabs to lastState.crashedTabs.toString(), + MetricsKeys.backgroundTotalTabs to lastState.totalTabs.toString(), + MetricsKeys.foregroundActiveTabs to currentState.activeEngineTabs.toString(), + MetricsKeys.foregroundCrashedTabs to currentState.crashedTabs.toString(), + MetricsKeys.foregroundTotalTabs to currentState.totalTabs.toString(), + MetricsKeys.timeInBackground to (currentState.timestamp - lastState.timestamp).toString() + )) + + pausedState = null + } + + private fun createTabState(): TabState { + val tabsWithEngineSession = store.state.tabs + .filter { tab -> tab.engineState.engineSession != null } + .filter { tab -> !tab.engineState.crashed } + .count() + + val totalTabs = store.state.tabs.count() + + val crashedTabs = store.state.tabs + .filter { tab -> tab.engineState.crashed } + .count() + + return TabState( + activeEngineTabs = tabsWithEngineSession, + totalTabs = totalTabs, + crashedTabs = crashedTabs + ) + } +} + +private data class TabState( + val timestamp: Long = Clock.elapsedRealtime(), + val totalTabs: Int, + val crashedTabs: Int, + val activeEngineTabs: Int +) diff --git a/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserverTest.kt b/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserverTest.kt new file mode 100644 index 000000000..cf054b9af --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserverTest.kt @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.telemetry + +import androidx.test.core.app.ApplicationProvider +import io.mockk.mockk +import mozilla.components.browser.session.engine.EngineMiddleware +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.base.android.Clock +import mozilla.components.support.test.ext.joinBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics + +@RunWith(FenixRobolectricTestRunner::class) +class TelemetryLifecycleObserverTest { + @get:Rule + val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) + + private val clock = FakeClock() + + @Before + fun setUp() { + Clock.delegate = clock + } + + @After + fun tearDown() { + Clock.reset() + } + + @Test + fun `resume without a pause does not record any metrics`() { + val store = BrowserStore() + val observer = TelemetryLifecycleObserver(store) + observer.onResume() + + assertFalse(EngineMetrics.foregroundMetrics.testHasValue()) + } + + @Test + fun `resume after pause records metrics`() { + val store = BrowserStore() + val observer = TelemetryLifecycleObserver(store) + + observer.onPause() + + clock.elapsedTime = 550 + + observer.onResume() + + assertTrue(EngineMetrics.foregroundMetrics.testHasValue()) + + val metrics = EngineMetrics.foregroundMetrics.testGetValue() + assertEquals(1, metrics.size) + + val metric = metrics[0] + assertNotNull(metric.extra) + assertEquals("550", metric.extra!!["time_in_background"]) + } + + @Test + fun `resume records expected values`() { + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "mozilla", engineSession = mockk(relaxed = true)), + createTab("https://news.google.com", id = "news"), + createTab("https://theverge.com", id = "theverge", engineSession = mockk(relaxed = true)), + createTab("https://www.google.com", id = "google", engineSession = mockk(relaxed = true)), + createTab("https://getpocket.com", id = "pocket", crashed = true) + ) + ), + middleware = EngineMiddleware.create(engine = mockk(), sessionLookup = { null }) + ) + + val observer = TelemetryLifecycleObserver(store) + + clock.elapsedTime = 120 + + observer.onPause() + + store.dispatch( + EngineAction.KillEngineSessionAction("theverge") + ).joinBlocking() + + store.dispatch( + EngineAction.SuspendEngineSessionAction("mozilla") + ).joinBlocking() + + clock.elapsedTime = 10340 + + observer.onResume() + + assertTrue(EngineMetrics.foregroundMetrics.testHasValue()) + + val metrics = EngineMetrics.foregroundMetrics.testGetValue() + assertEquals(1, metrics.size) + + val metric = metrics[0] + assertNotNull(metric.extra) + assertEquals("10220", metric.extra!!["time_in_background"]) + assertEquals("3", metric.extra!!["background_active_tabs"]) + assertEquals("1", metric.extra!!["background_crashed_tabs"]) + assertEquals("5", metric.extra!!["background_total_tabs"]) + assertEquals("1", metric.extra!!["foreground_active_tabs"]) + assertEquals("1", metric.extra!!["foreground_crashed_tabs"]) + assertEquals("5", metric.extra!!["foreground_total_tabs"]) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt index b991e7fd7..672c6df43 100644 --- a/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt @@ -391,7 +391,7 @@ class TelemetryMiddlewareTest { } } -private class FakeClock : Clock.Delegate { +internal class FakeClock : Clock.Delegate { var elapsedTime: Long = 0 override fun elapsedRealtime(): Long = elapsedTime } diff --git a/docs/metrics.md b/docs/metrics.md index 618377c12..63602a25b 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -126,6 +126,7 @@ In addition to those built-in metrics, the following metrics are added to the pi | downloads_management.item_deleted |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how often a user deletes one / more downloads at a time. |[mozilla-mobile/fenix#16728](https://github.com/mozilla-mobile/fenix/pull/16728), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | | | downloads_management.item_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how often a user tap to opens a download from inside the "Downloads" folder. |[mozilla-mobile/fenix#16728](https://github.com/mozilla-mobile/fenix/pull/16728), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | | | downloads_misc.download_added |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how many times something is downloaded in the app. |[mozilla-mobile/fenix#16730](https://github.com/mozilla-mobile/fenix/pull/16730), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | | +| engine_tab.foreground_metrics |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Event collecting data about the state of tabs when the app comes back to the foreground. |[mozilla-mobile/fenix#18747](https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764)||2021-12-31 |1 | | error_page.visited_error |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user encountered an error page |[mozilla-mobile/fenix#2491](https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 |2 | | events.app_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the app (from cold start, to the homescreen or browser) |[mozilla-mobile/fenix#1067](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 |2 | | events.app_opened_all_startup |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |**This probe has a known flaw:** for COLD start up, it doesn't take into account if the process is already running when the app starts, possibly inflating results (e.g. a Service started the process 20min ago and only now is HomeActivity launching). See the `cold_*_app_to_first_frame` probes for a replacement.

A user opened the app to the HomeActivity. The HomeActivity encompasses the home screen, browser screen, settings screen, collections and other screens in the nav_graph. This differs from the app_opened probe because it measures all startups, not just cold startup. Note: There is a short gap between the time application goes into background and the time android reports the application going into the background. Note: This metric does not record souce when app opened from task switcher: open application -> press home button -> open recent tasks -> choose fenix. In this case will report [source = unknown, type = hot, has_saved_instance_state = false]. |[mozilla-mobile/fenix#12114](https://github.com/mozilla-mobile/fenix/pull/12114#pullrequestreview-445245341), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#13494](https://github.com/mozilla-mobile/fenix/pull/13494#pullrequestreview-474050499), [mozilla-mobile/fenix#15605](https://github.com/mozilla-mobile/fenix/pull/15605#issuecomment-702365594)||2021-06-01 |2 |