-
-
Notifications
You must be signed in to change notification settings - Fork 465
fix(android): Improve app start type detection with main thread timing #4999
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
base: main
Are you sure you want to change the base?
Changes from all commits
e7047cb
4164619
70879c1
1547b1f
acf7e58
b0722d3
7034d95
ef2b3bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,11 @@ | |
| import android.app.Activity; | ||
| import android.app.Application; | ||
| import android.content.ContentProvider; | ||
| import android.os.Build; | ||
| import android.os.Bundle; | ||
| import android.os.Handler; | ||
| import android.os.Looper; | ||
| import android.os.MessageQueue; | ||
| import android.os.SystemClock; | ||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.Nullable; | ||
|
|
@@ -21,6 +23,7 @@ | |
| import io.sentry.android.core.SentryAndroidOptions; | ||
| import io.sentry.android.core.internal.util.FirstDrawDoneListener; | ||
| import io.sentry.util.AutoClosableReentrantLock; | ||
| import io.sentry.util.LazyEvaluator; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
|
|
@@ -56,7 +59,15 @@ public enum AppStartType { | |
| new AutoClosableReentrantLock(); | ||
|
|
||
| private @NotNull AppStartType appStartType = AppStartType.UNKNOWN; | ||
| private boolean appLaunchedInForeground; | ||
| private final LazyEvaluator<Boolean> appLaunchedInForeground = | ||
| new LazyEvaluator<>( | ||
| new LazyEvaluator.Evaluator<Boolean>() { | ||
| @Override | ||
| public @NotNull Boolean evaluate() { | ||
| return ContextUtils.isForegroundImportance(); | ||
| } | ||
| }); | ||
| private volatile long firstIdle = -1; | ||
|
|
||
| private final @NotNull TimeSpan appStartSpan; | ||
| private final @NotNull TimeSpan sdkInitTimeSpan; | ||
|
|
@@ -89,7 +100,6 @@ public AppStartMetrics() { | |
| applicationOnCreate = new TimeSpan(); | ||
| contentProviderOnCreates = new HashMap<>(); | ||
| activityLifecycles = new ArrayList<>(); | ||
| appLaunchedInForeground = ContextUtils.isForegroundImportance(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -140,12 +150,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) { | |
| } | ||
|
|
||
| public boolean isAppLaunchedInForeground() { | ||
| return appLaunchedInForeground; | ||
| return appLaunchedInForeground.getValue(); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) { | ||
| this.appLaunchedInForeground = appLaunchedInForeground; | ||
| this.appLaunchedInForeground.setValue(appLaunchedInForeground); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -176,7 +186,7 @@ public void onAppStartSpansSent() { | |
| } | ||
|
|
||
| public boolean shouldSendStartMeasurements() { | ||
| return shouldSendStartMeasurements && appLaunchedInForeground; | ||
| return shouldSendStartMeasurements && appLaunchedInForeground.getValue(); | ||
| } | ||
|
|
||
| public long getClassLoadedUptimeMs() { | ||
|
|
@@ -191,7 +201,7 @@ public long getClassLoadedUptimeMs() { | |
| final @NotNull SentryAndroidOptions options) { | ||
| // If the app start type was never determined or app wasn't launched in foreground, | ||
| // the app start is considered invalid | ||
| if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground) { | ||
| if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground.getValue()) { | ||
| if (options.isEnablePerformanceV2()) { | ||
| // Only started when sdk version is >= N | ||
| final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); | ||
|
|
@@ -212,6 +222,16 @@ public long getClassLoadedUptimeMs() { | |
| return new TimeSpan(); | ||
| } | ||
|
|
||
| @TestOnly | ||
| void setFirstIdle(final long firstIdle) { | ||
| this.firstIdle = firstIdle; | ||
| } | ||
|
|
||
| @TestOnly | ||
| long getFirstIdle() { | ||
| return firstIdle; | ||
| } | ||
|
|
||
| @TestOnly | ||
| public void clear() { | ||
| appStartType = AppStartType.UNKNOWN; | ||
|
|
@@ -229,11 +249,12 @@ public void clear() { | |
| } | ||
| appStartContinuousProfiler = null; | ||
| appStartSamplingDecision = null; | ||
| appLaunchedInForeground = false; | ||
| appLaunchedInForeground.resetValue(); | ||
| isCallbackRegistered = false; | ||
| shouldSendStartMeasurements = true; | ||
| firstDrawDone.set(false); | ||
| activeActivitiesCounter.set(0); | ||
| firstIdle = -1; | ||
| } | ||
|
|
||
| public @Nullable ITransactionProfiler getAppStartProfiler() { | ||
|
|
@@ -301,7 +322,8 @@ public static void onApplicationPostCreate(final @NotNull Application applicatio | |
| } | ||
|
|
||
| /** | ||
| * Register a callback to check if an activity was started after the application was created | ||
| * Register a callback to check if an activity was started after the application was created. Must | ||
| * be called from the main thread. | ||
| * | ||
| * @param application The application object to register the callback to | ||
| */ | ||
|
|
@@ -310,61 +332,85 @@ public void registerLifecycleCallbacks(final @NotNull Application application) { | |
| return; | ||
| } | ||
| isCallbackRegistered = true; | ||
| appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance(); | ||
| appLaunchedInForeground.resetValue(); | ||
| application.registerActivityLifecycleCallbacks(instance); | ||
| // We post on the main thread a task to post a check on the main thread. On Pixel devices | ||
| // (possibly others) the first task posted on the main thread is called before the | ||
| // Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate | ||
| // callback is called before the application one. | ||
| new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain()); | ||
|
|
||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| Looper.getMainLooper() | ||
| .getQueue() | ||
| .addIdleHandler( | ||
| new MessageQueue.IdleHandler() { | ||
| @Override | ||
| public boolean queueIdle() { | ||
| firstIdle = SystemClock.uptimeMillis(); | ||
| checkCreateTimeOnMain(); | ||
| return false; | ||
| } | ||
| }); | ||
| } else { | ||
| // We post on the main thread a task to post a check on the main thread. On Pixel devices | ||
| // (possibly others) the first task posted on the main thread is called before the | ||
| // Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate | ||
| // callback is called before the application one. | ||
| final Handler handler = new Handler(Looper.getMainLooper()); | ||
| handler.post( | ||
| new Runnable() { | ||
| @Override | ||
| public void run() { | ||
| // not technically correct, but close enough for pre-M | ||
| firstIdle = SystemClock.uptimeMillis(); | ||
| handler.post(() -> checkCreateTimeOnMain()); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private void checkCreateTimeOnMain() { | ||
| new Handler(Looper.getMainLooper()) | ||
| .post( | ||
| () -> { | ||
| // if no activity has ever been created, app was launched in background | ||
| if (activeActivitiesCounter.get() == 0) { | ||
| appLaunchedInForeground = false; | ||
|
|
||
| // we stop the app start profilers, as they are useless and likely to timeout | ||
| if (appStartProfiler != null && appStartProfiler.isRunning()) { | ||
| appStartProfiler.close(); | ||
| appStartProfiler = null; | ||
| } | ||
| if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) { | ||
| appStartContinuousProfiler.close(true); | ||
| appStartContinuousProfiler = null; | ||
| } | ||
| } | ||
| }); | ||
| // if no activity has ever been created, app was launched in background | ||
| if (activeActivitiesCounter.get() == 0) { | ||
| appLaunchedInForeground.setValue(false); | ||
|
|
||
| // we stop the app start profilers, as they are useless and likely to timeout | ||
| if (appStartProfiler != null && appStartProfiler.isRunning()) { | ||
| appStartProfiler.close(); | ||
| appStartProfiler = null; | ||
| } | ||
| if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) { | ||
| appStartContinuousProfiler.close(true); | ||
| appStartContinuousProfiler = null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { | ||
| final long activityCreatedUptimeMillis = SystemClock.uptimeMillis(); | ||
| CurrentActivityHolder.getInstance().setActivity(activity); | ||
|
|
||
| // the first activity determines the app start type | ||
| if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) { | ||
| final long nowUptimeMs = SystemClock.uptimeMillis(); | ||
|
|
||
| // If the app (process) was launched more than 1 minute ago, it's likely wrong | ||
| // If the app (process) was launched more than 1 minute ago, consider it a warm start | ||
| final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs(); | ||
| if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) { | ||
| if (!appLaunchedInForeground.getValue() | ||
| || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) { | ||
| appStartType = AppStartType.WARM; | ||
|
|
||
| shouldSendStartMeasurements = true; | ||
| appStartSpan.reset(); | ||
| appStartSpan.start(); | ||
| appStartSpan.setStartedAt(nowUptimeMs); | ||
| CLASS_LOADED_UPTIME_MS = nowUptimeMs; | ||
| appStartSpan.setStartedAt(activityCreatedUptimeMillis); | ||
| CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis; | ||
| contentProviderOnCreates.clear(); | ||
| applicationOnCreate.reset(); | ||
| } else if (savedInstanceState != null) { | ||
| appStartType = AppStartType.WARM; | ||
| } else if (firstIdle != -1 && activityCreatedUptimeMillis > firstIdle) { | ||
| appStartType = AppStartType.WARM; | ||
| } else { | ||
| appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM; | ||
| appStartType = AppStartType.COLD; | ||
| } | ||
| } | ||
| appLaunchedInForeground = true; | ||
| appLaunchedInForeground.setValue(true); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -403,9 +449,9 @@ public void onActivityDestroyed(@NonNull Activity activity) { | |
|
|
||
| final int remainingActivities = activeActivitiesCounter.decrementAndGet(); | ||
| // if the app is moving into background | ||
| // as the next Activity is considered like a new app start | ||
| // as the next onActivityCreated will treat it as a new warm app start | ||
| if (remainingActivities == 0 && !activity.isChangingConfigurations()) { | ||
| appLaunchedInForeground = false; | ||
| appLaunchedInForeground.setValue(true); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect boolean value in onActivityDestroyed sets wrong foreground stateMedium Severity In
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, this sounds legit I think? |
||
| shouldSendStartMeasurements = true; | ||
| firstDrawDone.set(false); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need this variable given it's just few microseconds apart from
activityCreatedUptimeMillis?