For #18836: add StartupStateProvider.
parent
e864e74960
commit
0cbedaadb1
@ -0,0 +1,71 @@
|
||||
/* 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 android.app.Activity
|
||||
import org.mozilla.fenix.perf.AppStartReasonProvider.StartReason
|
||||
import org.mozilla.fenix.perf.StartupActivityLog.LogEntry
|
||||
|
||||
/**
|
||||
* Identifies the "state" of start up where state can be COLD/WARM/HOT and possibly others. See
|
||||
* the [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary)
|
||||
* for specific definitions.
|
||||
*
|
||||
* This class is nuanced: **please read the kdoc carefully before using it.** Consider contacting
|
||||
* the perf team with your use case.
|
||||
*
|
||||
* For this class, we use the terminology from the [StartupActivityLog] such as STARTED and STOPPED.
|
||||
* However, we're assuming STARTED means foregrounded and STOPPED means backgrounded. If this
|
||||
* assumption is false, the logic in this class may be incorrect.
|
||||
*/
|
||||
class StartupStateProvider(
|
||||
private val startupLog: StartupActivityLog,
|
||||
private val startReasonProvider: AppStartReasonProvider
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns true if the current startup state is COLD and the currently started activity is the
|
||||
* first started activity (i.e. we can use it for performance measurements).
|
||||
*
|
||||
* This method must be called after the foreground Activity is STARTED.
|
||||
*/
|
||||
fun isColdStartForStartedActivity(activityClass: Class<out Activity>): Boolean {
|
||||
// A cold start means:
|
||||
// - the process was started for the first started activity (e.g. not a service)
|
||||
// - the first started activity ever is still active
|
||||
//
|
||||
// Thus, for the activity log we expect:
|
||||
// [... Activity-STARTED, App-STARTED]
|
||||
// since if another Activity was started, it would appear after App-STARTED. This is where:
|
||||
// - the app has not been stopped ever
|
||||
if (startReasonProvider.reason != StartReason.ACTIVITY) {
|
||||
return false
|
||||
}
|
||||
|
||||
val firstActivityStartedIndex = startupLog.log.indexOfFirst { it is LogEntry.StartedActivityLogEntry }
|
||||
if (firstActivityStartedIndex < 0) {
|
||||
return false // if no activities are started, then we haven't started up yet.
|
||||
}
|
||||
|
||||
val firstActivityStartedAndAfter = startupLog.log.subList(firstActivityStartedIndex, startupLog.log.size)
|
||||
val isFirstActivityStartedStillForegrounded = firstActivityStartedAndAfter == listOf(
|
||||
LogEntry.StartedActivityLogEntry(activityClass),
|
||||
LogEntry.AppStarted
|
||||
)
|
||||
|
||||
val hasAppBeenStopped = startupLog.log.contains(LogEntry.AppStopped)
|
||||
|
||||
return isFirstActivityStartedStillForegrounded && !hasAppBeenStopped
|
||||
}
|
||||
|
||||
/**
|
||||
* A short-circuit implementation of [isColdStartForStartedActivity] that will return false early
|
||||
* so we don't have to call [isColdStartForStartedActivity].
|
||||
*
|
||||
* When this can be called might be tightly coupled to [ColdStartupDurationTelemetry]: use at
|
||||
* your own risk.
|
||||
*/
|
||||
fun shouldShortCircuitColdStart(): Boolean = startupLog.log.contains(LogEntry.AppStopped)
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
/* 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 io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.IntentReceiverActivity
|
||||
import org.mozilla.fenix.perf.AppStartReasonProvider.StartReason
|
||||
import org.mozilla.fenix.perf.StartupActivityLog.LogEntry
|
||||
|
||||
class StartupStateProviderTest {
|
||||
|
||||
private lateinit var provider: StartupStateProvider
|
||||
@MockK private lateinit var startupActivityLog: StartupActivityLog
|
||||
@MockK private lateinit var startReasonProvider: AppStartReasonProvider
|
||||
|
||||
private lateinit var logEntries: MutableList<LogEntry>
|
||||
|
||||
private val homeActivityClass = HomeActivity::class.java
|
||||
private val irActivityClass = IntentReceiverActivity::class.java
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
provider = StartupStateProvider(startupActivityLog, startReasonProvider)
|
||||
|
||||
logEntries = mutableListOf()
|
||||
every { startupActivityLog.log } returns logEntries
|
||||
|
||||
every { startReasonProvider.reason } returns StartReason.ACTIVITY // default to minimize repetition.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN we launched to HomeActivity directly THEN start up is cold`() {
|
||||
// These entries mimic observed behavior.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertTrue(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN we launched to HA through a non-drawing IntentRA THEN start up is cold`() {
|
||||
// These entries mimic observed behavior.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertTrue(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN we launched HA through a drawing IntentRA THEN start up is not cold`() {
|
||||
// These entries mimic observed behavior for local code changes.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(irActivityClass),
|
||||
LogEntry.AppStarted,
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StoppedActivityLogEntry(irActivityClass)
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN two HomeActivities are created THEN start up is not cold`() {
|
||||
// We're making an assumption about how this would work based on previous observed patterns.
|
||||
// AIUI, we should never have more than one HomeActivity.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted,
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StoppedActivityLogEntry(homeActivityClass)
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity and we're truncating the log for optimization WHEN warm start THEN start up is not cold`() {
|
||||
// These entries are from observed behavior.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.AppStopped,
|
||||
LogEntry.StoppedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity and we're truncating the log for optimization WHEN hot start THEN start up is not cold`() {
|
||||
// These entries are from observed behavior.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.AppStopped,
|
||||
LogEntry.StoppedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity and we're not truncating the log for optimization WHEN warm start THEN start up is not cold`() {
|
||||
// While the entries are from observed behavior, this log shouldn't occur in the wild due to
|
||||
// our log optimizations. However, just in case the behavior changes, we check for it.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted,
|
||||
LogEntry.AppStopped,
|
||||
LogEntry.StoppedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity and we're not truncating the log for optimization WHEN hot start THEN start up is not cold`() {
|
||||
// This shouldn't occur in the wild due to the optimization but, just in case the behavior changes,
|
||||
// we check for it.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted,
|
||||
LogEntry.AppStopped,
|
||||
LogEntry.StoppedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN multiple activities are started but not stopped (maybe impossible) THEN start up is not cold`() {
|
||||
fun assertIsNotCold() { assertFalse(provider.isColdStartForStartedActivity(homeActivityClass)) }
|
||||
|
||||
// Since we've never observed this, there are multiple ways the events could
|
||||
// theoretically be ordered: we try a few.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(irActivityClass),
|
||||
LogEntry.AppStarted,
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass)
|
||||
))
|
||||
assertIsNotCold()
|
||||
|
||||
logEntries.clear()
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(irActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertIsNotCold()
|
||||
|
||||
logEntries.clear()
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(irActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertIsNotCold()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN an activity hasn't been created yet THEN start up is not cold`() {
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app started for an activity WHEN an activity hasn't started yet THEN start up is not cold`() {
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass)
|
||||
))
|
||||
assertFalse(provider.isColdStartForStartedActivity(homeActivityClass))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app did not start for an activity WHEN is cold is checked THEN it returns false`() {
|
||||
fun assertIsNotCold() { assertFalse(provider.isColdStartForStartedActivity(homeActivityClass)) }
|
||||
|
||||
every { startReasonProvider.reason } returns StartReason.NON_ACTIVITY
|
||||
assertIsNotCold() // 🔥
|
||||
|
||||
// These are normally the success paths.
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertIsNotCold()
|
||||
|
||||
logEntries.clear()
|
||||
logEntries.addAll(listOf(
|
||||
LogEntry.CreatedActivityLogEntry(irActivityClass),
|
||||
LogEntry.CreatedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.StartedActivityLogEntry(homeActivityClass),
|
||||
LogEntry.AppStarted
|
||||
))
|
||||
assertIsNotCold()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app has been stopped WHEN is cold short circuit is called THEN it returns true`() {
|
||||
logEntries.add(LogEntry.AppStopped)
|
||||
assertTrue(provider.shouldShortCircuitColdStart())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the app has not been stopped WHEN is cold short circuit is called THEN it returns false`() {
|
||||
assertFalse(provider.shouldShortCircuitColdStart())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue