Skip to content

Commit 82b9c7e

Browse files
committed
Remove splash screen time in warm/cold app startup heuristic
1 parent c2f4b6a commit 82b9c7e

File tree

7 files changed

+161
-10
lines changed

7 files changed

+161
-10
lines changed

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ interface AppStartupDataCollector {
2020
*/
2121
fun applicationInitEnd(timestampMs: Long? = null)
2222

23+
/**
24+
* Set the time the first activity was detected to have started, irrespective of whether it should be used for startup
25+
*/
26+
fun firstActivityInit(timestampMs: Long? = null)
27+
2328
/**
2429
* Set the time just prior to the creation of the Activity whose rendering will denote the end of the startup workflow
2530
*/

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ internal class AppStartupTraceEmitter(
113113
applicationInitEndMs = timestampMs ?: nowMs()
114114
}
115115

116+
override fun firstActivityInit(timestampMs: Long?) {
117+
firstActivityInitStartMs = timestampMs ?: nowMs()
118+
}
119+
116120
override fun startupActivityPreCreated(timestampMs: Long?) {
117121
startupActivityPreCreatedMs = timestampMs ?: nowMs()
118122
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,17 @@ class StartupTracker(
3535

3636
private var startupActivityId: Int? = null
3737
private var startupDataCollectionComplete = false
38+
private var firstActivitySeen = false
3839

3940
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
41+
firstActivityInit()
4042
if (activity.useAsStartupActivity()) {
4143
appStartupDataCollector.startupActivityPreCreated()
4244
}
4345
}
4446

4547
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
48+
firstActivityInit()
4649
if (activity.useAsStartupActivity()) {
4750
appStartupDataCollector.startupActivityInitStart()
4851
val application = activity.application
@@ -87,6 +90,13 @@ class StartupTracker(
8790

8891
override fun onActivityDestroyed(activity: Activity) {}
8992

93+
private fun firstActivityInit() {
94+
if (!firstActivitySeen) {
95+
appStartupDataCollector.firstActivityInit()
96+
firstActivitySeen = true
97+
}
98+
}
99+
90100
private fun startupComplete(application: Application) {
91101
if (!startupDataCollectionComplete) {
92102
application.unregisterActivityLifecycleCallbacks(this)

embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/StartupTrackerTest.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import io.embrace.android.embracesdk.fakes.FakeAppStartupDataCollector
1010
import io.embrace.android.embracesdk.fakes.FakeClock
1111
import io.embrace.android.embracesdk.fakes.FakeDrawEventEmitter
1212
import io.embrace.android.embracesdk.fakes.FakeEmbLogger
13-
import io.embrace.android.embracesdk.fakes.FakeNotStartupActivity
1413
import io.embrace.android.embracesdk.fakes.FakeSplashScreenActivity
1514
import io.embrace.android.embracesdk.internal.capture.startup.AppStartupTraceEmitter.Companion.startupHasRenderEvent
1615
import io.embrace.android.embracesdk.internal.logging.EmbLogger
@@ -61,6 +60,7 @@ internal class StartupTrackerTest {
6160
with(launchActivity()) {
6261
assertEquals("android.app.Activity", dataCollector.startupActivityName)
6362
verifyLifecycle(
63+
firstActivityInitTime = createTime,
6464
preCreateTime = createTime,
6565
createTime = createTime,
6666
postCreateTime = createTime,
@@ -76,6 +76,7 @@ internal class StartupTrackerTest {
7676
fun `cold start in Q`() {
7777
with(launchActivity()) {
7878
verifyLifecycle(
79+
firstActivityInitTime = createTime,
7980
preCreateTime = createTime,
8081
createTime = createTime,
8182
postCreateTime = createTime,
@@ -91,6 +92,7 @@ internal class StartupTrackerTest {
9192
fun `cold start in P`() {
9293
with(launchActivity()) {
9394
verifyLifecycle(
95+
firstActivityInitTime = createTime,
9496
createTime = createTime,
9597
startTime = startTime,
9698
resumeTime = resumeTime,
@@ -103,6 +105,7 @@ internal class StartupTrackerTest {
103105
fun `cold start in L`() {
104106
with(launchActivity()) {
105107
verifyLifecycle(
108+
firstActivityInitTime = createTime,
106109
createTime = createTime,
107110
startTime = startTime,
108111
resumeTime = resumeTime
@@ -113,11 +116,13 @@ internal class StartupTrackerTest {
113116
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
114117
@Test
115118
fun `cold start with different activities being created and foregrounded first`() {
119+
val firstActivityInitTime = clock.now()
116120
defaultActivityController.create()
117121
clock.tick()
118122
with(launchActivity(Robolectric.buildActivity(FakeActivity::class.java))) {
119123
assertEquals("io.embrace.android.embracesdk.fakes.FakeActivity", dataCollector.startupActivityName)
120124
verifyLifecycle(
125+
firstActivityInitTime = firstActivityInitTime,
121126
preCreateTime = createTime,
122127
createTime = createTime,
123128
postCreateTime = createTime,
@@ -131,11 +136,12 @@ internal class StartupTrackerTest {
131136
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
132137
@Test
133138
fun `cold start initial activity not tracked will use the second for timing`() {
134-
launchActivity(Robolectric.buildActivity(FakeSplashScreenActivity::class.java))
139+
val firstActivityInitTime = launchActivity(Robolectric.buildActivity(FakeSplashScreenActivity::class.java)).createTime
135140
clock.tick()
136141
with(launchActivity()) {
137142
assertEquals("android.app.Activity", dataCollector.startupActivityName)
138143
verifyLifecycle(
144+
firstActivityInitTime = firstActivityInitTime,
139145
preCreateTime = createTime,
140146
createTime = createTime,
141147
postCreateTime = createTime,
@@ -168,7 +174,7 @@ internal class StartupTrackerTest {
168174
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
169175
@Test
170176
fun `data only collected if activity is used as startup activity`() {
171-
launchActivity(Robolectric.buildActivity(FakeNotStartupActivity::class.java))
177+
launchActivity(Robolectric.buildActivity(FakeSplashScreenActivity::class.java))
172178
assertNull(dataCollector.applicationInitStartMs)
173179
assertNull(dataCollector.applicationInitEndMs)
174180
assertNull(dataCollector.startupActivityName)
@@ -206,6 +212,7 @@ internal class StartupTrackerTest {
206212
}
207213

208214
private fun launchActivity(controller: ActivityController<*> = defaultActivityController): ActivityTiming {
215+
clock.tick()
209216
val createTime = clock.now()
210217
controller.create()
211218
clock.tick()
@@ -227,13 +234,15 @@ internal class StartupTrackerTest {
227234
}
228235

229236
private fun verifyLifecycle(
237+
firstActivityInitTime: Long,
230238
preCreateTime: Long? = null,
231239
createTime: Long,
232240
postCreateTime: Long? = null,
233241
startTime: Long,
234242
resumeTime: Long,
235243
renderTime: Long? = null
236244
) {
245+
assertEquals(firstActivityInitTime, dataCollector.firstActivityInitMs)
237246
assertEquals(preCreateTime, dataCollector.startupActivityPreCreatedMs)
238247
assertEquals(createTime, dataCollector.startupActivityInitStartMs)
239248
assertEquals(postCreateTime, dataCollector.startupActivityPostCreatedMs)

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/AppStartupTraceTest.kt

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package io.embrace.android.embracesdk.testcases
22

33
import android.os.Build
44
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import io.embrace.android.embracesdk.fakes.FakeActivity
6+
import io.embrace.android.embracesdk.fakes.FakeSplashScreenActivity
57
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
68
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
79
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
@@ -15,12 +17,16 @@ import io.embrace.android.embracesdk.internal.spans.toStatus
1517
import io.embrace.android.embracesdk.spans.EmbraceSpanEvent
1618
import io.embrace.android.embracesdk.spans.ErrorCode
1719
import io.embrace.android.embracesdk.testframework.SdkIntegrationTestRule
20+
import io.embrace.android.embracesdk.testframework.actions.EmbraceActionInterface.Companion.ACTIVITY_GAP
21+
import io.embrace.android.embracesdk.testframework.actions.EmbraceActionInterface.Companion.LIFECYCLE_EVENT_GAP
22+
import io.embrace.android.embracesdk.testframework.actions.EmbraceActionInterface.Companion.POST_ACTIVITY_ACTION_DWELL
1823
import org.junit.Assert.assertEquals
1924
import org.junit.Assert.assertNotNull
2025
import org.junit.Assert.assertTrue
2126
import org.junit.Rule
2227
import org.junit.Test
2328
import org.junit.runner.RunWith
29+
import org.robolectric.Robolectric
2430
import org.robolectric.annotation.Config
2531

2632
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@@ -95,6 +101,125 @@ internal class AppStartupTraceTest {
95101
)
96102
}
97103

104+
@Test
105+
fun `warm startup`() {
106+
var startupActivityInitMs: Long? = null
107+
testRule.runTest(
108+
instrumentedConfig = FakeInstrumentedConfig(
109+
enabledFeatures = FakeEnabledFeatureConfig(
110+
bgActivityCapture = true
111+
)
112+
),
113+
testCaseAction = {
114+
val initGap = 10000L
115+
clock.tick(initGap)
116+
startupActivityInitMs = clock.now()
117+
simulateOpeningActivities(
118+
addStartupActivity = false
119+
)
120+
},
121+
otelExportAssertion = {
122+
val spans = awaitSpansWithType(3, EmbType.Performance.Default).associateBy { it.name }
123+
assertTrue(spans.isNotEmpty())
124+
with(checkNotNull(spans["emb-app-startup-warm"])) {
125+
assertEquals(startupActivityInitMs, startEpochNanos.nanosToMillis())
126+
}
127+
with(checkNotNull(spans["emb-activity-create"])) {
128+
assertEquals(startupActivityInitMs, startEpochNanos.nanosToMillis())
129+
}
130+
assertTrue(spans.containsKey("emb-activity-resume"))
131+
}
132+
)
133+
}
134+
135+
@Test
136+
fun `cold startup with long splash screen`() {
137+
var sdkStartTimeMs: Long? = null
138+
var firstActivityInitMs: Long? = null
139+
var startupActivityInitMs: Long? = null
140+
testRule.runTest(
141+
instrumentedConfig = FakeInstrumentedConfig(
142+
enabledFeatures = FakeEnabledFeatureConfig(
143+
bgActivityCapture = true
144+
)
145+
),
146+
testCaseAction = {
147+
val splashScreenDwellTime = 5000L
148+
sdkStartTimeMs = clock.now()
149+
firstActivityInitMs = clock.tick()
150+
startupActivityInitMs = clock.now() + (3 * LIFECYCLE_EVENT_GAP) + POST_ACTIVITY_ACTION_DWELL +
151+
ACTIVITY_GAP + splashScreenDwellTime
152+
simulateOpeningActivities(
153+
addStartupActivity = false,
154+
activitiesAndActions = listOf(
155+
Robolectric.buildActivity(FakeSplashScreenActivity::class.java) to {
156+
clock.tick(
157+
splashScreenDwellTime
158+
)
159+
},
160+
Robolectric.buildActivity(FakeActivity::class.java) to {},
161+
)
162+
)
163+
},
164+
otelExportAssertion = {
165+
val spans = awaitSpansWithType(5, EmbType.Performance.Default).associateBy { it.name }
166+
assertTrue(spans.isNotEmpty())
167+
assertTrue(spans.containsKey("emb-app-startup-cold"))
168+
assertTrue(spans.containsKey("emb-embrace-init"))
169+
with(checkNotNull(spans["emb-activity-init-gap"])) {
170+
assertEquals(sdkStartTimeMs, startEpochNanos.nanosToMillis())
171+
assertEquals(firstActivityInitMs, endEpochNanos.nanosToMillis())
172+
}
173+
with(checkNotNull(spans["emb-activity-create"])) {
174+
assertEquals(startupActivityInitMs, startEpochNanos.nanosToMillis())
175+
}
176+
assertTrue(spans.containsKey("emb-activity-resume"))
177+
}
178+
)
179+
}
180+
181+
@Test
182+
fun `warm startup with long splash screen`() {
183+
var firstActivityInitMs: Long? = null
184+
var startupActivityInitMs: Long? = null
185+
testRule.runTest(
186+
instrumentedConfig = FakeInstrumentedConfig(
187+
enabledFeatures = FakeEnabledFeatureConfig(
188+
bgActivityCapture = true
189+
)
190+
),
191+
testCaseAction = {
192+
val initGap = 10000L
193+
val splashScreenDwellTime = 5000L
194+
firstActivityInitMs = clock.tick(initGap)
195+
startupActivityInitMs = clock.now() + (3 * LIFECYCLE_EVENT_GAP) + POST_ACTIVITY_ACTION_DWELL +
196+
ACTIVITY_GAP + splashScreenDwellTime
197+
simulateOpeningActivities(
198+
addStartupActivity = false,
199+
activitiesAndActions = listOf(
200+
Robolectric.buildActivity(FakeSplashScreenActivity::class.java) to {
201+
clock.tick(
202+
splashScreenDwellTime
203+
)
204+
},
205+
Robolectric.buildActivity(FakeActivity::class.java) to {},
206+
)
207+
)
208+
},
209+
otelExportAssertion = {
210+
val spans = awaitSpansWithType(3, EmbType.Performance.Default).associateBy { it.name }
211+
assertTrue(spans.isNotEmpty())
212+
with(checkNotNull(spans["emb-app-startup-warm"])) {
213+
assertEquals(firstActivityInitMs, startEpochNanos.nanosToMillis())
214+
}
215+
with(checkNotNull(spans["emb-activity-create"])) {
216+
assertEquals(startupActivityInitMs, startEpochNanos.nanosToMillis())
217+
}
218+
assertTrue(spans.containsKey("emb-activity-resume"))
219+
}
220+
)
221+
}
222+
98223
@Test
99224
fun `applicationInitEnd call adds extra information`() {
100225
var applicationEndTimeMs: Long? = null

embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeAppStartupDataCollector.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class FakeAppStartupDataCollector(
1919
var applicationInitStartMs: Long? = null
2020
var applicationInitEndMs: Long? = null
2121
var startupActivityName: String? = null
22+
var firstActivityInitMs: Long? = null
2223
var startupActivityPreCreatedMs: Long? = null
2324
var startupActivityInitStartMs: Long? = null
2425
var startupActivityPostCreatedMs: Long? = null
@@ -36,6 +37,10 @@ class FakeAppStartupDataCollector(
3637
applicationInitEndMs = timestampMs ?: clock.now()
3738
}
3839

40+
override fun firstActivityInit(timestampMs: Long?) {
41+
firstActivityInitMs = timestampMs ?: clock.now()
42+
}
43+
3944
override fun startupActivityPreCreated(timestampMs: Long?) {
4045
startupActivityPreCreatedMs = timestampMs ?: clock.now()
4146
}

embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeNotStartupActivity.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)