Skip to content

Commit 224c2d6

Browse files
authored
Merge 71e4a2c into 8687935
2 parents 8687935 + 71e4a2c commit 224c2d6

File tree

10 files changed

+712
-72
lines changed

10 files changed

+712
-72
lines changed

.github/workflows/integration-tests-ui-critical.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,13 @@ jobs:
137137
script: |
138138
adb uninstall io.sentry.uitest.android.critical || echo "Already uninstalled (or not found)"
139139
adb install -r -d "${{env.APK_NAME}}"
140-
maestro test "${{env.BASE_PATH}}/maestro" --debug-output "${{env.BASE_PATH}}/maestro-logs"
140+
adb shell screenrecord --size 720x1280 --bit-rate 2000000 /sdcard/recording.mp4 & SCREENRECORD_PID=$!; maestro test "${{env.BASE_PATH}}/maestro" --debug-output "${{env.BASE_PATH}}/maestro-logs" || MAESTRO_EXIT_CODE=$?; kill $SCREENRECORD_PID 2>/dev/null || true; wait $SCREENRECORD_PID 2>/dev/null || true; adb pull /sdcard/recording.mp4 "${{env.BASE_PATH}}/maestro-logs/recording.mp4" || true; adb shell rm /sdcard/recording.mp4 || true; exit ${MAESTRO_EXIT_CODE:-0}
141141
142142
- name: Upload Maestro test results
143143
if: failure()
144144
uses: actions/upload-artifact@v6
145145
with:
146-
name: maestro-logs
146+
name: maestro-logs-${{ matrix.api-level }}-${{ matrix.arch }}-${{ matrix.target }}
147+
include-hidden-files: true # maestro debug logs are stored within maestro-logs/.maestro/
147148
path: "${{env.BASE_PATH}}/maestro-logs"
148149
retention-days: 1

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix warm app start type detection for edge cases ([#4999](https://github.com/getsentry/sentry-java/pull/4999))
8+
59
### Features
610

711
- Add `installGroupsOverride` parameter and `installGroups` property to Build Distribution SDK ([#5062](https://github.com/getsentry/sentry-java/pull/5062))

sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java

Lines changed: 114 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package io.sentry.android.core.performance;
22

33
import android.app.Activity;
4+
import android.app.ActivityManager;
45
import android.app.Application;
6+
import android.app.ApplicationStartInfo;
57
import android.content.ContentProvider;
8+
import android.content.Context;
9+
import android.os.Build;
610
import android.os.Bundle;
711
import android.os.Handler;
812
import android.os.Looper;
13+
import android.os.MessageQueue;
914
import android.os.SystemClock;
1015
import androidx.annotation.NonNull;
1116
import androidx.annotation.Nullable;
@@ -21,6 +26,7 @@
2126
import io.sentry.android.core.SentryAndroidOptions;
2227
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
2328
import io.sentry.util.AutoClosableReentrantLock;
29+
import io.sentry.util.LazyEvaluator;
2430
import java.util.ArrayList;
2531
import java.util.Collections;
2632
import java.util.HashMap;
@@ -56,7 +62,15 @@ public enum AppStartType {
5662
new AutoClosableReentrantLock();
5763

5864
private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
59-
private boolean appLaunchedInForeground;
65+
private final LazyEvaluator<Boolean> appLaunchedInForeground =
66+
new LazyEvaluator<>(
67+
new LazyEvaluator.Evaluator<Boolean>() {
68+
@Override
69+
public @NotNull Boolean evaluate() {
70+
return ContextUtils.isForegroundImportance();
71+
}
72+
});
73+
private volatile long firstIdle = -1;
6074

6175
private final @NotNull TimeSpan appStartSpan;
6276
private final @NotNull TimeSpan sdkInitTimeSpan;
@@ -89,7 +103,6 @@ public AppStartMetrics() {
89103
applicationOnCreate = new TimeSpan();
90104
contentProviderOnCreates = new HashMap<>();
91105
activityLifecycles = new ArrayList<>();
92-
appLaunchedInForeground = ContextUtils.isForegroundImportance();
93106
}
94107

95108
/**
@@ -140,12 +153,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
140153
}
141154

142155
public boolean isAppLaunchedInForeground() {
143-
return appLaunchedInForeground;
156+
return appLaunchedInForeground.getValue();
144157
}
145158

146159
@VisibleForTesting
147160
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
148-
this.appLaunchedInForeground = appLaunchedInForeground;
161+
this.appLaunchedInForeground.setValue(appLaunchedInForeground);
149162
}
150163

151164
/**
@@ -176,7 +189,7 @@ public void onAppStartSpansSent() {
176189
}
177190

178191
public boolean shouldSendStartMeasurements() {
179-
return shouldSendStartMeasurements && appLaunchedInForeground;
192+
return shouldSendStartMeasurements && appLaunchedInForeground.getValue();
180193
}
181194

182195
public long getClassLoadedUptimeMs() {
@@ -191,7 +204,7 @@ public long getClassLoadedUptimeMs() {
191204
final @NotNull SentryAndroidOptions options) {
192205
// If the app start type was never determined or app wasn't launched in foreground,
193206
// the app start is considered invalid
194-
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground) {
207+
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground.getValue()) {
195208
if (options.isEnablePerformanceV2()) {
196209
// Only started when sdk version is >= N
197210
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
@@ -212,6 +225,16 @@ public long getClassLoadedUptimeMs() {
212225
return new TimeSpan();
213226
}
214227

228+
@TestOnly
229+
void setFirstIdle(final long firstIdle) {
230+
this.firstIdle = firstIdle;
231+
}
232+
233+
@TestOnly
234+
long getFirstIdle() {
235+
return firstIdle;
236+
}
237+
215238
@TestOnly
216239
public void clear() {
217240
appStartType = AppStartType.UNKNOWN;
@@ -229,11 +252,12 @@ public void clear() {
229252
}
230253
appStartContinuousProfiler = null;
231254
appStartSamplingDecision = null;
232-
appLaunchedInForeground = false;
255+
appLaunchedInForeground.resetValue();
233256
isCallbackRegistered = false;
234257
shouldSendStartMeasurements = true;
235258
firstDrawDone.set(false);
236259
activeActivitiesCounter.set(0);
260+
firstIdle = -1;
237261
}
238262

239263
public @Nullable ITransactionProfiler getAppStartProfiler() {
@@ -301,7 +325,8 @@ public static void onApplicationPostCreate(final @NotNull Application applicatio
301325
}
302326

303327
/**
304-
* Register a callback to check if an activity was started after the application was created
328+
* Register a callback to check if an activity was started after the application was created. Must
329+
* be called from the main thread.
305330
*
306331
* @param application The application object to register the callback to
307332
*/
@@ -310,61 +335,106 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
310335
return;
311336
}
312337
isCallbackRegistered = true;
313-
appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance();
338+
appLaunchedInForeground.resetValue();
314339
application.registerActivityLifecycleCallbacks(instance);
315-
// We post on the main thread a task to post a check on the main thread. On Pixel devices
316-
// (possibly others) the first task posted on the main thread is called before the
317-
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
318-
// callback is called before the application one.
319-
new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain());
340+
341+
final @Nullable ActivityManager activityManager =
342+
(ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
343+
344+
if (activityManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
345+
final List<ApplicationStartInfo> historicalProcessStartReasons =
346+
activityManager.getHistoricalProcessStartReasons(1);
347+
if (!historicalProcessStartReasons.isEmpty()) {
348+
final @NotNull ApplicationStartInfo info = historicalProcessStartReasons.get(0);
349+
if (info.getStartupState() == ApplicationStartInfo.STARTUP_STATE_STARTED) {
350+
if (info.getStartType() == ApplicationStartInfo.START_TYPE_COLD) {
351+
appStartType = AppStartType.COLD;
352+
} else {
353+
appStartType = AppStartType.WARM;
354+
}
355+
}
356+
}
357+
}
358+
359+
if (appStartType == AppStartType.UNKNOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
360+
Looper.getMainLooper()
361+
.getQueue()
362+
.addIdleHandler(
363+
new MessageQueue.IdleHandler() {
364+
@Override
365+
public boolean queueIdle() {
366+
firstIdle = SystemClock.uptimeMillis();
367+
checkCreateTimeOnMain();
368+
return false;
369+
}
370+
});
371+
} else if (appStartType == AppStartType.UNKNOWN) {
372+
// We post on the main thread a task to post a check on the main thread. On Pixel devices
373+
// (possibly others) the first task posted on the main thread is called before the
374+
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
375+
// callback is called before the application one.
376+
final Handler handler = new Handler(Looper.getMainLooper());
377+
handler.post(
378+
new Runnable() {
379+
@Override
380+
public void run() {
381+
// not technically correct, but close enough for pre-M
382+
firstIdle = SystemClock.uptimeMillis();
383+
handler.post(() -> checkCreateTimeOnMain());
384+
}
385+
});
386+
}
320387
}
321388

322389
private void checkCreateTimeOnMain() {
323-
new Handler(Looper.getMainLooper())
324-
.post(
325-
() -> {
326-
// if no activity has ever been created, app was launched in background
327-
if (activeActivitiesCounter.get() == 0) {
328-
appLaunchedInForeground = false;
329-
330-
// we stop the app start profilers, as they are useless and likely to timeout
331-
if (appStartProfiler != null && appStartProfiler.isRunning()) {
332-
appStartProfiler.close();
333-
appStartProfiler = null;
334-
}
335-
if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) {
336-
appStartContinuousProfiler.close(true);
337-
appStartContinuousProfiler = null;
338-
}
339-
}
340-
});
390+
// if no activity has ever been created, app was launched in background
391+
if (activeActivitiesCounter.get() == 0) {
392+
appLaunchedInForeground.setValue(false);
393+
394+
// we stop the app start profilers, as they are useless and likely to timeout
395+
if (appStartProfiler != null && appStartProfiler.isRunning()) {
396+
appStartProfiler.close();
397+
appStartProfiler = null;
398+
}
399+
if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) {
400+
appStartContinuousProfiler.close(true);
401+
appStartContinuousProfiler = null;
402+
}
403+
}
341404
}
342405

343406
@Override
344407
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
408+
final long activityCreatedUptimeMillis = SystemClock.uptimeMillis();
345409
CurrentActivityHolder.getInstance().setActivity(activity);
346410

347411
// the first activity determines the app start type
348412
if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) {
349413
final long nowUptimeMs = SystemClock.uptimeMillis();
350414

351-
// If the app (process) was launched more than 1 minute ago, it's likely wrong
415+
// If the app (process) was launched more than 1 minute ago, consider it a warm start
352416
final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs();
353-
if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
417+
if (!appLaunchedInForeground.getValue()
418+
|| durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
354419
appStartType = AppStartType.WARM;
355-
356420
shouldSendStartMeasurements = true;
357421
appStartSpan.reset();
358-
appStartSpan.start();
359-
appStartSpan.setStartedAt(nowUptimeMs);
360-
CLASS_LOADED_UPTIME_MS = nowUptimeMs;
422+
appStartSpan.setStartedAt(activityCreatedUptimeMillis);
423+
CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis;
361424
contentProviderOnCreates.clear();
362425
applicationOnCreate.reset();
363-
} else {
364-
appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM;
426+
} else if (appStartType == AppStartType.UNKNOWN) {
427+
// pre API 35 handling
428+
if (savedInstanceState != null) {
429+
appStartType = AppStartType.WARM;
430+
} else if (firstIdle != -1 && activityCreatedUptimeMillis > firstIdle) {
431+
appStartType = AppStartType.WARM;
432+
} else {
433+
appStartType = AppStartType.COLD;
434+
}
365435
}
366436
}
367-
appLaunchedInForeground = true;
437+
appLaunchedInForeground.setValue(true);
368438
}
369439

370440
@Override
@@ -403,9 +473,10 @@ public void onActivityDestroyed(@NonNull Activity activity) {
403473

404474
final int remainingActivities = activeActivitiesCounter.decrementAndGet();
405475
// if the app is moving into background
406-
// as the next Activity is considered like a new app start
476+
// as the next onActivityCreated will treat it as a new warm app start
407477
if (remainingActivities == 0 && !activity.isChangingConfigurations()) {
408-
appLaunchedInForeground = false;
478+
appStartType = AppStartType.WARM;
479+
appLaunchedInForeground.setValue(true);
409480
shouldSendStartMeasurements = true;
410481
firstDrawDone.set(false);
411482
}

0 commit comments

Comments
 (0)