Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

### Fixes

- Prevent inflated cold app start when the OS spawns the process in the background (e.g. FCM push) on API 35+ ([#5841](https://github.com/getsentry/sentry-java/pull/5841), [#5880](https://github.com/getsentry/sentry-java/pull/5880))
- Preserve single-sample ANR profile chunks so profiles remain available on ANR events ([#5872](https://github.com/getsentry/sentry-java/pull/5872))
- Prevent inflated cold app start when the OS spawns the process in the background (e.g. FCM push) on API 35+ ([#5841](https://github.com/getsentry/sentry-java/pull/5841))
- Avoid a CPU busy-loop when recording discarded log or metric envelopes under rate limiting ([#5835](https://github.com/getsentry/sentry-java/pull/5835))
- `ClientReportRecorder` now reads the item count from the envelope item header instead of deserializing the payload, which under sustained rate limiting could pin CPU cores while repeatedly throwing exceptions
- Report tasks handed to a no-op `ISentryExecutorService` as cancelled ([#5874](https://github.com/getsentry/sentry-java/pull/5874))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
import io.sentry.protocol.SentryId;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.LazyEvaluator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -70,14 +69,7 @@ public enum AppStartType {
new AutoClosableReentrantLock();

private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
private final LazyEvaluator<Boolean> appLaunchedInForeground =
new LazyEvaluator<>(
new LazyEvaluator.Evaluator<Boolean>() {
@Override
public @NotNull Boolean evaluate() {
return ContextUtils.isForegroundImportance();
}
});
private @Nullable volatile Boolean appLaunchedInForeground;
private volatile long firstIdle = -1;

private final @NotNull TimeSpan appStartSpan;
Expand Down Expand Up @@ -239,12 +231,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
}

public boolean isAppLaunchedInForeground() {
return appLaunchedInForeground.getValue();
return Boolean.TRUE.equals(appLaunchedInForeground);
}

@VisibleForTesting
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
this.appLaunchedInForeground.setValue(appLaunchedInForeground);
this.appLaunchedInForeground = appLaunchedInForeground;
}

public void setHeadlessAppStartListener(final @Nullable HeadlessAppStartListener listener) {
Expand Down Expand Up @@ -318,8 +310,7 @@ public void onAppStartSpansSent() {
}

public boolean shouldSendStartMeasurements(final boolean ignoreForegroundCheck) {
return shouldSendStartMeasurements
&& (ignoreForegroundCheck || appLaunchedInForeground.getValue());
return shouldSendStartMeasurements && (ignoreForegroundCheck || isAppLaunchedInForeground());
}

public boolean shouldSendStartMeasurements() {
Expand Down Expand Up @@ -349,7 +340,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.getValue()) {
if (appStartType != AppStartType.UNKNOWN && isAppLaunchedInForeground()) {
if (options.isEnablePerformanceV2()) {
// Only started when sdk version is >= N
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
Expand Down Expand Up @@ -412,7 +403,7 @@ public void clear() {
}
appStartContinuousProfiler = null;
appStartSamplingDecision = null;
appLaunchedInForeground.resetValue();
appLaunchedInForeground = null;
isCallbackRegistered = false;
shouldSendStartMeasurements = true;
firstDrawDone.set(false);
Expand Down Expand Up @@ -510,7 +501,7 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
return;
}
isCallbackRegistered = true;
appLaunchedInForeground.resetValue();
appLaunchedInForeground = null;
application.registerActivityLifecycleCallbacks(instance);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
Expand All @@ -529,7 +520,7 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
} else {
appStartType = AppStartType.WARM;
}
appLaunchedInForeground.setValue(isForegroundStartReason(info.getReason()));
appLaunchedInForeground = isForegroundStartReason(info.getReason());
}
}
} catch (RuntimeException ignored) {
Expand All @@ -543,6 +534,10 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
}
}
}
// Fallback, if no matching ApplicationStartInfo is available
if (appLaunchedInForeground == null) {
appLaunchedInForeground = ContextUtils.isForegroundImportance();
}

if (appStartType == AppStartType.UNKNOWN || headlessAppStartListener != null) {
scheduleHeadlessAppStartCheckOnMain();
Expand Down Expand Up @@ -591,7 +586,7 @@ private void handleHeadlessAppStartIfNeededOnMain() {
return;
}

appLaunchedInForeground.setValue(false);
appLaunchedInForeground = false;

// Headless starts have no Activity signal for the pre-API 35 warm/cold heuristic.
// If ApplicationStartInfo did not resolve the type, classify the process start as cold.
Expand Down Expand Up @@ -677,7 +672,7 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
// An active extension explicitly keeps the launch alive: resetting the span here would make
// the extended vital measure from the activity while the eager app.start transaction stays
// anchored at process start.
if ((!appLaunchedInForeground.getValue()
if ((!isAppLaunchedInForeground()
|| durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1))
&& !appStartExtension.isActive()) {
appStartType = AppStartType.WARM;
Expand All @@ -698,7 +693,7 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
}
}
}
appLaunchedInForeground.setValue(true);
appLaunchedInForeground = true;
}

@Override
Expand Down Expand Up @@ -744,7 +739,7 @@ public void onActivityDestroyed(@NonNull Activity activity) {
// as the next onActivityCreated will treat it as a new warm app start
if (remainingActivities == 0 && !activity.isChangingConfigurations()) {
appStartType = AppStartType.WARM;
appLaunchedInForeground.setValue(true);
appLaunchedInForeground = true;
shouldSendStartMeasurements = true;
firstDrawDone.set(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,16 @@ class AppStartMetricsTest {
}

@Test
fun `headless app start fires HeadlessAppStartListener`() = headlessProcess {
val listenerCalls = AtomicInteger()
fun `headless app start fires HeadlessAppStartListener`() =
withProcessImportance(false) {
val listenerCalls = AtomicInteger()

AppStartMetrics.getInstance().setHeadlessAppStartListener { listenerCalls.incrementAndGet() }
AppStartMetrics.getInstance().registerLifecycleCallbacks(mock<Application>())
waitForMainLooperIdle()
AppStartMetrics.getInstance().setHeadlessAppStartListener { listenerCalls.incrementAndGet() }
AppStartMetrics.getInstance().registerLifecycleCallbacks(mock<Application>())
waitForMainLooperIdle()

assertEquals(1, listenerCalls.get())
}
assertEquals(1, listenerCalls.get())
}

@Test
fun `foreground process does not fire HeadlessAppStartListener`() {
Expand Down Expand Up @@ -295,7 +296,7 @@ class AppStartMetricsTest {

@Test
fun `resolveHeadlessAppStartEndTime uses applicationOnCreate stop when Gradle plugin instrumented`() =
headlessProcess {
withProcessImportance(false) {
val metrics = AppStartMetrics.getInstance()
metrics.appStartTimeSpan.setStartedAt(100)
metrics.setHeadlessAppStartListener {}
Expand All @@ -312,7 +313,7 @@ class AppStartMetricsTest {

@Test
fun `resolveHeadlessAppStartEndTime falls back to CLASS_LOADED_UPTIME_MS when no plugin and no ApplicationStartInfo`() =
headlessProcess {
withProcessImportance(false) {
val metrics = AppStartMetrics.getInstance()
metrics.setClassLoadedUptimeMs(200)
metrics.appStartTimeSpan.setStartedAt(100)
Expand Down Expand Up @@ -371,12 +372,18 @@ class AppStartMetricsTest {
Shadows.shadowOf(Looper.getMainLooper()).idle()
}

// Simulates a real headless start (broadcast/service), i.e. a non-foreground-importance process.
// The Robolectric default importance in this test class is IMPORTANCE_FOREGROUND, so headless
// scenarios must opt into a background importance explicitly.
private fun <T> headlessProcess(block: () -> T): T =
/**
* Mocks the process importance to simulate a user initiated start (e.g. launcher) or a real
* headless start (broadcast/service), i.e. a non-foreground-importance process.
*
* The Robolectric default importance in this test class is IMPORTANCE_FOREGROUND, so any headless
* scenarios must opt into a background importance explicitly.
*/
private fun <T> withProcessImportance(isForeground: Boolean, block: () -> T): T =
mockStatic(ContextUtils::class.java).use { contextUtils ->
contextUtils.`when`<Boolean> { ContextUtils.isForegroundImportance() }.thenReturn(false)
contextUtils
.`when`<Boolean> { ContextUtils.isForegroundImportance() }
.thenReturn(isForeground)
block()
}

Expand Down Expand Up @@ -1123,4 +1130,20 @@ class AppStartMetricsTest {
assertEquals(now, metrics.appStartTimeSpan.startUptimeMs)
metrics.appStartExtension.setExtendAppStartListener(null)
}

@Test
fun `broadcast starts are not considered a foreground start`() =
withProcessImportance(false) {
val metrics = AppStartMetrics.getInstance()
metrics.registerLifecycleCallbacks(mock<Application>())
assertFalse(metrics.isAppLaunchedInForeground)
}

@Test
fun `typical app starts are considered a foreground start`() =
withProcessImportance(true) {
val metrics = AppStartMetrics.getInstance()
metrics.registerLifecycleCallbacks(mock<Application>())
assertTrue(metrics.isAppLaunchedInForeground)
}
}
Loading