Skip to content

Implement cold start detection logic #6975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : Pro

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

private val myProcessDetails by lazy {
ProcessDetailsProvider.getCurrentProcessDetails(appContext)
}
private val myProcessDetails by lazy { ProcessDetailsProvider.getMyProcessDetails(appContext) }

private var hasGeneratedSession: Boolean = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,9 @@ import android.os.Build
import android.os.Process
import com.google.android.gms.common.util.ProcessUtils

/**
* Provider of ProcessDetails.
*
* @hide
*/
/** Provide [ProcessDetails] for all app processes. */
internal object ProcessDetailsProvider {
/** Gets the details for all of this app's running processes. */
/** Gets the details for all the app's running processes. */
fun getAppProcessDetails(context: Context): List<ProcessDetails> {
val appUid = context.applicationInfo.uid
val defaultProcessName = context.applicationInfo.processName
Expand All @@ -53,27 +49,19 @@ internal object ProcessDetailsProvider {
}

/**
* Gets this app's current process details.
* Gets this process's details.
*
* If the current process details are not found for whatever reason, returns process details with
* just the current process name and pid set.
* If this process's full details are not found for whatever reason, returns process details with
* just the process name and pid set.
*/
fun getCurrentProcessDetails(context: Context): ProcessDetails {
fun getMyProcessDetails(context: Context): ProcessDetails {
val pid = Process.myPid()
return getAppProcessDetails(context).find { it.pid == pid }
?: buildProcessDetails(getProcessName(), pid)
?: ProcessDetails(getProcessName(), pid, importance = 0, isDefaultProcess = false)
}

/** Builds a ProcessDetails object. */
private fun buildProcessDetails(
processName: String,
pid: Int = 0,
importance: Int = 0,
isDefaultProcess: Boolean = false
) = ProcessDetails(processName, pid, importance, isDefaultProcess)

/** Gets the app's current process name. If it could not be found, returns an empty string. */
internal fun getProcessName(): String {
private fun getProcessName(): String {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
return Process.myProcessName()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ internal object SessionEvents {
versionName = packageInfo.versionName ?: buildVersion,
appBuildVersion = buildVersion,
deviceManufacturer = Build.MANUFACTURER,
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext),
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext),
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.google.firebase.sessions
import android.util.Log
import androidx.datastore.core.DataStore
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.api.SessionSubscriber
import com.google.firebase.sessions.settings.SessionsSettings
Expand Down Expand Up @@ -92,7 +91,7 @@ constructor(
return
}
val sessionData = localSessionData
Log.d(TAG, "App backgrounded on ${getProcessName()} - $sessionData")
Log.d(TAG, "App backgrounded on ${processDataManager.myProcessName} - $sessionData")

CoroutineScope(backgroundDispatcher).launch {
try {
Expand All @@ -113,32 +112,57 @@ constructor(
return
}
val sessionData = localSessionData
Log.d(TAG, "App foregrounded on ${getProcessName()} - $sessionData")
Log.d(TAG, "App foregrounded on ${processDataManager.myProcessName} - $sessionData")

if (shouldInitiateNewSession(sessionData)) {
// Check if maybe the session data needs to be updated
if (isSessionExpired(sessionData) || isMyProcessStale(sessionData)) {
CoroutineScope(backgroundDispatcher).launch {
try {
sessionDataStore.updateData { currentSessionData ->
// Double-check pattern
if (shouldInitiateNewSession(currentSessionData)) {
val isSessionExpired = isSessionExpired(currentSessionData)
val isColdStart = isColdStart(currentSessionData)
val isMyProcessStale = isMyProcessStale(currentSessionData)

val newProcessDataMap =
if (isColdStart) {
// Generate a new process data map for cold app start
processDataManager.generateProcessDataMap()
} else if (isMyProcessStale) {
// Update the data map with this process if stale
processDataManager.updateProcessDataMap(currentSessionData.processDataMap)
} else {
// No change
currentSessionData.processDataMap
}

// This is an expression, and returns the updated session data
if (isSessionExpired || isColdStart) {
val newSessionDetails =
sessionGenerator.generateNewSession(sessionData.sessionDetails)
sessionGenerator.generateNewSession(currentSessionData.sessionDetails)
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
processDataManager.onSessionGenerated()
currentSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
currentSessionData.copy(
sessionDetails = newSessionDetails,
backgroundTime = null,
processDataMap = newProcessDataMap,
)
} else if (isMyProcessStale) {
currentSessionData.copy(
processDataMap = processDataManager.updateProcessDataMap(newProcessDataMap)
)
} else {
currentSessionData
}
}
} catch (ex: Exception) {
Log.d(TAG, "App appForegrounded, failed to update data. Message: ${ex.message}")
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
localSessionData =
localSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)

val sessionId = newSessionDetails.sessionId
notifySubscribers(sessionId, NotificationType.FALLBACK)
if (isSessionExpired(sessionData)) {
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
localSessionData =
sessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
notifySubscribers(newSessionDetails.sessionId, NotificationType.FALLBACK)
}
}
}
}
Expand All @@ -161,22 +185,47 @@ constructor(
}
}

private fun shouldInitiateNewSession(sessionData: SessionData): Boolean {
/** Checks if the session has expired. If no background time, consider it not expired. */
private fun isSessionExpired(sessionData: SessionData): Boolean {
sessionData.backgroundTime?.let { backgroundTime ->
val interval = timeProvider.currentTime() - backgroundTime
if (interval > sessionsSettings.sessionRestartTimeout) {
Log.d(TAG, "Passed session restart timeout, so initiate a new session")
return true
val sessionExpired = (interval > sessionsSettings.sessionRestartTimeout)
if (sessionExpired) {
Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} is expired")
}
return sessionExpired
}

Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} has not backgrounded yet")
return false
}

/** Checks for cold app start. If no process data map, consider it a cold start. */
private fun isColdStart(sessionData: SessionData): Boolean {
sessionData.processDataMap?.let { processDataMap ->
Log.d(TAG, "Has not passed session restart timeout, so check for cold app start")
return processDataManager.isColdStart(processDataMap)
val coldStart = processDataManager.isColdStart(processDataMap)
if (coldStart) {
Log.d(TAG, "Cold app start detected")
}
return coldStart
}

Log.d(TAG, "No process has backgrounded yet and no process data, should not change the session")
return false
Log.d(TAG, "No process data map")
return true
}

/** Checks if this process is stale. If no process data map, consider the process stale. */
private fun isMyProcessStale(sessionData: SessionData): Boolean {
sessionData.processDataMap?.let { processDataMap ->
val myProcessStale = processDataManager.isMyProcessStale(processDataMap)
if (myProcessStale) {
Log.d(TAG, "Process ${processDataManager.myProcessName} is stale")
}
return myProcessStale
}

Log.d(TAG, "No process data for ${processDataManager.myProcessName}")
return true
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ class ApplicationInfoTest {
@Test
fun applicationInfo_populatesInfoCorrectly() {
val firebaseApp = FakeFirebaseApp().firebaseApp
val actualCurrentProcessDetails =
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
val actualAppProcessDetails =
val myProcessDetails =
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
val appProcessDetails =
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)
val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
assertThat(applicationInfo)
Expand All @@ -54,8 +54,8 @@ class ApplicationInfoTest {
versionName = FakeFirebaseApp.MOCK_APP_VERSION,
appBuildVersion = FakeFirebaseApp.MOCK_APP_BUILD_VERSION,
deviceManufacturer = Build.MANUFACTURER,
actualCurrentProcessDetails,
actualAppProcessDetails,
myProcessDetails,
appProcessDetails,
),
)
)
Expand All @@ -74,9 +74,9 @@ class ApplicationInfoTest {
.build(),
)

val actualCurrentProcessDetails =
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
val actualAppProcessDetails =
val myProcessDetails =
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
val appProcessDetails =
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)

val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
Expand All @@ -94,8 +94,8 @@ class ApplicationInfoTest {
versionName = "0",
appBuildVersion = "0",
deviceManufacturer = Build.MANUFACTURER,
actualCurrentProcessDetails,
actualAppProcessDetails,
myProcessDetails,
appProcessDetails,
),
)
)
Expand Down
Loading
Loading