Skip to content

Implement ProcessDataManager #6961

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 2 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -23,32 +23,48 @@ import javax.inject.Singleton

/** Manage process data, used for detecting cold app starts. */
internal interface ProcessDataManager {
/** An in-memory uuid to uniquely identify this instance of this process. */
/** This process's name. */
val myProcessName: String

/** This process's pid. */
val myPid: Int

/** An in-memory uuid to uniquely identify this instance of this process, not the uid. */
val myUuid: String

/** Checks if this is a cold app start, meaning all processes in the mapping table are stale. */
fun isColdStart(processDataMap: Map<String, ProcessData>): Boolean

/** Checks if this process is stale. */
fun isMyProcessStale(processDataMap: Map<String, ProcessData>): Boolean

/** Call to notify the process data manager that a session has been generated. */
fun onSessionGenerated()

/** Update the mapping of the current processes with data about this process. */
fun updateProcessDataMap(processDataMap: Map<String, ProcessData>?): Map<String, ProcessData>

/** Generate a new mapping of process data with the current process only. */
fun generateProcessDataMap() = updateProcessDataMap(mapOf())
/** Generate a new mapping of process data about this process only. */
fun generateProcessDataMap(): Map<String, ProcessData> = updateProcessDataMap(emptyMap())
}

/** Manage process data, used for detecting cold app starts. */
@Singleton
internal class ProcessDataManagerImpl
@Inject
constructor(private val appContext: Context, private val uuidGenerator: UuidGenerator) :
ProcessDataManager {
constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : ProcessDataManager {
/**
* This process's name.
*
* This value is cached, so will not reflect changes to the process name during runtime.
*/
override val myProcessName: String by lazy { myProcessDetails.processName }

override val myPid = Process.myPid()

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

private val myProcessName: String by lazy {
ProcessDetailsProvider.getCurrentProcessDetails(appContext).processName
private val myProcessDetails by lazy {
ProcessDetailsProvider.getCurrentProcessDetails(appContext)
}

private var hasGeneratedSession: Boolean = false
Expand All @@ -59,7 +75,8 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene
return false
}

return ProcessDetailsProvider.getAppProcessDetails(appContext)
// A cold start is when all app processes are stale
return getAppProcessDetails()
.mapNotNull { processDetails ->
processDataMap[processDetails.processName]?.let { processData ->
Pair(processDetails, processData)
Expand All @@ -68,6 +85,11 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene
.all { (processDetails, processData) -> isProcessStale(processDetails, processData) }
}

override fun isMyProcessStale(processDataMap: Map<String, ProcessData>): Boolean {
val myProcessData = processDataMap[myProcessName] ?: return true
return myProcessData.pid != myPid || myProcessData.uuid != myUuid
}

override fun onSessionGenerated() {
hasGeneratedSession = true
}
Expand All @@ -81,17 +103,22 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene
?.toMap()
?: mapOf(myProcessName to ProcessData(Process.myPid(), myUuid))

/** Gets the current details for all of the app's running processes. */
private fun getAppProcessDetails() = ProcessDetailsProvider.getAppProcessDetails(appContext)

/**
* Returns true if the process is stale, meaning the persisted process data does not match the
* running process details.
*
* The [processDetails] is the running process details, and [processData] is the persisted data.
*/
private fun isProcessStale(
runningProcessDetails: ProcessDetails,
persistedProcessData: ProcessData,
): Boolean =
if (myProcessName == runningProcessDetails.processName) {
runningProcessDetails.pid != persistedProcessData.pid || myUuid != persistedProcessData.uuid
private fun isProcessStale(processDetails: ProcessDetails, processData: ProcessData): Boolean =
if (myProcessName == processDetails.processName) {
// For this process, check pid and uuid
processDetails.pid != processData.pid || myUuid != processData.uuid
} else {
runningProcessDetails.pid != persistedProcessData.pid
// For other processes, only check pid to avoid inter-process communication
// It is very unlikely for there to be a pid collision
processDetails.pid != processData.pid
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,53 @@ internal class ProcessDataManagerTest {
assertThat(coldStart).isFalse()
}

@Test
fun isMyProcessStale() {
val appContext =
FakeFirebaseApp(processes = listOf(myProcessInfo)).firebaseApp.applicationContext
val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1))

val myProcessStale =
processDataManager.isMyProcessStale(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1)))

assertThat(myProcessStale).isFalse()
}

@Test
fun isMyProcessStale_otherProcessCurrent() {
val appContext =
FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo))
.firebaseApp
.applicationContext
val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1))

val myProcessStale =
processDataManager.isMyProcessStale(
mapOf(
MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1),
OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2),
)
)

assertThat(myProcessStale).isTrue()
}

@Test
fun isMyProcessStale_missingMyProcessData() {
val appContext =
FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo))
.firebaseApp
.applicationContext
val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1))

val myProcessStale =
processDataManager.isMyProcessStale(
mapOf(OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2))
)

assertThat(myProcessStale).isTrue()
}

@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import com.google.firebase.sessions.ProcessDataManager
*/
internal class FakeProcessDataManager(
private val coldStart: Boolean = false,
private var myProcessStale: Boolean = false,
override val myProcessName: String = "com.google.firebase.sessions.test",
override var myPid: Int = 0,
override var myUuid: String = FakeUuidGenerator.UUID_1,
) : ProcessDataManager {
private var hasGeneratedSession: Boolean = false
Expand All @@ -33,9 +36,12 @@ internal class FakeProcessDataManager(
if (hasGeneratedSession) {
return false
}

return coldStart
}

override fun isMyProcessStale(processDataMap: Map<String, ProcessData>): Boolean = myProcessStale

override fun onSessionGenerated() {
hasGeneratedSession = true
}
Expand Down
Loading