Skip to content

Commit c146223

Browse files
authored
Merge 51be24b into e873777
2 parents e873777 + 51be24b commit c146223

File tree

13 files changed

+843
-81
lines changed

13 files changed

+843
-81
lines changed

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ env:
1515
BUILD_PATH: "build/outputs/apk/release"
1616
APK_NAME: "sentry-uitest-android-critical-release.apk"
1717
APK_ARTIFACT_NAME: "sentry-uitest-android-critical-release"
18-
MAESTRO_VERSION: "1.39.0"
18+
MAESTRO_VERSION: "2.1.0"
1919

2020
jobs:
2121
build:
@@ -60,23 +60,23 @@ jobs:
6060
matrix:
6161
include:
6262
- api-level: 31 # Android 12
63-
target: aosp_atd
63+
target: google_apis
6464
channel: canary # Necessary for ATDs
6565
arch: x86_64
6666
- api-level: 33 # Android 13
67-
target: aosp_atd
67+
target: google_apis
6868
channel: canary # Necessary for ATDs
6969
arch: x86_64
7070
- api-level: 34 # Android 14
71-
target: aosp_atd
71+
target: google_apis
7272
channel: canary # Necessary for ATDs
7373
arch: x86_64
7474
- api-level: 35 # Android 15
75-
target: aosp_atd
75+
target: google_apis
7676
channel: canary # Necessary for ATDs
7777
arch: x86_64
7878
- api-level: 36 # Android 16
79-
target: aosp_atd
79+
target: google_apis
8080
channel: canary # Necessary for ATDs
8181
arch: x86_64
8282
steps:
@@ -109,7 +109,7 @@ jobs:
109109
force-avd-creation: false
110110
disable-animations: true
111111
disable-spellchecker: true
112-
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
112+
emulator-options: -memory 4096 -no-window -gpu auto -noaudio -no-boot-anim -camera-back none
113113
disk-size: 4096M
114114
script: echo "Generated AVD snapshot for caching."
115115

@@ -133,16 +133,16 @@ jobs:
133133
force-avd-creation: false
134134
disable-animations: true
135135
disable-spellchecker: true
136-
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save
136+
emulator-options: -memory 4096 -no-window -gpu auto -noaudio -no-boot-anim -camera-back none -no-snapshot-save
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+
mkdir "${{env.BASE_PATH}}/maestro-logs/" || true; adb emu screenrecord start --time-limit 360 "${{env.BASE_PATH}}/maestro-logs/recording.webm" || true; maestro test "${{env.BASE_PATH}}/maestro" --test-output-dir="${{env.BASE_PATH}}/maestro-logs/test-output" || MAESTRO_EXIT_CODE=$?; adb emu screenrecord stop || true; adb logcat -d > "${{env.BASE_PATH}}/maestro-logs/logcat.txt" || true; exit ${MAESTRO_EXIT_CODE:-0}
141141
142142
- name: Upload Maestro test results
143-
if: failure()
143+
if: ${{ always() }}
144144
uses: actions/upload-artifact@v6
145145
with:
146-
name: maestro-logs
146+
name: maestro-logs-${{ matrix.api-level }}-${{ matrix.arch }}-${{ matrix.target }}
147147
path: "${{env.BASE_PATH}}/maestro-logs"
148148
retention-days: 1

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- Fix scroll target detection for Jetpack Compose ([#5017](https://github.com/getsentry/sentry-java/pull/5017))
2929
- No longer fork Sentry `Scopes` for `reactor-kafka` consumer poll `Runnable` ([#5080](https://github.com/getsentry/sentry-java/pull/5080))
3030
- This was causing a memory leak because `reactor-kafka`'s poll event reschedules itself infinitely, and each invocation of `SentryScheduleHook` created forked scopes with a parent reference, building an unbounded chain that couldn't be garbage collected.
31+
- Fix cold/warm app start type detection for Android devices running API level 34+ ([#4999](https://github.com/getsentry/sentry-java/pull/4999))
3132

3233
### Internal
3334

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
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.sentry.android.core
2+
3+
import android.app.ActivityManager
4+
import android.app.ApplicationStartInfo
5+
import android.os.Build
6+
import org.robolectric.annotation.Implementation
7+
import org.robolectric.annotation.Implements
8+
9+
@Implements(ActivityManager::class, minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
10+
class SentryShadowActivityManager {
11+
companion object {
12+
private var historicalProcessStartReasons: List<ApplicationStartInfo> = emptyList()
13+
14+
fun setHistoricalProcessStartReasons(startReasons: List<ApplicationStartInfo>) {
15+
historicalProcessStartReasons = startReasons
16+
}
17+
18+
fun reset() {
19+
historicalProcessStartReasons = emptyList()
20+
}
21+
}
22+
23+
@Implementation
24+
fun getHistoricalProcessStartReasons(maxNum: Int): List<ApplicationStartInfo> {
25+
return historicalProcessStartReasons.take(maxNum)
26+
}
27+
}

0 commit comments

Comments
 (0)