Introduce process lifecycle observer to collect metrics about tabs when app goes to foreground/background.
parent
54d46c7e94
commit
dfb3c4c9bf
@ -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
|
||||
)
|
@ -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"])
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue