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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158))
- 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))

### Improvements

- Recover Android 17 `MemoryLimiter` app exits recorded as `ApplicationExitInfo.REASON_MEMORY_LIMITER` ([#6174](https://github.com/getsentry/sentry-java/pull/6174))

## 8.58.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,24 +190,30 @@ private MemoryLimiterPolicy(final @NotNull SentryAndroidOptions options) {
/**
* Returns true if the provided {@code exitInfo} looks like it came from a MemoryLimiter-induced
* process death.
*
* <p>Criteria taken from <a
* href="https://developer.android.com/about/versions/17/behavior-changes-all#app-memory-limits">here</a>.
*/
@Override
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean matches(final @NotNull ApplicationExitInfo exitInfo) {
if (exitInfo.getReason() != ApplicationExitInfo.REASON_OTHER) {
return false;
final int reason = exitInfo.getReason();

// 1. Return true for explicit matches.
if (reason == ApplicationExitInfo.REASON_MEMORY_LIMITER) {
Comment thread
0xadam-brown marked this conversation as resolved.
return true;
}

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

return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,30 @@ class MemoryLimiterIntegrationTest {
verify(fixture.options.executorService).submit(any())
}

// "Explicit records" are ApplicationExitInfo instances that use
// ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill. Contrast "heuristic
// records" below.
@Test
fun `captures exit when reason and description match exits produced by MemoryLimiter`() {
fun `captures exit for explicit MemoryLimiter record`() {
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
fixture.addAppExitInfo(
reason = ApplicationExitInfo.REASON_MEMORY_LIMITER,
description = null,
timestamp = newTimestamp,
)

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

verify(fixture.scopes).captureEvent(any(), anyOrNull<Hint>())
}

// "Heuristic records" are ApplicationExitInfo instances that use something other than
// ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill.
// (REASON_MEMORY_LIMITER wasn't introduced until 37.2.) They consist of
// ApplicationExitInfo.REASON_OTHER + an ApplicationExitInfo.description that contains the prefix
// "MemoryLimiter:".
@Test
fun `captures exit for heuristic MemoryLimiter record`() {
val integration =
fixture.getSut(
memoryLimiterEnabled = true,
Expand Down Expand Up @@ -285,25 +307,34 @@ class MemoryLimiterIntegrationTest {
}

@Test
fun `ignores exit when reason does not match exits produced by MemoryLimiter`() {
fun `captures exit for heuristic record even if description suffix is not AnonSwap`() {
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
fixture.addAppExitInfo(
reason = ApplicationExitInfo.REASON_ANR,
description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION,
reason = ApplicationExitInfo.REASON_OTHER,
// Description suffix is "Memory" rather than "AnonSwap".
description = "MemoryLimiter:Memory",
timestamp = newTimestamp,
)

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

verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
verify(fixture.scopes)
.captureEvent(
check<SentryEvent> { event ->
assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description)
},
anyOrNull<Hint>(),
)
}

// "Heuristic candidates" are ApplicationExitInfo instances that may or may not be heuristic
// records.
@Test
fun `ignores exit when description does not match exits produced by MemoryLimiter`() {
fun `ignores exit for heuristic candidate when reason does not match`() {
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
fixture.addAppExitInfo(
reason = ApplicationExitInfo.REASON_OTHER,
description = "LowSwapKiller",
reason = ApplicationExitInfo.REASON_ANR,
description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION,
timestamp = newTimestamp,
)

Expand All @@ -313,29 +344,21 @@ class MemoryLimiterIntegrationTest {
}

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

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

verify(fixture.scopes)
.captureEvent(
check<SentryEvent> { event ->
assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description)
},
anyOrNull<Hint>(),
)
verify(fixture.scopes, never()).captureEvent(any(), anyOrNull<Hint>())
}

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

@Test
fun `ignores exit when description is null`() {
fun `ignores exit for heuristic candidate when description is null`() {
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
fixture.addAppExitInfo(
reason = ApplicationExitInfo.REASON_OTHER,
Expand Down Expand Up @@ -481,7 +504,7 @@ class MemoryLimiterIntegrationTest {
}

@Test
fun `dedupes MemoryLimiter exists independently of ANR exits`() {
fun `dedupes MemoryLimiter exits independently of ANR exits`() {
val integration =
fixture.getSut(
tmpDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
Expand Down Expand Up @@ -992,7 +993,7 @@ fun IntegrationsScreen() {
tagSampleAction("memory_limiter")
activity.startActivity(Intent(activity, MemoryLimiterActivity::class.java))
},
modifier = Modifier,
modifier = Modifier.testTag("open_memory_limiter_demo"),
) {
Text("MemoryLimiter Demo", maxLines = 2, overflow = TextOverflow.Ellipsis)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -307,13 +308,25 @@ private fun MemoryLimiterScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(onClick = onStartAllocating, enabled = isSupported && !isAllocating) {
Button(
onClick = onStartAllocating,
enabled = isSupported && !isAllocating,
modifier = Modifier.testTag("memory_limiter_start_pressure"),
Comment thread
0xadam-brown marked this conversation as resolved.
) {
Text("Start memory pressure")
}
OutlinedButton(onClick = onReleaseBuffers, enabled = allocatedMb > 0 || isAllocating) {
OutlinedButton(
onClick = onReleaseBuffers,
enabled = allocatedMb > 0 || isAllocating,
modifier = Modifier.testTag("memory_limiter_reset_run"),
) {
Text("Reset run")
}
OutlinedButton(onClick = onMoveToBackground, enabled = isSupported) {
OutlinedButton(
onClick = onMoveToBackground,
enabled = isSupported,
modifier = Modifier.testTag("memory_limiter_move_background"),
) {
Text("Move app to background")
}
}
Expand Down Expand Up @@ -431,7 +444,7 @@ private fun RuntimeCard(
isAllocating: Boolean,
statusLine: String,
) {
ElevatedCard {
ElevatedCard(modifier = Modifier.testTag("memory_limiter_runtime_card")) {
Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
text = "Runtime",
Expand All @@ -445,6 +458,7 @@ private fun RuntimeCard(
RuntimeRow(label = "Retained", value = "$allocatedMb MiB")
HorizontalDivider()
Text(
modifier = Modifier.testTag("memory_limiter_status_line"),
text = statusLine,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
Expand Down
Loading