Skip to content

Commit 6e33cf3

Browse files
committed
Implement cold start detection logic (#6975)
Implement cold start detection logic as described in https://docs.google.com/document/d/1yEd8dCIzwNwLhRRwkDtzWFx9vxV5d-eQU-P47wDL40k/edit?usp=sharing Also renamed process related functions to be more consistent to make the code less error prone. This is also described in the doc Tested by unit tests and with the sessions test app
1 parent 66e340e commit 6e33cf3

File tree

8 files changed

+137
-87
lines changed

8 files changed

+137
-87
lines changed

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : Pro
6363

6464
override val myUuid: String by lazy { uuidGenerator.next().toString() }
6565

66-
private val myProcessDetails by lazy {
67-
ProcessDetailsProvider.getCurrentProcessDetails(appContext)
68-
}
66+
private val myProcessDetails by lazy { ProcessDetailsProvider.getMyProcessDetails(appContext) }
6967

7068
private var hasGeneratedSession: Boolean = false
7169

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,9 @@ import android.os.Build
2323
import android.os.Process
2424
import com.google.android.gms.common.util.ProcessUtils
2525

26-
/**
27-
* Provider of ProcessDetails.
28-
*
29-
* @hide
30-
*/
26+
/** Provide [ProcessDetails] for all app processes. */
3127
internal object ProcessDetailsProvider {
32-
/** Gets the details for all of this app's running processes. */
28+
/** Gets the details for all the app's running processes. */
3329
fun getAppProcessDetails(context: Context): List<ProcessDetails> {
3430
val appUid = context.applicationInfo.uid
3531
val defaultProcessName = context.applicationInfo.processName
@@ -53,27 +49,19 @@ internal object ProcessDetailsProvider {
5349
}
5450

5551
/**
56-
* Gets this app's current process details.
52+
* Gets this process's details.
5753
*
58-
* If the current process details are not found for whatever reason, returns process details with
59-
* just the current process name and pid set.
54+
* If this process's full details are not found for whatever reason, returns process details with
55+
* just the process name and pid set.
6056
*/
61-
fun getCurrentProcessDetails(context: Context): ProcessDetails {
57+
fun getMyProcessDetails(context: Context): ProcessDetails {
6258
val pid = Process.myPid()
6359
return getAppProcessDetails(context).find { it.pid == pid }
64-
?: buildProcessDetails(getProcessName(), pid)
60+
?: ProcessDetails(getProcessName(), pid, importance = 0, isDefaultProcess = false)
6561
}
6662

67-
/** Builds a ProcessDetails object. */
68-
private fun buildProcessDetails(
69-
processName: String,
70-
pid: Int = 0,
71-
importance: Int = 0,
72-
isDefaultProcess: Boolean = false
73-
) = ProcessDetails(processName, pid, importance, isDefaultProcess)
74-
7563
/** Gets the app's current process name. If it could not be found, returns an empty string. */
76-
internal fun getProcessName(): String {
64+
private fun getProcessName(): String {
7765
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
7866
return Process.myProcessName()
7967
}

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ internal object SessionEvents {
8383
versionName = packageInfo.versionName ?: buildVersion,
8484
appBuildVersion = buildVersion,
8585
deviceManufacturer = Build.MANUFACTURER,
86-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext),
86+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext),
8787
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext),
8888
),
8989
)

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.google.firebase.sessions
1919
import android.util.Log
2020
import androidx.datastore.core.DataStore
2121
import com.google.firebase.annotations.concurrent.Background
22-
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
2322
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
2423
import com.google.firebase.sessions.api.SessionSubscriber
2524
import com.google.firebase.sessions.settings.SessionsSettings
@@ -92,7 +91,7 @@ constructor(
9291
return
9392
}
9493
val sessionData = localSessionData
95-
Log.d(TAG, "App backgrounded on ${getProcessName()} - $sessionData")
94+
Log.d(TAG, "App backgrounded on ${processDataManager.myProcessName} - $sessionData")
9695

9796
CoroutineScope(backgroundDispatcher).launch {
9897
try {
@@ -113,32 +112,58 @@ constructor(
113112
return
114113
}
115114
val sessionData = localSessionData
116-
Log.d(TAG, "App foregrounded on ${getProcessName()} - $sessionData")
115+
Log.d(TAG, "App foregrounded on ${processDataManager.myProcessName} - $sessionData")
117116

118-
if (shouldInitiateNewSession(sessionData)) {
117+
// Check if maybe the session data needs to be updated
118+
if (isSessionExpired(sessionData) || isMyProcessStale(sessionData)) {
119119
CoroutineScope(backgroundDispatcher).launch {
120120
try {
121121
sessionDataStore.updateData { currentSessionData ->
122-
// Double-check pattern
123-
if (shouldInitiateNewSession(currentSessionData)) {
122+
// Check again using the current session data on disk
123+
val isSessionExpired = isSessionExpired(currentSessionData)
124+
val isColdStart = isColdStart(currentSessionData)
125+
val isMyProcessStale = isMyProcessStale(currentSessionData)
126+
127+
val newProcessDataMap =
128+
if (isColdStart) {
129+
// Generate a new process data map for cold app start
130+
processDataManager.generateProcessDataMap()
131+
} else if (isMyProcessStale) {
132+
// Update the data map with this process if stale
133+
processDataManager.updateProcessDataMap(currentSessionData.processDataMap)
134+
} else {
135+
// No change
136+
currentSessionData.processDataMap
137+
}
138+
139+
// This is an expression, and returns the updated session data
140+
if (isSessionExpired || isColdStart) {
124141
val newSessionDetails =
125-
sessionGenerator.generateNewSession(sessionData.sessionDetails)
142+
sessionGenerator.generateNewSession(currentSessionData.sessionDetails)
126143
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
127144
processDataManager.onSessionGenerated()
128-
currentSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
145+
currentSessionData.copy(
146+
sessionDetails = newSessionDetails,
147+
backgroundTime = null,
148+
processDataMap = newProcessDataMap,
149+
)
150+
} else if (isMyProcessStale) {
151+
currentSessionData.copy(
152+
processDataMap = processDataManager.updateProcessDataMap(newProcessDataMap)
153+
)
129154
} else {
130155
currentSessionData
131156
}
132157
}
133158
} catch (ex: Exception) {
134159
Log.d(TAG, "App appForegrounded, failed to update data. Message: ${ex.message}")
135-
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
136-
localSessionData =
137-
localSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
138-
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
139-
140-
val sessionId = newSessionDetails.sessionId
141-
notifySubscribers(sessionId, NotificationType.FALLBACK)
160+
if (isSessionExpired(sessionData)) {
161+
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
162+
localSessionData =
163+
sessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
164+
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
165+
notifySubscribers(newSessionDetails.sessionId, NotificationType.FALLBACK)
166+
}
142167
}
143168
}
144169
}
@@ -161,22 +186,47 @@ constructor(
161186
}
162187
}
163188

164-
private fun shouldInitiateNewSession(sessionData: SessionData): Boolean {
189+
/** Checks if the session has expired. If no background time, consider it not expired. */
190+
private fun isSessionExpired(sessionData: SessionData): Boolean {
165191
sessionData.backgroundTime?.let { backgroundTime ->
166192
val interval = timeProvider.currentTime() - backgroundTime
167-
if (interval > sessionsSettings.sessionRestartTimeout) {
168-
Log.d(TAG, "Passed session restart timeout, so initiate a new session")
169-
return true
193+
val sessionExpired = (interval > sessionsSettings.sessionRestartTimeout)
194+
if (sessionExpired) {
195+
Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} is expired")
170196
}
197+
return sessionExpired
171198
}
172199

200+
Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} has not backgrounded yet")
201+
return false
202+
}
203+
204+
/** Checks for cold app start. If no process data map, consider it a cold start. */
205+
private fun isColdStart(sessionData: SessionData): Boolean {
173206
sessionData.processDataMap?.let { processDataMap ->
174-
Log.d(TAG, "Has not passed session restart timeout, so check for cold app start")
175-
return processDataManager.isColdStart(processDataMap)
207+
val coldStart = processDataManager.isColdStart(processDataMap)
208+
if (coldStart) {
209+
Log.d(TAG, "Cold app start detected")
210+
}
211+
return coldStart
176212
}
177213

178-
Log.d(TAG, "No process has backgrounded yet and no process data, should not change the session")
179-
return false
214+
Log.d(TAG, "No process data map")
215+
return true
216+
}
217+
218+
/** Checks if this process is stale. If no process data map, consider the process stale. */
219+
private fun isMyProcessStale(sessionData: SessionData): Boolean {
220+
sessionData.processDataMap?.let { processDataMap ->
221+
val myProcessStale = processDataManager.isMyProcessStale(processDataMap)
222+
if (myProcessStale) {
223+
Log.d(TAG, "Process ${processDataManager.myProcessName} is stale")
224+
}
225+
return myProcessStale
226+
}
227+
228+
Log.d(TAG, "No process data for ${processDataManager.myProcessName}")
229+
return true
180230
}
181231

182232
private companion object {

firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class ApplicationInfoTest {
3636
@Test
3737
fun applicationInfo_populatesInfoCorrectly() {
3838
val firebaseApp = FakeFirebaseApp().firebaseApp
39-
val actualCurrentProcessDetails =
40-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
41-
val actualAppProcessDetails =
39+
val myProcessDetails =
40+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
41+
val appProcessDetails =
4242
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)
4343
val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
4444
assertThat(applicationInfo)
@@ -54,8 +54,8 @@ class ApplicationInfoTest {
5454
versionName = FakeFirebaseApp.MOCK_APP_VERSION,
5555
appBuildVersion = FakeFirebaseApp.MOCK_APP_BUILD_VERSION,
5656
deviceManufacturer = Build.MANUFACTURER,
57-
actualCurrentProcessDetails,
58-
actualAppProcessDetails,
57+
myProcessDetails,
58+
appProcessDetails,
5959
),
6060
)
6161
)
@@ -74,9 +74,9 @@ class ApplicationInfoTest {
7474
.build(),
7575
)
7676

77-
val actualCurrentProcessDetails =
78-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
79-
val actualAppProcessDetails =
77+
val myProcessDetails =
78+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
79+
val appProcessDetails =
8080
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)
8181

8282
val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
@@ -94,8 +94,8 @@ class ApplicationInfoTest {
9494
versionName = "0",
9595
appBuildVersion = "0",
9696
deviceManufacturer = Build.MANUFACTURER,
97-
actualCurrentProcessDetails,
98-
actualAppProcessDetails,
97+
myProcessDetails,
98+
appProcessDetails,
9999
),
100100
)
101101
)

0 commit comments

Comments
 (0)