Skip to content
Open
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
88 changes: 88 additions & 0 deletions papa/src/main/java/papa/AppLaunch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package papa

/*

TODO Plan to make this more usable:

- Move it out of PapaEvent into its own dedicated thing
- Potentially split out from all the existing app start data gathering, which allows moving it into
its own artifact
- Remove savedInstanceState, this should instead become "the list of activities involved in the
launch" and for each activity we can have savedInstanceState
- We should capture timings for each activity.
- Remove the cold vs warm vs hot launch type, which instead should be determined live based on
launch attributes. This allows anyone to make different determinations. For each activity involved
in the launch we can capture its state transitions. E.g. initial states and all states until it
launch end. Alternatively this could be tracked as a history instead of a list of activity
("activity A created with state, then destroyed then activity B created no state"). Then "hot" is
just "a single activity was resumed". Then we can have "did the launch involve a process start", which
has data about whether it was a first install etc, and also whether the process importance was not 100.
Though actually this is more "did the activity launch happen after first post or not". And in the
"not" case there's information about the process importance at startup.

- trampolined becomes "activities.size > 1"
- API for activities to call up until on resume and say "hey I'm in a launch please keep waiting
until I tell you I'm done loading". And capture the next frame after that. Basically a useful version
of reportFullyDrawn.
*/
/**
* The app came was in the background and came to the foreground. In practice this means the app
* had 0 activity in started or resumed state, and now has at least one activity in resumed state.
*
* Going from "paused" back to "resumed" isn't considered a foregrounding here. This is
* an intentional decision, as a paused but not stopped activity is still visible and rendering.
*
* Currently we report an [AppLaunch] if at any point in time we go from 1 to 0 to 1 resumed
* activity. This can lead to false positives (e.g. when finishing one activity restarts a new
* activity stack) or shorter app launches (when using incorrectly written Trampoline activities).
*/
class AppLaunch(
val launchType: LaunchType,

/**
* The app launch duration, also known as the Time To Initial Display (TTID), is the time it
* takes for an application to produce its first frame, including process initialization (if a
* cold start), activity creation (if cold/warm), and displaying first frame.
*
* https://developer.android.com/topic/performance/vitals/launch-time#time-initial
*/
val durationUptimeMillis: Long,

/**
* True if more than one activity was launched as part of this launch. Trampoline activities
* are a common pattern where the launcher activity immediately starts another activity based
* on runtime conditions (for example whether the user is logged in or not).
* Note that trampoline activities should always start the next activity in their onCreate()
* method, otherwise the first frame rendered will show the trampoline activity instead of the
* target activity (in that case, the launch end will be measured at that first frame and
* [trampolined] will actually be false)
*/
val trampolined: Boolean,

/**
* The elapsed real time millis duration the app spent invisible, or null if the app has
* never been visible before. This is the elapsed real time duration from when the app
* became invisible until the start of this launch.
*/
val invisibleDurationRealtimeMillis: Long?,

val startUptimeMillis: Long,
) : PapaEvent() {
/**
* Whether this launch will be considered a slow launch by the Play Store and is likely to
* be reported as "bad behavior".
*/
val isSlowLaunch: Boolean
get() = durationUptimeMillis >= launchType.slowThresholdMillis

override fun toString(): String {
return "AppLaunch(" +
"launchType=$launchType, " +
"duration=$durationUptimeMillis ms, " +
"isSlowLaunch=$isSlowLaunch, " +
"trampolined=$trampolined, " +
"backgroundDuration=$invisibleDurationRealtimeMillis ms, " +
"startUptimeMillis=$startUptimeMillis" +
")"
}
}
28 changes: 0 additions & 28 deletions papa/src/main/java/papa/AppLaunchType.kt

This file was deleted.

93 changes: 93 additions & 0 deletions papa/src/main/java/papa/LaunchType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package papa

/**
* Also see https://developer.android.com/topic/performance/vitals/launch-time
*/
sealed class LaunchType {

abstract val slowThresholdMillis: Long

/**
* The process was started with a FOREGROUND importance and
* the launched activity was created, started and resumed before our first post
* ran.
* Source: https://support.google.com/googleplay/android-developer/answer/9844486
*/
class Cold(
val preLaunchState: PreLaunchState
) : LaunchType() {
override val slowThresholdMillis = 5000L

enum class PreLaunchState {
NORMAL,
/**
* Same as [NORMAL] but this was the first launch ever,
* which might trigger first launch additional work.
*/
FIRST_LAUNCH_AFTER_INSTALL,
/**
* Same as [NORMAL] but this was the first launch after the app was upgraded, which might
* trigger additional migration work. Note that if the upgrade if the first upgrade
* that introduces this library, the value will be [FIRST_LAUNCH_AFTER_CLEAR_DATA]
* instead.
*/
FIRST_LAUNCH_AFTER_UPGRADE,
/**
* Same as [NORMAL] but this was either the first launch after a clear data, or
* this was the first launch after the upgrade that introduced this library.
*/
FIRST_LAUNCH_AFTER_CLEAR_DATA
}

override fun toString(): String {
return "Cold(preLaunchState=$preLaunchState)"
}
}

/**
* A warm starts is tracked when the application creates and resumes an activity, if the
* application process had previously already been started (i.e. this is not a cold start) and
* there were no activity in created state (the application was in background and came to the
* foreground)
*
* A warm start encompasses a subset of the operations that take place during a cold start; There
* are many potential states that could be considered warm starts.
* Source: https://support.google.com/googleplay/android-developer/answer/9844486
*
* [savedInstanceState] if true, the task can benefit somewhat from the saved instance state
* bundle passed into onCreate().
*
* [processWasLaunchingInBackground] If true, this is the coldest type of "warm start". The
* process was not started with a FOREGROUND importance yet the launched activity was created,
* started and resumed before our first post ran. This means that while the process while
* starting, the system decided to launch the activity.
*/
// TODO A launch can involve a series of activities. We might want to move savedInstanceState
// into a list of activity details.
class Warm(val savedInstanceState: Boolean, val processWasLaunchingInBackground: Boolean) :
LaunchType() {
override val slowThresholdMillis = 2000L
override fun toString(): String {
return "Warm(savedInstanceState=$savedInstanceState, processWasLaunchingInBackground=$processWasLaunchingInBackground)"
}
}

/**
* This is a "hot start", the activity was already created and had been stopped when the app
* went in background. Bringing it to the foreground means the activity was started and then
* resumed. Note that there isn't a "ACTIVITY_WAS_PAUSED" entry here. We do not consider
* going from PAUSE to RESUME to be a launch because the activity was still visible so there
* is nothing to redraw on resume.
* A hot start is a warm start where all the system does is bring a stopped activity to the
* foreground.
* Note: https://developer.android.com/topic/performance/vitals/launch-time#av reports
* 1.5 seconds and https://support.google.com/googleplay/android-developer/answer/9844486 reports
* 1 second.
*/
class Hot : LaunchType() {
override val slowThresholdMillis = 1000L
override fun toString(): String {
return "Hot"
}
}
}
63 changes: 0 additions & 63 deletions papa/src/main/java/papa/PapaEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,6 @@ package papa
import papa.PapaEvent.FrozenFrameOnTouch.Companion.FROZEN_FRAME_THRESHOLD

