Skip to content

Commit

Permalink
Add tests for StartupTracker
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed May 12, 2024
1 parent e31760d commit ddf4a92
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 19 deletions.
2 changes: 1 addition & 1 deletion embrace-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ dependencies {
testImplementation "io.mockk:mockk:1.12.2"
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "org.robolectric:robolectric:4.10.3"
testImplementation "org.robolectric:robolectric:4.12.1"
testImplementation project(path: ":embrace-android-sdk")
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3"
testImplementation("com.google.protobuf:protobuf-java:3.24.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.embrace.android.embracesdk.capture.startup

/**
* Collects relevant information during app startup to be used to produce telemetry about the startup workflow.
*
* Due to differences in behaviour between platform versions and various startup scenarios, you cannot assume that these methods
* will be invoked in any order or at all. Implementations need to take into account that fact when using the underlying data.
*/
internal interface AppStartupDataCollector {
/**
* Set the time when the application object initialization was started
*/
fun applicationInitStart(timestampMs: Long? = null)

/**
* Set the time when the application object initialization has finished
*/
fun applicationInitEnd(timestampMs: Long? = null)

/**
* Set the time just prior to the creation of the Activity whose rendering will denote the end of the startup workflow
*/
fun startupActivityPreCreated(timestampMs: Long? = null)

/**
* Set the time for the start of the initialization of the Activity whose rendering will denote the end of the startup workflow
*/
fun startupActivityInitStart(timestampMs: Long? = null)

/**
* Set the time just after the creation of the Activity whose rendering will denote the end of the startup workflow
*/
fun startupActivityPostCreated(timestampMs: Long? = null)

/**
* Set the time for the end of the initialization of the Activity whose rendering will denote the end of the startup workflow
*/
fun startupActivityInitEnd(timestampMs: Long? = null)

/**
* Set the time for when the startup Activity begins to render as well as its name
*/
fun startupActivityResumed(activityName: String, timestampMs: Long? = null)

/**
* Set the time for when the startup Activity has finished rendering its first frame as well as its name
*/
fun firstFrameRendered(activityName: String, timestampMs: Long? = null)

/**
* Set an arbitrary time interval during startup that is of note
*/
fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal class AppStartupTraceEmitter(
private val backgroundWorker: BackgroundWorker,
private val versionChecker: VersionChecker,
private val logger: EmbLogger
) {
) : AppStartupDataCollector {
private val processCreateRequestedMs: Long?
private val processCreatedMs: Long?
private val additionalTrackedIntervals = ConcurrentLinkedQueue<TrackedInterval>()
Expand Down Expand Up @@ -90,52 +90,55 @@ internal class AppStartupTraceEmitter(
private val startupRecorded = AtomicBoolean(false)
private val endWithFrameDraw: Boolean = versionChecker.isAtLeast(VERSION_CODES.Q)

fun applicationInitStart(timestampMs: Long? = null) {
override fun applicationInitStart(timestampMs: Long?) {
applicationInitStartMs = timestampMs ?: nowMs()
}

fun applicationInitEnd(timestampMs: Long? = null) {
override fun applicationInitEnd(timestampMs: Long?) {
applicationInitEndMs = timestampMs ?: nowMs()
}

fun startupActivityPreCreated(timestampMs: Long? = null) {
override fun startupActivityPreCreated(timestampMs: Long?) {
startupActivityPreCreatedMs = timestampMs ?: nowMs()
}

fun startupActivityInitStart(timestampMs: Long? = null) {
override fun startupActivityInitStart(timestampMs: Long?) {
startupActivityInitStartMs = timestampMs ?: nowMs()
}

fun startupActivityPostCreated(timestampMs: Long? = null) {
override fun startupActivityPostCreated(timestampMs: Long?) {
startupActivityPostCreatedMs = timestampMs ?: nowMs()
}

fun startupActivityInitEnd(timestampMs: Long? = null) {
override fun startupActivityInitEnd(timestampMs: Long?) {
startupActivityInitEndMs = timestampMs ?: nowMs()
}

fun startupActivityResumed(activityName: String, timestampMs: Long? = null) {
override fun startupActivityResumed(activityName: String, timestampMs: Long?) {
startupActivityName = activityName
startupActivityResumedMs = timestampMs ?: nowMs()
if (!endWithFrameDraw) {
dataCollectionComplete()
}
}

fun firstFrameRendered(activityName: String, timestampMs: Long? = null) {
override fun firstFrameRendered(activityName: String, timestampMs: Long?) {
startupActivityName = activityName
firstFrameRenderedMs = timestampMs ?: nowMs()
if (endWithFrameDraw) {
dataCollectionComplete()
}
}

fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long) {
override fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long) {
additionalTrackedIntervals.add(
TrackedInterval(name = name, startTimeMs = startTimeMs, endTimeMs = endTimeMs)
)
}

/**
* Called when app startup is considered complete, i.e. the data can be used and any additional updates can be ignored
*/
private fun dataCollectionComplete() {
if (!startupRecorded.get()) {
synchronized(startupRecorded) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import io.embrace.android.embracesdk.logging.EmbLogger
* that can be found here: https://blog.p-y.wtf/tracking-android-app-launch-in-production. PY's code was adapted and tweaked for use here.
*/
internal class StartupTracker(
private val appStartupTraceEmitter: AppStartupTraceEmitter,
private val appStartupDataCollector: AppStartupDataCollector,
private val logger: EmbLogger,
private val versionChecker: VersionChecker,
) : Application.ActivityLifecycleCallbacks {
Expand All @@ -46,14 +46,14 @@ internal class StartupTracker(

override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity.useAsStartupActivity()) {
appStartupTraceEmitter.startupActivityPreCreated()
appStartupDataCollector.startupActivityPreCreated()
}
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity.useAsStartupActivity()) {
val activityName = activity.localClassName
appStartupTraceEmitter.startupActivityInitStart()
appStartupDataCollector.startupActivityInitStart()
if (versionChecker.isAtLeast(Build.VERSION_CODES.Q)) {
if (!isFirstDraw) {
val window = activity.window
Expand All @@ -63,7 +63,7 @@ internal class StartupTracker(
decorView.onNextDraw {
if (!isFirstDraw) {
isFirstDraw = true
val callback = { appStartupTraceEmitter.firstFrameRendered(activityName = activityName) }
val callback = { appStartupDataCollector.firstFrameRendered(activityName = activityName) }
decorView.viewTreeObserver.registerFrameCommitCallback(callback)
}
}
Expand All @@ -79,19 +79,19 @@ internal class StartupTracker(

override fun onActivityPostCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity.useAsStartupActivity()) {
appStartupTraceEmitter.startupActivityPostCreated()
appStartupDataCollector.startupActivityPostCreated()
}
}

override fun onActivityStarted(activity: Activity) {
if (activity.isStartupActivity()) {
appStartupTraceEmitter.startupActivityInitEnd()
appStartupDataCollector.startupActivityInitEnd()
}
}

override fun onActivityResumed(activity: Activity) {
if (activity.observeForStartup()) {
appStartupTraceEmitter.startupActivityResumed(activityName = activity.localClassName)
appStartupDataCollector.startupActivityResumed(activityName = activity.localClassName)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ internal class DataCaptureServiceModuleImpl @JvmOverloads constructor(

override val startupTracker: StartupTracker by singleton {
StartupTracker(
appStartupTraceEmitter = appStartupTraceEmitter,
appStartupDataCollector = appStartupTraceEmitter,
logger = initModule.logger,
versionChecker = versionChecker,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package io.embrace.android.embracesdk.capture.startup

import android.app.Activity
import android.app.Application
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.fakes.FakeActivity
import io.embrace.android.embracesdk.fakes.FakeAppStartupDataCollector
import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakeSplashScreenActivity
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
import io.embrace.android.embracesdk.logging.InternalEmbraceLogger
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RuntimeEnvironment
import org.robolectric.android.controller.ActivityController
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
internal class StartupTrackerTest {
private lateinit var application: Application
private lateinit var clock: FakeClock
private lateinit var dataCollector: FakeAppStartupDataCollector
private lateinit var logger: InternalEmbraceLogger
private lateinit var startupTracker: StartupTracker
private lateinit var defaultActivityController: ActivityController<Activity>

@Before
fun setUp() {
application = RuntimeEnvironment.getApplication()
clock = FakeClock()
logger = InternalEmbraceLogger()
dataCollector = FakeAppStartupDataCollector(clock = clock)
startupTracker = StartupTracker(
appStartupDataCollector = dataCollector,
logger = logger,
versionChecker = BuildVersionChecker
)
application.registerActivityLifecycleCallbacks(startupTracker)
defaultActivityController = Robolectric.buildActivity(Activity::class.java)
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `cold start in U`() {
with(launchActivity()) {
assertEquals("android.app.Activity", dataCollector.startupActivityName)
verifyTiming(
preCreateTime = createTime,
createTime = createTime,
postCreateTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

@Config(sdk = [Build.VERSION_CODES.Q])
@Test
fun `cold start in Q`() {
with(launchActivity()) {
verifyTiming(
preCreateTime = createTime,
createTime = createTime,
postCreateTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

@Config(sdk = [Build.VERSION_CODES.P])
@Test
fun `cold start in P`() {
with(launchActivity()) {
verifyTiming(
createTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `cold start in L`() {
with(launchActivity()) {
verifyTiming(
createTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `cold start with activity instance recreated`() {
defaultActivityController.create()
clock.tick()
val recreateTime = clock.now()
defaultActivityController.recreate()
clock.tick()
val startTime = clock.now()
defaultActivityController.start()
clock.tick()
val resumeTime = clock.now()
defaultActivityController.resume()
clock.tick()

verifyTiming(
preCreateTime = recreateTime,
createTime = recreateTime,
postCreateTime = recreateTime,
startTime = startTime,
resumeTime = resumeTime
)
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `cold start with different activities being created and foregrounded first`() {
defaultActivityController.create()
clock.tick()
with(launchActivity(Robolectric.buildActivity(FakeActivity::class.java))) {
assertEquals("io.embrace.android.embracesdk.fakes.FakeActivity", dataCollector.startupActivityName)
verifyTiming(
preCreateTime = createTime,
createTime = createTime,
postCreateTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `cold start initial activity not tracked will use the second for timing`() {
launchActivity(Robolectric.buildActivity(FakeSplashScreenActivity::class.java))
clock.tick()
with(launchActivity()) {
assertEquals("android.app.Activity", dataCollector.startupActivityName)
verifyTiming(
preCreateTime = createTime,
createTime = createTime,
postCreateTime = createTime,
startTime = startTime,
resumeTime = resumeTime
)
}
}

private fun launchActivity(controller: ActivityController<*> = defaultActivityController): ActivityTiming {
val createTime = clock.now()
controller.create()
clock.tick()
val startTime = clock.now()
controller.start()
clock.tick()
val resumeTime = clock.now()
controller.resume()
clock.tick()
return ActivityTiming(createTime, startTime, resumeTime)
}

private fun verifyTiming(
preCreateTime: Long? = null,
createTime: Long,
postCreateTime: Long? = null,
startTime: Long,
resumeTime: Long
) {
assertEquals(preCreateTime, dataCollector.startupActivityPreCreatedMs)
assertEquals(createTime, dataCollector.startupActivityInitStartMs)
assertEquals(postCreateTime, dataCollector.startupActivityPostCreatedMs)
assertEquals(startTime, dataCollector.startupActivityInitEndMs)
assertEquals(resumeTime, dataCollector.startupActivityResumedMs)
}

private data class ActivityTiming(
val createTime: Long,
val startTime: Long,
val resumeTime: Long
)
}
Loading

0 comments on commit ddf4a92

Please sign in to comment.