Skip to content

Commit

Permalink
feat(android): add lukewarm launch
Browse files Browse the repository at this point in the history
  • Loading branch information
abhaysood committed Sep 19, 2024
1 parent 544dc9d commit d8ee17f
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ internal class TestMeasureInitializer(
private val procProvider: ProcProvider = ProcProviderImpl(),
private val debugProvider: DebugProvider = DefaultDebugProvider(),
private val runtimeProvider: RuntimeProvider = DefaultRuntimeProvider(),
private val osSysConfProvider: OsSysConfProvider = OsSysConfProviderImpl(),
private val memoryReader: MemoryReader = DefaultMemoryReader(
logger = logger,
processInfo = processInfoProvider,
procProvider = procProvider,
debugProvider = debugProvider,
runtimeProvider = runtimeProvider,
osSysConfProvider = osSysConfProvider,
),
private val localeProvider: LocaleProvider = LocaleProviderImpl(),
private val prefsStorage: PrefsStorage = PrefsStorageImpl(context = application),
Expand Down Expand Up @@ -166,6 +168,7 @@ internal class TestMeasureInitializer(
logger,
context = application,
localeProvider = localeProvider,
osSysConfProvider = osSysConfProvider,
),
private val appAttributeProcessor: AppAttributeProcessor = AppAttributeProcessor(
context = application,
Expand Down Expand Up @@ -243,7 +246,6 @@ internal class TestMeasureInitializer(
processInfoProvider = processInfoProvider,
),
override val periodicEventExporter: PeriodicEventExporter = NoopPeriodicEventExporter(),
private val osSysConfProvider: OsSysConfProvider = OsSysConfProviderImpl(),
override val unhandledExceptionCollector: UnhandledExceptionCollector = UnhandledExceptionCollector(
logger = logger,
timeProvider = timeProvider,
Expand Down Expand Up @@ -306,7 +308,6 @@ internal class TestMeasureInitializer(
application = application,
eventProcessor = eventProcessor,
timeProvider = timeProvider,
processInfo = processInfoProvider,
),
override val networkChangesCollector: NetworkChangesCollector = NetworkChangesCollector(
logger = logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ internal class MeasureInitializerImpl(
application = application,
eventProcessor = eventProcessor,
timeProvider = timeProvider,
processInfo = processInfoProvider,
),
override val networkChangesCollector: NetworkChangesCollector = NetworkChangesCollector(
logger = logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import sh.measure.android.events.EventProcessor
import sh.measure.android.events.EventType
import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.utils.ProcessInfoProvider
import sh.measure.android.utils.TimeProvider

internal interface ColdLaunchListener {
Expand All @@ -20,15 +19,14 @@ internal class AppLaunchCollector(
private val application: Application,
private val timeProvider: TimeProvider,
private val eventProcessor: EventProcessor,
private val processInfo: ProcessInfoProvider,
) : LaunchCallbacks {

private var coldLaunchListener: ColdLaunchListener? = null

fun register() {
logger.log(LogLevel.Debug, "Registering AppLaunchCollector")
application.registerActivityLifecycleCallbacks(
LaunchTracker(logger, processInfo, this),
LaunchTracker(logger, this),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ internal data class ColdLaunchData(

@Serializable
internal data class WarmLaunchData(
/**
* The start time captured using [Process.getStartUptimeMillis].
*/
val process_start_uptime: Long?,

/**
* The start time captured using [Process.getStartRequestedUptimeMillis].
*/
val process_start_requested_uptime: Long?,

/**
* The start time captured using [MeasureInitProvider.attachInfo].
*/
val content_provider_attach_uptime: Long?,

/**
* The time at which the app became visible to the user.
*/
Expand All @@ -69,6 +84,11 @@ internal data class WarmLaunchData(
* The Intent data used to launch the [launched_activity].
*/
var intent_data: String?,

/**
* Whether the warm launch is actually a lukewarm launch.
*/
var is_lukewarm: Boolean,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.os.Process
* This information is used by [AppLaunchCollector] to calculate the cold launch time.
*/
internal object LaunchState {
var processImportanceOnInit: Int? = null
var contentLoaderAttachUptime: Long? = null
var lastAppVisibleTime: Long? = null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sh.measure.android.applaunch

import android.app.Activity
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
import android.os.Bundle
import android.os.SystemClock
import curtains.onNextDraw
Expand All @@ -9,7 +10,6 @@ import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.mainHandler
import sh.measure.android.postAtFrontOfQueueAsync
import sh.measure.android.utils.ProcessInfoProvider

internal interface LaunchCallbacks {
fun onColdLaunch(coldLaunchData: ColdLaunchData)
Expand All @@ -23,7 +23,6 @@ internal interface LaunchCallbacks {
*/
internal class LaunchTracker(
private val logger: Logger,
private val processInfo: ProcessInfoProvider,
private val callbacks: LaunchCallbacks,
) : ActivityLifecycleAdapter {

Expand Down Expand Up @@ -141,19 +140,34 @@ internal class LaunchTracker(
}

"Warm" -> {
LaunchState.lastAppVisibleTime?.let {
callbacks.onWarmLaunch(
WarmLaunchData(
app_visible_uptime = it,
on_next_draw_uptime = onNextDrawUptime,
launched_activity = onCreateRecord.activityName,
has_saved_state = onCreateRecord.hasSavedState,
intent_data = onCreateRecord.intentData,
),
)
} ?: logger.log(
LogLevel.Error,
"lastAppVisibleTime is null, cannot calculate warm launch time",
callbacks.onWarmLaunch(
WarmLaunchData(
process_start_uptime = LaunchState.processStartUptime,
process_start_requested_uptime = LaunchState.processStartRequestedUptime,
content_provider_attach_uptime = LaunchState.contentLoaderAttachUptime,
app_visible_uptime = LaunchState.lastAppVisibleTime ?: 0,
on_next_draw_uptime = onNextDrawUptime,
launched_activity = onCreateRecord.activityName,
has_saved_state = onCreateRecord.hasSavedState,
intent_data = onCreateRecord.intentData,
is_lukewarm = false,
),
)
}

"Lukewarm" -> {
callbacks.onWarmLaunch(
WarmLaunchData(
process_start_uptime = LaunchState.processStartUptime,
process_start_requested_uptime = LaunchState.processStartRequestedUptime,
content_provider_attach_uptime = LaunchState.contentLoaderAttachUptime,
app_visible_uptime = LaunchState.lastAppVisibleTime ?: 0,
on_next_draw_uptime = onNextDrawUptime,
launched_activity = onCreateRecord.activityName,
has_saved_state = onCreateRecord.hasSavedState,
intent_data = onCreateRecord.intentData,
is_lukewarm = true,
),
)
}

Expand Down Expand Up @@ -191,19 +205,25 @@ internal class LaunchTracker(
}
}

// Cold launch hasn't completed yet.
// However, the activity has a saved state, so it must be a warm launch. The process
// was recreated but the system still retained some state. This is not a cold launch as
// the process didn't really start from scratch.
onCreateRecord.hasSavedState -> "Warm"

processInfo.isForegroundProcess() -> "Cold"

// While the process was starting in background the system must have decided to create
// the activity and it got resumed. This is not a cold start as the system likely got a
// chance to warm up before the activity was created. Sadly the system doesn't tell us
// when it decided to do so, the data for this can be noisy.
else -> "Warm"
// This could have been a cold launch, but the activity was created with a saved state.
// Which reflects that the app was previously alive but the system evicted it from
// memory, but still kept the saved state. This is a "lukewarm" launch as the activity
// will still be created from scratch. It's not a cold launch as the system can benefit
// from the saved state.
LaunchState.processImportanceOnInit == IMPORTANCE_FOREGROUND && onCreateRecord.hasSavedState -> "Lukewarm"

// This is clearly a cold launch as the process was started with a foreground importance
// and does not have a saved state.
LaunchState.processImportanceOnInit == IMPORTANCE_FOREGROUND -> "Cold"

// This is a case where activity was created and resumed, but the app was
// not launched with a foreground importance. The system started the app without
// foreground importance but decided to change it's mind later. We track this as a
// lukewarm launch as the system got a chance to warm up before deciding to bring the
// activity to the foreground. Sadly we do not know when the system changed it's mind, so
// we just use the same launch time as a cold launch. We cannot rely on
// app_visible_uptime as it won't be set in this case.
else -> "Lukewarm"
}
}
}

0 comments on commit d8ee17f

Please sign in to comment.