Skip to content

Commit dc3cf3d

Browse files
committed
ref(android): Match MemoryLimiter app exit reason introduced in Android 37.2
Android 37.2 introduced a new ApplicationExitInfo.REASON_MEMORY_LIMITER flag. This commit updates our MemoryLimiterIntegration code to match against it (while preserving ouro previous matching logic). No Android API guard is needed for the new flag because: - we already check MemoryLimiterIntegration registration against API 37; and - REASON_MEMORY_LIMITER is a static integer constant and will be inlined as an integer by the Java compiler (meaning we don't need extra protection for devices on 37.0 or 37.1).
1 parent 63926e5 commit dc3cf3d

5 files changed

Lines changed: 87 additions & 39 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158))
88
- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154))
99

10+
### Improvements
11+
12+
- Recover Android 17 `MemoryLimiter` app exits recorded as `ApplicationExitInfo.REASON_MEMORY_LIMITER` ([#6174](https://github.com/getsentry/sentry-java/pull/6174))
13+
1014
## 8.58.0
1115

1216
### Features

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,24 +190,30 @@ private MemoryLimiterPolicy(final @NotNull SentryAndroidOptions options) {
190190
/**
191191
* Returns true if the provided {@code exitInfo} looks like it came from a MemoryLimiter-induced
192192
* process death.
193-
*
194-
* <p>Criteria taken from <a
195-
* href="https://developer.android.com/about/versions/17/behavior-changes-all#app-memory-limits">here</a>.
196193
*/
197194
@Override
198195
@RequiresApi(api = Build.VERSION_CODES.R)
199196
public boolean matches(final @NotNull ApplicationExitInfo exitInfo) {
200-
if (exitInfo.getReason() != ApplicationExitInfo.REASON_OTHER) {
201-
return false;
197+
final int reason = exitInfo.getReason();
198+
199+
// 1. Return true for explicit matches.
200+
if (reason == ApplicationExitInfo.REASON_MEMORY_LIMITER) {
201+
return true;
202202
}
203203

204-
final String description = exitInfo.getDescription();
205-
// We match on the "MemoryLimiter:" prefix rather than the full "MemoryLimiter:AnonSwap"
206-
// string mentioned in the Android 17 release notes because we want to capture any future
207-
// MemoryLimiter kill reason without a code change. (MemoryLimiter source already tracks
208-
// MemoryLimiter:Memory and MemoryLimiter:Swap, but for now doesn't kill the process because
209-
// of them.)
210-
return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION_PREFIX);
204+
// 2. Return true for heuristic matches based on criteria discussed here:
205+
// https://developer.android.com/about/versions/17/behavior-changes-all#app-memory-limits
206+
if (reason == ApplicationExitInfo.REASON_OTHER) {
207+
final String description = exitInfo.getDescription();
208+
// We match on the "MemoryLimiter:" prefix rather than the full "MemoryLimiter:AnonSwap"
209+
// string mentioned in the Android 17 release notes because we want to capture any future
210+
// MemoryLimiter kill reason without a code change. (MemoryLimiter source already tracks
211+
// MemoryLimiter:Memory and MemoryLimiter:Swap, but for now doesn't kill the process because
212+
// of them.)
213+
return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION_PREFIX);
214+
}
215+
216+
return false;
211217
}
212218

213219
@Override

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

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,30 @@ class MemoryLimiterIntegrationTest {
214214
verify(fixture.options.executorService).submit(any())
215215
}
216216

217+
// "Explicit records" are ApplicationExitInfo instances that use
218+
// ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill. Contrast "heuristic
219+
// records" below.
217220
@Test
218-
fun `captures exit when reason and description match exits produced by MemoryLimiter`() {
221+
fun `captures exit for explicit MemoryLimiter record`() {
222+
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
223+
fixture.addAppExitInfo(
224+
reason = ApplicationExitInfo.REASON_MEMORY_LIMITER,
225+
description = null,
226+
timestamp = newTimestamp,
227+
)
228+
229+
integration.register(fixture.scopes, fixture.options)
230+
231+
verify(fixture.scopes).captureEvent(any(), anyOrNull<Hint>())
232+
}
233+
234+
// "Heuristic records" are ApplicationExitInfo instances that use something other than
235+
// ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill.
236+
// (REASON_MEMORY_LIMITER wasn't introduced until 37.2.) They consist of
237+
// ApplicationExitInfo.REASON_OTHER + an ApplicationExitInfo.description that contains the prefix
238+
// "MemoryLimiter:".
239+
@Test
240+
fun `captures exit for heuristic MemoryLimiter record`() {
219241
val integration =
220242
fixture.getSut(
221243
memoryLimiterEnabled = true,
@@ -285,25 +307,34 @@ class MemoryLimiterIntegrationTest {
285307
}
286308

287309
@Test
288-
fun `ignores exit when reason does not match exits produced by MemoryLimiter`() {
310+
fun `captures exit for heuristic record even if description suffix is not AnonSwap`() {
289311
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
290312
fixture.addAppExitInfo(
291-
reason = ApplicationExitInfo.REASON_ANR,
292-
description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION,
313+
reason = ApplicationExitInfo.REASON_OTHER,
314+
// Description suffix is "Memory" rather than "AnonSwap".
315+
description = "MemoryLimiter:Memory",
293316
timestamp = newTimestamp,
294317
)
295318

296319
integration.register(fixture.scopes, fixture.options)
297320

298-
verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
321+
verify(fixture.scopes)
322+
.captureEvent(
323+
check<SentryEvent> { event ->
324+
assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description)
325+
},
326+
anyOrNull<Hint>(),
327+
)
299328
}
300329

330+
// "Heuristic candidates" are ApplicationExitInfo instances that may or may not be heuristic
331+
// records.
301332
@Test
302-
fun `ignores exit when description does not match exits produced by MemoryLimiter`() {
333+
fun `ignores exit for heuristic candidate when reason does not match`() {
303334
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
304335
fixture.addAppExitInfo(
305-
reason = ApplicationExitInfo.REASON_OTHER,
306-
description = "LowSwapKiller",
336+
reason = ApplicationExitInfo.REASON_ANR,
337+
description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION,
307338
timestamp = newTimestamp,
308339
)
309340

@@ -313,29 +344,21 @@ class MemoryLimiterIntegrationTest {
313344
}
314345

315346
@Test
316-
fun `captures exit for any MemoryLimiter sub-reason, not just AnonSwap`() {
347+
fun `ignores exit for heuristic candidate when description does not match`() {
317348
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
318-
// A future MemoryLimiter kill sub-reason (e.g. the memory or swap limits) still lives in the
319-
// "MemoryLimiter:" namespace and must be captured, with its raw sub-reason preserved.
320349
fixture.addAppExitInfo(
321350
reason = ApplicationExitInfo.REASON_OTHER,
322-
description = "MemoryLimiter:Memory",
351+
description = "LowSwapKiller",
323352
timestamp = newTimestamp,
324353
)
325354

326355
integration.register(fixture.scopes, fixture.options)
327356

328-
verify(fixture.scopes)
329-
.captureEvent(
330-
check<SentryEvent> { event ->
331-
assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description)
332-
},
333-
anyOrNull<Hint>(),
334-
)
357+
verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
335358
}
336359

337360
@Test
338-
fun `ignores exit when description mentions MemoryLimiter without the namespace delimiter`() {
361+
fun `ignores exit for heuristic candidate when description mentions MemoryLimiter without the namespace delimiter`() {
339362
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
340363
// "MemoryLimiter" without the ":" delimiter is not a MemoryLimiter kill; matching requires the
341364
// namespace prefix so we don't over-capture unrelated REASON_OTHER exits.
@@ -351,7 +374,7 @@ class MemoryLimiterIntegrationTest {
351374
}
352375

353376
@Test
354-
fun `ignores exit when description is null`() {
377+
fun `ignores exit for heuristic candidate when description is null`() {
355378
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
356379
fixture.addAppExitInfo(
357380
reason = ApplicationExitInfo.REASON_OTHER,
@@ -481,7 +504,7 @@ class MemoryLimiterIntegrationTest {
481504
}
482505

483506
@Test
484-
fun `dedupes MemoryLimiter exists independently of ANR exits`() {
507+
fun `dedupes MemoryLimiter exits independently of ANR exits`() {
485508
val integration =
486509
fixture.getSut(
487510
tmpDir,

‎sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import androidx.compose.ui.draw.shadow
7676
import androidx.compose.ui.graphics.Color
7777
import androidx.compose.ui.graphics.vector.ImageVector
7878
import androidx.compose.ui.platform.LocalContext
79+
import androidx.compose.ui.platform.testTag
7980
import androidx.compose.ui.res.painterResource
8081
import androidx.compose.ui.res.stringResource
8182
import androidx.compose.ui.text.font.FontWeight
@@ -992,7 +993,7 @@ fun IntegrationsScreen() {
992993
tagSampleAction("memory_limiter")
993994
activity.startActivity(Intent(activity, MemoryLimiterActivity::class.java))
994995
},
995-
modifier = Modifier,
996+
modifier = Modifier.testTag("open_memory_limiter_demo"),
996997
) {
997998
Text("MemoryLimiter Demo", maxLines = 2, overflow = TextOverflow.Ellipsis)
998999
}

‎sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/memory/MemoryLimiterActivity.kt‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import androidx.compose.runtime.setValue
4747
import androidx.compose.ui.Alignment
4848
import androidx.compose.ui.Modifier
4949
import androidx.compose.ui.graphics.Color
50+
import androidx.compose.ui.platform.testTag
5051
import androidx.compose.ui.text.font.FontFamily
5152
import androidx.compose.ui.text.font.FontWeight
5253
import androidx.compose.ui.unit.dp
@@ -307,13 +308,25 @@ private fun MemoryLimiterScreen(
307308
horizontalArrangement = Arrangement.spacedBy(12.dp),
308309
verticalArrangement = Arrangement.spacedBy(12.dp),
309310
) {
310-
Button(onClick = onStartAllocating, enabled = isSupported && !isAllocating) {
311+
Button(
312+
onClick = onStartAllocating,
313+
enabled = isSupported && !isAllocating,
314+
modifier = Modifier.testTag("memory_limiter_start_pressure"),
315+
) {
311316
Text("Start memory pressure")
312317
}
313-
OutlinedButton(onClick = onReleaseBuffers, enabled = allocatedMb > 0 || isAllocating) {
318+
OutlinedButton(
319+
onClick = onReleaseBuffers,
320+
enabled = allocatedMb > 0 || isAllocating,
321+
modifier = Modifier.testTag("memory_limiter_reset_run"),
322+
) {
314323
Text("Reset run")
315324
}
316-
OutlinedButton(onClick = onMoveToBackground, enabled = isSupported) {
325+
OutlinedButton(
326+
onClick = onMoveToBackground,
327+
enabled = isSupported,
328+
modifier = Modifier.testTag("memory_limiter_move_background"),
329+
) {
317330
Text("Move app to background")
318331
}
319332
}
@@ -431,7 +444,7 @@ private fun RuntimeCard(
431444
isAllocating: Boolean,
432445
statusLine: String,
433446
) {
434-
ElevatedCard {
447+
ElevatedCard(modifier = Modifier.testTag("memory_limiter_runtime_card")) {
435448
Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
436449
Text(
437450
text = "Runtime",
@@ -445,6 +458,7 @@ private fun RuntimeCard(
445458
RuntimeRow(label = "Retained", value = "$allocatedMb MiB")
446459
HorizontalDivider()
447460
Text(
461+
modifier = Modifier.testTag("memory_limiter_status_line"),
448462
text = statusLine,
449463
style = MaterialTheme.typography.bodyMedium,
450464
color = MaterialTheme.colorScheme.onSurfaceVariant,

0 commit comments

Comments
 (0)