Skip to content

Commit cc0773d

Browse files
committed
feat(android): Match MemoryLimiter exits on the description namespace prefix
Match the "MemoryLimiter:" prefix instead of the full "MemoryLimiter:AnonSwap" string. Per AOSP, AnonSwap is the only MemoryLimiter kill sub-reason on Android 17, but the memory and swap limits it also tracks may start killing in a future release; matching the namespace prefix keeps capturing those without a code change, while the colon still anchors matching to the MemoryLimiter namespace. The raw description is retained on the event mechanism.
1 parent 161483e commit cc0773d

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

‎sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@
7272
@ApiStatus.Internal
7373
public final class MemoryLimiterIntegration implements Integration, Closeable {
7474

75-
static final @NotNull String MEMORY_LIMITER_DESCRIPTION = "MemoryLimiter:AnonSwap";
75+
static final @NotNull String MEMORY_LIMITER_DESCRIPTION_PREFIX = "MemoryLimiter:";
76+
static final @NotNull String MEMORY_LIMITER_DESCRIPTION =
77+
MEMORY_LIMITER_DESCRIPTION_PREFIX + "AnonSwap";
78+
7679
static final @NotNull String MEMORY_LIMITER_MESSAGE = "Android process killed by MemoryLimiter";
7780

7881
static final @NotNull String IMPORTANCE_DATA_KEY = "importance";
@@ -186,20 +189,26 @@ private MemoryLimiterPolicy(final @NotNull SentryAndroidOptions options) {
186189
}
187190

188191
/**
189-
* Matches Android exits that the system records as generic "other" deaths and annotates with
190-
* the MemoryLimiter description.
192+
* Returns true if the provided {@code exitInfo} looks like it comes from a
193+
* MemoryLimiter-induced process death.
191194
*
192195
* <p>Criteria taken from <a
193-
* href="https://developer.android.com/about/versions/17/behavior-changes-all?utm_source=android-studio-app&utm_medium=app#app-memory-limits">here</a>.
196+
* href="https://developer.android.com/about/versions/17/behavior-changes-all#app-memory-limits">here</a>.
194197
*/
195198
@Override
196199
@RequiresApi(api = Build.VERSION_CODES.R)
197200
public boolean matches(final @NotNull ApplicationExitInfo exitInfo) {
198201
if (exitInfo.getReason() != ApplicationExitInfo.REASON_OTHER) {
199202
return false;
200203
}
204+
201205
final String description = exitInfo.getDescription();
202-
return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION);
206+
// We match on the "MemoryLimiter:" prefix rather than the full "MemoryLimiter:AnonSwap"
207+
// string mentioned in the Android 17 release notes because we want to capture any future
208+
// MemoryLimiter kill reason without a code change. (MemoryLimiter source already tracks
209+
// MemoryLimiter:Memory and MemoryLimiter:Swap reasons, but for now doesn't kill the process
210+
// because of them.)
211+
return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION_PREFIX);
203212
}
204213

205214
@Override
@@ -276,7 +285,7 @@ public void markReported(final long timestamp) {
276285
* <p>The <a
277286
* href="https://source.android.com/docs/core/perf/memory-limiter#process-monitoring">MemoryLimiter
278287
* docs</a> classify exact {@code PROCESS_STATE_*} values, but {@link ApplicationExitInfo} only
279-
* exposes the coarser {@link RunningAppProcessInfo#Importance} bucket.
288+
* exposes the coarser {@link RunningAppProcessInfo} importance bucket.
280289
*/
281290
private @NotNull String getProcessVisibility(final int importance) {
282291
switch (importance) {

‎sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,44 @@ class MemoryLimiterIntegrationTest {
365365
verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
366366
}
367367

368+
@Test
369+
fun `captures exit for any MemoryLimiter sub-reason, not just AnonSwap`() {
370+
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
371+
// A future MemoryLimiter kill sub-reason (e.g. the memory or swap limits) still lives in the
372+
// "MemoryLimiter:" namespace and must be captured, with its raw sub-reason preserved.
373+
fixture.addAppExitInfo(
374+
reason = ApplicationExitInfo.REASON_OTHER,
375+
description = "MemoryLimiter:Memory",
376+
timestamp = newTimestamp,
377+
)
378+
379+
integration.register(fixture.scopes, fixture.options)
380+
381+
verify(fixture.scopes)
382+
.captureEvent(
383+
check<SentryEvent> { event ->
384+
assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description)
385+
},
386+
anyOrNull<Hint>(),
387+
)
388+
}
389+
390+
@Test
391+
fun `ignores exit when description mentions MemoryLimiter without the namespace delimiter`() {
392+
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
393+
// "MemoryLimiter" without the ":" delimiter is not a MemoryLimiter kill; matching requires the
394+
// namespace prefix so we don't over-capture unrelated REASON_OTHER exits.
395+
fixture.addAppExitInfo(
396+
reason = ApplicationExitInfo.REASON_OTHER,
397+
description = "NotAMemoryLimiterKill",
398+
timestamp = newTimestamp,
399+
)
400+
401+
integration.register(fixture.scopes, fixture.options)
402+
403+
verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
404+
}
405+
368406
@Test
369407
fun `ignores exit when description is null`() {
370408
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)

0 commit comments

Comments
 (0)