sealed class PapaEvent {

/**
* The app came was in the background and came to the foreground. In practice this means the app
* had 0 activity in started or resumed state, and now has at least one activity in resumed state.
*
* Going from "paused" back to "resumed" isn't considered a foregrounding here. This is
* an intentional decision, as a paused by not stopped activity is still visible and rendering.
*
* Currently we report an [AppLaunch] if at any point in time we go from 1 to 0 to 1 resumed
* activity. This can lead to false positives (e.g. when finishing one activity restarts a new
* activity stack) or shorter app launches (when using incorrectly written Trampoline activities).
*/
class AppLaunch(
val preLaunchState: PreLaunchState,

/**
* The app launch duration, also known as the Time To Initial Display (TTID), is the time it
* takes for an application to produce its first frame, including process initialization (if a
* cold start), activity creation (if cold/warm), and displaying first frame.
*
* https://developer.android.com/topic/performance/vitals/launch-time#time-initial
*/
val durationUptimeMillis: Long,

/**
* True if more than one activity was launched as part of this launch. Trampoline activities
* are a common pattern where the launcher activity immediately starts another activity based
* on runtime conditions (for example whether the user is logged in or not).
* Note that trampoline activities should always start the next activity in their onCreate()
* method, otherwise the first frame rendered will show the trampoline activity instead of the
* target activity (in that case, the launch end will be measured at that first frame and
* [trampolined] will actually be false)
*/
val trampolined: Boolean,

/**
* The elapsed real time millis duration the app spent invisible, or null if the app has
* never been visible before. This is the elapsed real time duration from when the app
* became invisible until the start of this launch.
*/
val invisibleDurationRealtimeMillis: Long?,

val startUptimeMillis: Long,
) : PapaEvent() {
/**
* Whether this launch will be considered a slow launch by the Play Store and is likely to
* be reported as "bad behavior".
*/
val isSlowLaunch: Boolean
get() = durationUptimeMillis >= preLaunchState.launchType.slowThresholdMillis

override fun toString(): String {
return "AppLaunch(" +
"preLaunchState=$preLaunchState, " +
"duration=$durationUptimeMillis ms, " +
"isSlowLaunch=$isSlowLaunch, " +
"trampolined=$trampolined, " +
"backgroundDuration=$invisibleDurationRealtimeMillis ms, " +
"startUptimeMillis=$startUptimeMillis" +
")"
}
}

/**
* Event sent when there was more than [FROZEN_FRAME_THRESHOLD] of uptime between when a touch down
* event was sent by the display and the next frame after it was handled.
Expand Down
72 changes: 0 additions & 72 deletions papa/src/main/java/papa/PreLaunchState.kt

This file was deleted.

1 change: 1 addition & 0 deletions papa/src/main/java/papa/internal/ApplicationHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal object ApplicationHolder {
fun install(application: Application, isForegroundImportance: Boolean) {
this.application = application
SafeTraceSetup.init(application)
// TODO Look into setting Perfs.isTracingLaunch = true, probs forgot.
if (isForegroundImportance) {
SafeTrace.beginAsyncSection(Perfs.LAUNCH_TRACE_NAME)
}
Expand Down
11 changes: 11 additions & 0 deletions papa/src/main/java/papa/internal/LaunchTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ internal class LaunchTracker(
mainHandler.post(updateLastLifecycleChangeUptimeMillis)
}

/**
* No more timeout once we've entered onResume(), we'll wait for the first
* frame forever and consider that one launch.
* If we get any other lifecycle callback after this, we'll reinstate the timeout.
*/
fun clearLastLifecycleChangeTimeOnResume() {
lastLifecycleChangeDoneUptimeMillis = null
mainHandler.removeCallbacks(updateLastLifecycleChangeUptimeMillis)
}

/**
* Stale if the last lifecycle update for the launch in progress was done more than 500ms
* ago.
Expand Down Expand Up @@ -96,6 +106,7 @@ internal class LaunchTracker(
if (launchInProgress == null) {
return
}
launchInProgress!!.clearLastLifecycleChangeTimeOnResume()

// We're ending the launch of first frame post draw of this activity. If the activity ends up
// not drawing and another activity is resumed immediately after, whichever activity draws
Expand Down
Loading