forked from mozilla-mobile/fenix
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For mozilla-mobile#18836: add & integrate StartupTypeTelemetry.
- Loading branch information
Showing
3 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
app/src/main/java/org/mozilla/fenix/perf/StartupTypeTelemetry.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* 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.perf | ||
|
||
import androidx.annotation.VisibleForTesting | ||
import androidx.annotation.VisibleForTesting.NONE | ||
import androidx.annotation.VisibleForTesting.PRIVATE | ||
import androidx.lifecycle.DefaultLifecycleObserver | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleOwner | ||
import mozilla.components.support.base.log.logger.Logger | ||
import org.mozilla.fenix.GleanMetrics.PerfStartup | ||
import org.mozilla.fenix.HomeActivity | ||
import org.mozilla.fenix.perf.StartupPathProvider.StartupPath | ||
import org.mozilla.fenix.perf.StartupStateProvider.StartupState | ||
|
||
private val activityClass = HomeActivity::class.java | ||
|
||
private val logger = Logger("StartupTypeTelemetry") | ||
|
||
/** | ||
* Records telemetry for the number of start ups. See the | ||
* [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) | ||
* for specific definitions. | ||
* | ||
* This should be a member variable of [HomeActivity] because its data is tied to the lifecycle of an | ||
* Activity. Call [attachOnHomeActivityOnCreate] for this class to work correctly. | ||
* | ||
* N.B.: this class is lightly hardcoded to HomeActivity. | ||
*/ | ||
class StartupTypeTelemetry( | ||
private val startupStateProvider: StartupStateProvider, | ||
private val startupPathProvider: StartupPathProvider | ||
) { | ||
|
||
fun attachOnHomeActivityOnCreate(lifecycle: Lifecycle) { | ||
lifecycle.addObserver(StartupTypeLifecycleObserver()) | ||
} | ||
|
||
private fun getTelemetryLabel(startupState: StartupState, startupPath: StartupPath): String { | ||
// We don't use the enum name directly to avoid unintentional changes when refactoring. | ||
val stateLabel = when (startupState) { | ||
StartupState.COLD -> "cold" | ||
StartupState.WARM -> "warm" | ||
StartupState.HOT -> "hot" | ||
StartupState.UNKNOWN -> "unknown" | ||
} | ||
|
||
val pathLabel = when (startupPath) { | ||
StartupPath.MAIN -> "main" | ||
StartupPath.VIEW -> "view" | ||
|
||
// To avoid combinatorial explosion in label names, we bucket NOT_SET into UNKNOWN. | ||
StartupPath.NOT_SET, | ||
StartupPath.UNKNOWN -> "unknown" | ||
} | ||
|
||
return "${stateLabel}_$pathLabel" | ||
} | ||
|
||
@VisibleForTesting(otherwise = NONE) | ||
fun getTestCallbacks() = StartupTypeLifecycleObserver() | ||
|
||
@VisibleForTesting(otherwise = PRIVATE) | ||
fun record() { | ||
val startupState = startupStateProvider.getStartupStateForStartedActivity(activityClass) | ||
val startupPath = startupPathProvider.startupPathForActivity | ||
val label = getTelemetryLabel(startupState, startupPath) | ||
|
||
PerfStartup.startupType[label].add(1) | ||
logger.info("Recorded start up: $label") | ||
} | ||
|
||
@VisibleForTesting(otherwise = PRIVATE) | ||
inner class StartupTypeLifecycleObserver : DefaultLifecycleObserver { | ||
private var shouldRecordStart = false | ||
|
||
override fun onStart(owner: LifecycleOwner) { | ||
shouldRecordStart = true | ||
} | ||
|
||
override fun onResume(owner: LifecycleOwner) { | ||
// We must record in onResume because the StartupStateProvider can only be called for | ||
// STARTED activities and we can't guarantee our onStart is called before its. | ||
// | ||
// We only record if start was called for this resume to avoid recording | ||
// for onPause -> onResume states. | ||
if (shouldRecordStart) { | ||
record() | ||
shouldRecordStart = false | ||
} | ||
} | ||
} | ||
} |
140 changes: 140 additions & 0 deletions
140
app/src/test/java/org/mozilla/fenix/perf/StartupTypeTelemetryTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/* 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.perf | ||
|
||
import androidx.lifecycle.Lifecycle | ||
import io.mockk.MockKAnnotations | ||
import io.mockk.every | ||
import io.mockk.impl.annotations.MockK | ||
import io.mockk.mockk | ||
import io.mockk.spyk | ||
import io.mockk.verify | ||
import mozilla.components.support.ktx.kotlin.crossProduct | ||
import mozilla.components.support.test.robolectric.testContext | ||
import mozilla.telemetry.glean.testing.GleanTestRule | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Assert.assertFalse | ||
import org.junit.Before | ||
import org.junit.Rule | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.mozilla.fenix.GleanMetrics.PerfStartup | ||
import org.mozilla.fenix.HomeActivity | ||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner | ||
import org.mozilla.fenix.perf.StartupPathProvider.StartupPath | ||
import org.mozilla.fenix.perf.StartupStateProvider.StartupState | ||
|
||
private val validTelemetryLabels = run { | ||
val allStates = listOf("cold", "warm", "hot", "unknown") | ||
val allPaths = listOf("main", "view", "unknown") | ||
|
||
allStates.crossProduct(allPaths) { state, path -> "${state}_$path" }.toSet() | ||
} | ||
|
||
private val activityClass = HomeActivity::class.java | ||
|
||
@RunWith(FenixRobolectricTestRunner::class) | ||
class StartupTypeTelemetryTest { | ||
|
||
@get:Rule | ||
val gleanTestRule = GleanTestRule(testContext) | ||
|
||
private lateinit var telemetry: StartupTypeTelemetry | ||
private lateinit var callbacks: StartupTypeTelemetry.StartupTypeLifecycleObserver | ||
@MockK private lateinit var stateProvider: StartupStateProvider | ||
@MockK private lateinit var pathProvider: StartupPathProvider | ||
|
||
@Before | ||
fun setUp() { | ||
MockKAnnotations.init(this) | ||
telemetry = spyk(StartupTypeTelemetry(stateProvider, pathProvider)) | ||
callbacks = telemetry.getTestCallbacks() | ||
} | ||
|
||
@Test | ||
fun `WHEN attach is called THEN it is registered to the lifecycle`() { | ||
val lifecycle = mockk<Lifecycle>(relaxed = true) | ||
telemetry.attachOnHomeActivityOnCreate(lifecycle) | ||
|
||
verify { lifecycle.addObserver(any()) } | ||
} | ||
|
||
@Test | ||
fun `GIVEN all possible path and state combinations WHEN record telemetry THEN the labels are incremented the appropriate number of times`() { | ||
val allPossibleInputArgs = StartupState.values().toList().crossProduct( | ||
StartupPath.values().toList() | ||
) { state, path -> | ||
Pair(state, path) | ||
} | ||
|
||
allPossibleInputArgs.forEach { (state, path) -> | ||
every { stateProvider.getStartupStateForStartedActivity(activityClass) } returns state | ||
every { pathProvider.startupPathForActivity } returns path | ||
|
||
telemetry.record() | ||
} | ||
|
||
validTelemetryLabels.forEach { label -> | ||
// Path == NOT_SET gets bucketed with Path == UNKNOWN so we'll increment twice for those. | ||
val expected = if (label.endsWith("unknown")) 2 else 1 | ||
assertEquals("label: $label", expected, PerfStartup.startupType[label].testGetValue()) | ||
} | ||
|
||
// All invalid labels go to a single bucket: let's verify it has no value. | ||
assertFalse(PerfStartup.startupType["__other__"].testHasValue()) | ||
} | ||
|
||
@Test | ||
fun `WHEN record is called THEN telemetry is recorded with the appropriate label`() { | ||
every { stateProvider.getStartupStateForStartedActivity(activityClass) } returns StartupState.COLD | ||
every { pathProvider.startupPathForActivity } returns StartupPath.MAIN | ||
|
||
telemetry.record() | ||
|
||
assertEquals(1, PerfStartup.startupType["cold_main"].testGetValue()) | ||
} | ||
|
||
@Test | ||
fun `GIVEN the activity is launched WHEN onResume is called THEN we record the telemetry`() { | ||
launchApp() | ||
verify(exactly = 1) { telemetry.record() } | ||
} | ||
|
||
@Test | ||
fun `GIVEN the activity is launched WHEN the activity is paused and resumed THEN record is not called`() { | ||
// This part of the test duplicates another test but it's needed to initialize the state of this test. | ||
launchApp() | ||
verify(exactly = 1) { telemetry.record() } | ||
|
||
callbacks.onPause(mockk()) | ||
callbacks.onResume(mockk()) | ||
|
||
verify(exactly = 1) { telemetry.record() } // i.e. this shouldn't be called again. | ||
} | ||
|
||
@Test | ||
fun `GIVEN the activity is launched WHEN the activity is stopped and resumed THEN record is called again`() { | ||
// This part of the test duplicates another test but it's needed to initialize the state of this test. | ||
launchApp() | ||
verify(exactly = 1) { telemetry.record() } | ||
|
||
callbacks.onPause(mockk()) | ||
callbacks.onStop(mockk()) | ||
callbacks.onStart(mockk()) | ||
callbacks.onResume(mockk()) | ||
|
||
verify(exactly = 2) { telemetry.record() } // i.e. this should be called again. | ||
} | ||
|
||
private fun launchApp() { | ||
// What these return isn't important. | ||
every { stateProvider.getStartupStateForStartedActivity(activityClass) } returns StartupState.COLD | ||
every { pathProvider.startupPathForActivity } returns StartupPath.MAIN | ||
|
||
callbacks.onCreate(mockk()) | ||
callbacks.onStart(mockk()) | ||
callbacks.onResume(mockk()) | ||
} | ||
} |