Skip to content

Commit

Permalink
Create callback mechanism to run code post app startup
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed Sep 27, 2024
1 parent 5be30d0 commit a74b5bf
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ interface AppStartupDataCollector {
/**
* Set the time for when the startup Activity begins to render as well as its name
*/
fun startupActivityResumed(activityName: String, timestampMs: Long? = null)
fun startupActivityResumed(activityName: String, collectionCompleteCallback: () -> Unit, 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)
fun firstFrameRendered(activityName: String, collectionCompleteCallback: () -> Unit, timestampMs: Long? = null)

/**
* Set an arbitrary time interval during startup that is of note
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,27 @@ internal class AppStartupTraceEmitter(
startupActivityInitEndMs = timestampMs ?: nowMs()
}

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

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

Expand All @@ -152,7 +160,7 @@ internal class AppStartupTraceEmitter(
/**
* Called when app startup is considered complete, i.e. the data can be used and any additional updates can be ignored
*/
private fun dataCollectionComplete() {
private fun dataCollectionComplete(callback: () -> Unit) {
if (!startupRecorded.get()) {
synchronized(startupRecorded) {
if (!startupRecorded.get()) {
Expand All @@ -162,6 +170,7 @@ internal class AppStartupTraceEmitter(
logger.logWarning("App startup trace recording attempted but did not succeed")
}
}
callback()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ class StartupTracker(
decorView.onNextDraw {
if (!isFirstDraw) {
isFirstDraw = true
val callback = { appStartupDataCollector.firstFrameRendered(activityName = activityName) }
val callback = {
appStartupDataCollector.firstFrameRendered(
activityName = activityName,
collectionCompleteCallback = {}

Check warning on line 69 in embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/StartupTracker.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/StartupTracker.kt#L66-L69

Added lines #L66 - L69 were not covered by tests
)
}
decorView.viewTreeObserver.registerFrameCommitCallback(callback)
}
}
Expand Down Expand Up @@ -91,7 +96,10 @@ class StartupTracker(

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

Check warning on line 101 in embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/StartupTracker.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/StartupTracker.kt#L101

Added line #L101 was not covered by tests
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.embrace.android.embracesdk.internal.spans.findAttributeValue
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
import io.embrace.android.embracesdk.internal.worker.BackgroundWorker
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -37,6 +38,7 @@ import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
internal class AppStartupTraceEmitterTest {
private var startupService: StartupService? = null
private var dataCollectionCompletedCallbackInvoked: Boolean = false
private lateinit var clock: FakeClock
private lateinit var spanSink: SpanSink
private lateinit var spanService: SpanService
Expand Down Expand Up @@ -75,7 +77,8 @@ internal class AppStartupTraceEmitterTest {
@Test
fun `no crashes if startup service not available in T`() {
startupService = null
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
assertTrue(dataCollectionCompletedCallbackInvoked)
}

@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
Expand Down Expand Up @@ -106,7 +109,8 @@ internal class AppStartupTraceEmitterTest {
@Test
fun `no crashes if startup service not available in S`() {
startupService = null
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
assertTrue(dataCollectionCompletedCallbackInvoked)
}

@Config(sdk = [Build.VERSION_CODES.S])
Expand Down Expand Up @@ -137,7 +141,8 @@ internal class AppStartupTraceEmitterTest {
@Test
fun `no crashes if startup service not available in P`() {
startupService = null
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
assertTrue(dataCollectionCompletedCallbackInvoked)
}

@Config(sdk = [Build.VERSION_CODES.P])
Expand All @@ -162,7 +167,8 @@ internal class AppStartupTraceEmitterTest {
@Test
fun `no crashes if startup service not available in M`() {
startupService = null
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
assertTrue(dataCollectionCompletedCallbackInvoked)
}

@Config(sdk = [Build.VERSION_CODES.M])
Expand All @@ -187,7 +193,8 @@ internal class AppStartupTraceEmitterTest {
@Test
fun `no crashes if startup service not available in L`() {
startupService = null
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
assertTrue(dataCollectionCompletedCallbackInvoked)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
Expand All @@ -208,6 +215,10 @@ internal class AppStartupTraceEmitterTest {
verifyWarmStartWithResumeWithoutAppInitEvents()
}

private fun dataCollectionCompletedCallback() {
dataCollectionCompletedCallbackInvoked = true
}

private fun verifyColdStartWithRender(processCreateDelayMs: Long? = null) {
clock.tick(100L)
appStartupTraceEmitter.applicationInitStart()
Expand Down Expand Up @@ -518,11 +529,11 @@ internal class AppStartupTraceEmitterTest {
}

private fun startupActivityRender(renderFrame: Boolean = true): Pair<Long, Long> {
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.startupActivityResumed(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
val resumed = clock.now()
if (renderFrame) {
clock.tick(199L)
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME)
appStartupTraceEmitter.firstFrameRendered(STARTUP_ACTIVITY_NAME, ::dataCollectionCompletedCallback)
}
return Pair(resumed, clock.now())
}
Expand All @@ -549,6 +560,7 @@ internal class AppStartupTraceEmitterTest {
assertEquals(expectedFirstActivityLifecycleEventMs?.toString(), attrs.findAttributeValue("first-activity-init-ms"))
assertEquals("false", attrs.findAttributeValue("embrace-init-in-foreground"))
assertEquals("main", attrs.findAttributeValue("embrace-init-thread-name"))
assertTrue(dataCollectionCompletedCallbackInvoked)
}

private fun assertChildSpan(span: EmbraceSpanData, expectedStartTimeNanos: Long, expectedEndTimeNanos: Long) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ class FakeAppStartupDataCollector(
startupActivityInitEndMs = timestampMs ?: clock.now()
}

override fun startupActivityResumed(activityName: String, timestampMs: Long?) {
override fun startupActivityResumed(
activityName: String,
collectionCompleteCallback: () -> Unit,
timestampMs: Long?
) {
startupActivityName = activityName
startupActivityResumedMs = timestampMs ?: clock.now()
}

override fun firstFrameRendered(activityName: String, timestampMs: Long?) {
override fun firstFrameRendered(
activityName: String,
collectionCompleteCallback: () -> Unit,
timestampMs: Long?
) {
startupActivityName = activityName
firstFrameRenderedMs = timestampMs ?: clock.now()
}
Expand Down

0 comments on commit a74b5bf

Please sign in to comment.