Skip to content

Commit bc5c4c9

Browse files
authored
feat(android): Recover MemoryLimiter app exits on startup (JAVA-687) (#6111)
Introduce a new MemoryLimiterIntegration that captures process deaths attributable to Android 17's new [MemoryLimiter](https://source.android.com/docs/core/perf/memory-limiter#process-monitoring) system service (see also [here](https://android-developers.googleblog.com/2026/06/prioritizing-memory-efficiency-steps-for-android-17.html)). Process death info is extracted from ApplicationExitInfo on the next app launch. We then enrich it with persisted SDK state and send it to Relay as a fatal Sentry event. Integration is experimental; is only available for Android API >= 37; and is disabled by default.
1 parent 5efb2d7 commit bc5c4c9

19 files changed

Lines changed: 1682 additions & 37 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add Android SDK support for reporting `MemoryLimiter` app exits recovered from `ApplicationExitInfo` ([#6111](https://github.com/getsentry/sentry-java/pull/6111)).
78
- Sentry can now configure Log4j2 automatically for Spring Boot 4 when `sentry-log4j2` is on the classpath and Log4j2 Core is the active logging backend ([#5403](https://github.com/getsentry/sentry-java/pull/5403))
89
- Enable automatic appender registration with:
910
```properties

sentry-android-core/api/sentry-android-core.api

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,20 @@ public final class io/sentry/android/core/LoadClass : io/sentry/util/LoadClass {
334334
public fun loadClass (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/Class;
335335
}
336336

337+
public final class io/sentry/android/core/MemoryLimiterIntegration : io/sentry/Integration, java/io/Closeable {
338+
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;)V
339+
public fun close ()V
340+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
341+
}
342+
343+
public final class io/sentry/android/core/MemoryLimiterIntegration$MemoryLimiterHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/Backfillable {
344+
public fun <init> (JLio/sentry/ILogger;JZ)V
345+
public fun isFlushable (Lio/sentry/protocol/SentryId;)Z
346+
public fun setFlushable (Lio/sentry/protocol/SentryId;)V
347+
public fun shouldEnrich ()Z
348+
public fun timestamp ()J
349+
}
350+
337351
public final class io/sentry/android/core/NativeEventCollector {
338352
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
339353
public fun collect ()V
@@ -446,7 +460,9 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
446460
public fun isEnableStandaloneAppStartTracing ()Z
447461
public fun isEnableSystemEventBreadcrumbs ()Z
448462
public fun isEnableSystemEventBreadcrumbsExtras ()Z
463+
public fun isMemoryLimiterEnabled ()Z
449464
public fun isReportHistoricalAnrs ()Z
465+
public fun isReportHistoricalMemoryLimiterExits ()Z
450466
public fun isReportHistoricalTombstones ()Z
451467
public fun isTombstoneEnabled ()Z
452468
public fun setAnrEnabled (Z)V
@@ -480,10 +496,12 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
480496
public fun setEnableSystemEventBreadcrumbs (Z)V
481497
public fun setEnableSystemEventBreadcrumbsExtras (Z)V
482498
public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
499+
public fun setMemoryLimiterEnabled (Z)V
483500
public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V
484501
public fun setNativeSdkName (Ljava/lang/String;)V
485502
public fun setNdkAppHangTimeoutIntervalMillis (J)V
486503
public fun setReportHistoricalAnrs (Z)V
504+
public fun setReportHistoricalMemoryLimiterExits (Z)V
487505
public fun setReportHistoricalTombstones (Z)V
488506
public fun setTombstoneEnabled (Z)V
489507
}
@@ -641,8 +659,8 @@ public class io/sentry/android/core/TombstoneIntegration$TombstonePolicy : io/se
641659
public fun buildReport (Landroid/app/ApplicationExitInfo;Z)Lio/sentry/android/core/ApplicationExitInfoHistoryDispatcher$Report;
642660
public fun getLabel ()Ljava/lang/String;
643661
public fun getLastReportedTimestamp ()Ljava/lang/Long;
644-
public fun getTargetReason ()I
645662
public fun markReported (J)V
663+
public fun matches (Landroid/app/ApplicationExitInfo;)Z
646664
public fun shouldReportHistorical ()Z
647665
}
648666

@@ -747,14 +765,18 @@ public final class io/sentry/android/core/anr/StackTraceConverter {
747765
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
748766
public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
749767
public static final field LAST_ANR_REPORT Ljava/lang/String;
768+
public static final field LAST_MEMORY_LIMITER_MARKER_LABEL Ljava/lang/String;
769+
public static final field LAST_MEMORY_LIMITER_REPORT Ljava/lang/String;
750770
public static final field LAST_TOMBSTONE_MARKER_LABEL Ljava/lang/String;
751771
public static final field LAST_TOMBSTONE_REPORT Ljava/lang/String;
752772
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
753773
public fun getDirectory ()Ljava/io/File;
754774
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
755775
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
776+
public static fun lastReportedMemoryLimiter (Lio/sentry/SentryOptions;)Ljava/lang/Long;
756777
public static fun lastReportedTombstone (Lio/sentry/SentryOptions;)Ljava/lang/Long;
757778
public static fun markAnrReported (Lio/sentry/SentryOptions;J)V
779+
public static fun markMemoryLimiterReported (Lio/sentry/SentryOptions;J)V
758780
public static fun markTombstoneReported (Lio/sentry/SentryOptions;J)V
759781
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
760782
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ static void installDefaultIntegrations(
434434
options.addIntegration(new TombstoneIntegration(context));
435435
}
436436

437+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.CINNAMON_BUN) {
438+
options.addIntegration(new MemoryLimiterIntegration(context, buildInfoProvider));
439+
}
440+
437441
// this integration uses android.os.FileObserver, we can't move to sentry
438442
// before creating a pure java impl.
439443
options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());

sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ private static final class AnrV2Policy
118118
}
119119

120120
@Override
121-
public int getTargetReason() {
122-
return ApplicationExitInfo.REASON_ANR;
121+
public boolean matches(final @NotNull ApplicationExitInfo exitInfo) {
122+
return exitInfo.getReason() == ApplicationExitInfo.REASON_ANR;
123123
}
124124

125125
@Override

sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ private void setDist(
587587
timestamp = ((AbnormalExit) hint).timestamp();
588588
} else if (hint instanceof NativeCrashExit) {
589589
timestamp = ((NativeCrashExit) hint).timestamp();
590+
} else if (hint instanceof MemoryLimiterIntegration.MemoryLimiterHint) {
591+
timestamp = ((MemoryLimiterIntegration.MemoryLimiterHint) hint).timestamp();
590592
} else {
591593
timestamp = null;
592594
}
@@ -800,9 +802,7 @@ private final class AnrHintEnricher implements HintEnricher {
800802

801803
@Override
802804
public boolean supports(@NotNull Object hint) {
803-
// While this is specifically an ANR enricher we discriminate enrichment application
804-
// on the broader AbnormalExit hints for now.
805-
return hint instanceof AbnormalExit;
805+
return hint instanceof AnrV2Integration.AnrV2Hint;
806806
}
807807

808808
// by default we assume that the ANR is foreground, unless abnormalMechanism is "anr_background"

sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoHistoryDispatcher.java

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@
2424
import org.jetbrains.annotations.NotNull;
2525
import org.jetbrains.annotations.Nullable;
2626

27+
/**
28+
* Shared startup-time pipeline for reporting process deaths recovered from Android's {@link
29+
* ApplicationExitInfo} history.
30+
*
31+
* <p>This class provides the generic flow used by integrations like ANR, tombstone, and
32+
* MemoryLimiter recovery. That flow involves:
33+
*
34+
* <ul>
35+
* <li>reading the system's historical exit list;
36+
* <li>finding matching records through an {@link ApplicationExitInfoPolicy};
37+
* <li>skipping exits that are too old or have already been reported; and
38+
* <li>capturing synthetic Sentry events for the remaining matches.
39+
* </ul>
40+
*
41+
* <p>The {@code ApplicationExitInfoPolicy} provides the exit-specific rules, while the dispatcher
42+
* owns the lifecycle and bookkeeping around them.
43+
*/
2744
@ApiStatus.Internal
2845
final class ApplicationExitInfoHistoryDispatcher implements Runnable {
2946

@@ -70,7 +87,13 @@ public void run() {
7087

7188
waitPreviousSessionFlush();
7289

73-
final List<ApplicationExitInfo> exitInfos = new ArrayList<>(applicationExitInfoList);
90+
final List<ApplicationExitInfo> exitInfos = new ArrayList<>(applicationExitInfoList.size());
91+
for (final ApplicationExitInfo exitInfo : applicationExitInfoList) {
92+
if (exitInfo != null) {
93+
exitInfos.add(exitInfo);
94+
}
95+
}
96+
7497
final @Nullable Long lastReportedTimestamp = policy.getLastReportedTimestamp();
7598

7699
final ApplicationExitInfo latest = removeLatest(exitInfos);
@@ -135,7 +158,7 @@ private void waitPreviousSessionFlush() {
135158
final @NotNull List<ApplicationExitInfo> exitInfos) {
136159
for (Iterator<ApplicationExitInfo> it = exitInfos.iterator(); it.hasNext(); ) {
137160
ApplicationExitInfo applicationExitInfo = it.next();
138-
if (applicationExitInfo.getReason() == policy.getTargetReason()) {
161+
if (policy.matches(applicationExitInfo)) {
139162
it.remove();
140163
return applicationExitInfo;
141164
}
@@ -149,7 +172,7 @@ private void reportHistorical(
149172
final @Nullable Long lastReportedTimestamp) {
150173
Collections.reverse(exitInfos);
151174
for (ApplicationExitInfo applicationExitInfo : exitInfos) {
152-
if (applicationExitInfo.getReason() == policy.getTargetReason()) {
175+
if (policy.matches(applicationExitInfo)) {
153176
if (applicationExitInfo.getTimestamp() < threshold) {
154177
options
155178
.getLogger()
@@ -223,20 +246,43 @@ private void report(final @NotNull ApplicationExitInfo exitInfo, final boolean e
223246
}
224247
}
225248

249+
/**
250+
* Exit-specific contract for the shared {@link ApplicationExitInfo} recovery pipeline.
251+
*
252+
* <p>{@link ApplicationExitInfoHistoryDispatcher} owns the generic startup flow for recovering
253+
* past process deaths from Android's historical exit list. An {@code ApplicationExitInfoPolicy}
254+
* provides the rules for a given exit family, including how to recognize matching records,
255+
* whether older matches should also be reported, how deduplication is tracked, and how matching
256+
* record is turned into a synthetic Sentry event.
257+
*/
226258
interface ApplicationExitInfoPolicy {
259+
260+
/** Returns the human-readable label used in dispatcher logs for this exit family. */
227261
@NotNull
228262
String getLabel();
229263

230-
int getTargetReason();
264+
/** Returns {@code true} when the given {@link ApplicationExitInfo} belongs to this policy. */
265+
boolean matches(@NotNull ApplicationExitInfo exitInfo);
231266

267+
/** Returns whether older matching exits should be reported in addition to the latest one. */
232268
boolean shouldReportHistorical();
233269

270+
/** Returns the timestamp of the most recently reported matching exit, if one was recorded. */
234271
@Nullable
235272
Long getLastReportedTimestamp();
236273

237-
/** Records {@code timestamp} as the last reported exit, so it is not reported again. */
274+
/**
275+
* Records {@code timestamp} as the last reported matching exit, so it is not reported again.
276+
*/
238277
void markReported(long timestamp);
239278

279+
/**
280+
* Builds the synthetic report for a matching exit.
281+
*
282+
* <p>{@code enrich} indicates whether the dispatcher is reporting the latest recovered exit,
283+
* which may be backfilled with persisted launch state, or an older historical one, which is
284+
* typically kept leaner.
285+
*/
240286
@Nullable
241287
Report buildReport(@NotNull ApplicationExitInfo exitInfo, boolean enrich);
242288
}

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ final class ManifestMetadataReader {
4747
static final String TOMBSTONE_ENABLE = "io.sentry.tombstone.enable";
4848
static final String TOMBSTONE_ATTACH_RAW = "io.sentry.tombstone.attach-raw";
4949
static final String TOMBSTONE_REPORT_HISTORICAL = "io.sentry.tombstone.report-historical";
50+
static final String MEMORY_LIMITER_ENABLE = "io.sentry.memory-limiter.enable";
51+
static final String MEMORY_LIMITER_REPORT_HISTORICAL =
52+
"io.sentry.memory-limiter.report-historical";
5053

5154
static final String AUTO_INIT = "io.sentry.auto-init";
5255
static final String NDK_ENABLE = "io.sentry.ndk.enable";
@@ -253,6 +256,14 @@ static void applyMetadata(
253256
logger,
254257
TOMBSTONE_REPORT_HISTORICAL,
255258
options.isReportHistoricalTombstones()));
259+
options.setMemoryLimiterEnabled(
260+
readBool(metadata, logger, MEMORY_LIMITER_ENABLE, options.isMemoryLimiterEnabled()));
261+
options.setReportHistoricalMemoryLimiterExits(
262+
readBool(
263+
metadata,
264+
logger,
265+
MEMORY_LIMITER_REPORT_HISTORICAL,
266+
options.isReportHistoricalMemoryLimiterExits()));
256267

257268
// use enableAutoSessionTracking as fallback
258269
options.setEnableAutoSessionTracking(

0 commit comments

Comments
 (0)