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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))

## 7.22.2

### Fixes
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ object Config {
val msgpack = "org.msgpack:msgpack-core:0.9.8"
val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14"
val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:$composeVersion"
val okio = "com.squareup.okio:okio:1.13.0"
}

object QualityPlugins {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ static void initializeIntegrationsAndProcessors(
new AndroidConnectionStatusProvider(context, options.getLogger(), buildInfoProvider));
}

if (options.getCacheDirPath() != null) {
options.addScopeObserver(new PersistingScopeObserver(options));
options.addOptionsObserver(new PersistingOptionsObserver(options));
}

options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
options.addEventProcessor(
new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
Expand Down Expand Up @@ -221,13 +226,6 @@ static void initializeIntegrationsAndProcessors(
}
}
options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options));

if (options.getCacheDirPath() != null) {
if (options.isEnableScopePersistence()) {
options.addScopeObserver(new PersistingScopeObserver(options));
}
options.addOptionsObserver(new PersistingOptionsObserver(options));
}
}

static void installDefaultIntegrations(
Expand Down Expand Up @@ -273,6 +271,8 @@ static void installDefaultIntegrations(
// AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration
// relies on AppState set by it
options.addIntegration(new AppLifecycleIntegration());
// AnrIntegration must be installed before ReplayIntegration, as ReplayIntegration relies on
// it to set the replayId in case of an ANR
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));

// registerActivityLifecycleCallbacks is only available if Context is an AppContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.sentry.SentryEvent;
import io.sentry.SentryExceptionFactory;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SentryStackTraceFactory;
import io.sentry.SpanContext;
import io.sentry.android.core.internal.util.CpuInfoUtils;
Expand Down Expand Up @@ -83,13 +84,16 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor {

private final @NotNull SentryExceptionFactory sentryExceptionFactory;

private final @Nullable PersistingScopeObserver persistingScopeObserver;

public AnrV2EventProcessor(
final @NotNull Context context,
final @NotNull SentryAndroidOptions options,
final @NotNull BuildInfoProvider buildInfoProvider) {
this.context = ContextUtils.getApplicationContext(context);
this.options = options;
this.buildInfoProvider = buildInfoProvider;
this.persistingScopeObserver = options.findPersistingScopeObserver();

final SentryStackTraceFactory sentryStackTraceFactory =
new SentryStackTraceFactory(this.options);
Expand Down Expand Up @@ -188,8 +192,7 @@ private boolean sampleReplay(final @NotNull SentryEvent event) {
}

private void setReplayId(final @NotNull SentryEvent event) {
@Nullable
String persistedReplayId = PersistingScopeObserver.read(options, REPLAY_FILENAME, String.class);
@Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
final @NotNull File replayFolder =
new File(options.getCacheDirPath(), "replay_" + persistedReplayId);
if (!replayFolder.exists()) {
Expand Down Expand Up @@ -224,8 +227,7 @@ private void setReplayId(final @NotNull SentryEvent event) {
}

private void setTrace(final @NotNull SentryEvent event) {
final SpanContext spanContext =
PersistingScopeObserver.read(options, TRACE_FILENAME, SpanContext.class);
final SpanContext spanContext = readFromDisk(options, TRACE_FILENAME, SpanContext.class);
if (event.getContexts().getTrace() == null) {
if (spanContext != null
&& spanContext.getSpanId() != null
Expand All @@ -236,8 +238,7 @@ private void setTrace(final @NotNull SentryEvent event) {
}

private void setLevel(final @NotNull SentryEvent event) {
final SentryLevel level =
PersistingScopeObserver.read(options, LEVEL_FILENAME, SentryLevel.class);
final SentryLevel level = readFromDisk(options, LEVEL_FILENAME, SentryLevel.class);
if (event.getLevel() == null) {
event.setLevel(level);
}
Expand All @@ -246,7 +247,7 @@ private void setLevel(final @NotNull SentryEvent event) {
@SuppressWarnings("unchecked")
private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Object hint) {
final List<String> fingerprint =
(List<String>) PersistingScopeObserver.read(options, FINGERPRINT_FILENAME, List.class);
(List<String>) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
if (event.getFingerprints() == null) {
event.setFingerprints(fingerprint);
}
Expand All @@ -262,16 +263,14 @@ private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Ob
}

private void setTransaction(final @NotNull SentryEvent event) {
final String transaction =
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String.class);
final String transaction = readFromDisk(options, TRANSACTION_FILENAME, String.class);
if (event.getTransaction() == null) {
event.setTransaction(transaction);
}
}

private void setContexts(final @NotNull SentryBaseEvent event) {
final Contexts persistedContexts =
PersistingScopeObserver.read(options, CONTEXTS_FILENAME, Contexts.class);
final Contexts persistedContexts = readFromDisk(options, CONTEXTS_FILENAME, Contexts.class);
if (persistedContexts == null) {
return;
}
Expand All @@ -291,7 +290,7 @@ private void setContexts(final @NotNull SentryBaseEvent event) {
@SuppressWarnings("unchecked")
private void setExtras(final @NotNull SentryBaseEvent event) {
final Map<String, Object> extras =
(Map<String, Object>) PersistingScopeObserver.read(options, EXTRAS_FILENAME, Map.class);
(Map<String, Object>) readFromDisk(options, EXTRAS_FILENAME, Map.class);
if (extras == null) {
return;
}
Expand All @@ -309,14 +308,12 @@ private void setExtras(final @NotNull SentryBaseEvent event) {
@SuppressWarnings("unchecked")
private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
final List<Breadcrumb> breadcrumbs =
(List<Breadcrumb>)
PersistingScopeObserver.read(
options, BREADCRUMBS_FILENAME, List.class, new Breadcrumb.Deserializer());
(List<Breadcrumb>) readFromDisk(options, BREADCRUMBS_FILENAME, List.class);
if (breadcrumbs == null) {
return;
}
if (event.getBreadcrumbs() == null) {
event.setBreadcrumbs(new ArrayList<>(breadcrumbs));
event.setBreadcrumbs(breadcrumbs);
} else {
event.getBreadcrumbs().addAll(breadcrumbs);
}
Expand All @@ -326,7 +323,7 @@ private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
private void setScopeTags(final @NotNull SentryBaseEvent event) {
final Map<String, String> tags =
(Map<String, String>)
PersistingScopeObserver.read(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
readFromDisk(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
if (tags == null) {
return;
}
Expand All @@ -343,19 +340,29 @@ private void setScopeTags(final @NotNull SentryBaseEvent event) {

private void setUser(final @NotNull SentryBaseEvent event) {
if (event.getUser() == null) {
final User user = PersistingScopeObserver.read(options, USER_FILENAME, User.class);
final User user = readFromDisk(options, USER_FILENAME, User.class);
event.setUser(user);
}
}

private void setRequest(final @NotNull SentryBaseEvent event) {
if (event.getRequest() == null) {
final Request request =
PersistingScopeObserver.read(options, REQUEST_FILENAME, Request.class);
final Request request = readFromDisk(options, REQUEST_FILENAME, Request.class);
event.setRequest(request);
}
}

private <T> @Nullable T readFromDisk(
final @NotNull SentryOptions options,
final @NotNull String fileName,
final @NotNull Class<T> clazz) {
if (persistingScopeObserver == null) {
return null;
}

return persistingScopeObserver.read(options, fileName, clazz);
}

// endregion

// region options persisted values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.sentry.android.timber.SentryTimberIntegration
import io.sentry.cache.PersistingOptionsObserver
import io.sentry.cache.PersistingScopeObserver
import io.sentry.compose.gestures.ComposeGestureTargetLocator
import io.sentry.test.ImmediateExecutorService
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
Expand Down Expand Up @@ -55,6 +56,7 @@ class AndroidOptionsInitializerTest {
configureContext: Context.() -> Unit = {},
assets: AssetManager? = null
) {
sentryOptions.executorService = ImmediateExecutorService()
mockContext = if (metadata != null) {
ContextUtilsTestHelper.mockMetaData(
mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext),
Expand Down Expand Up @@ -686,9 +688,10 @@ class AndroidOptionsInitializerTest {
}

@Test
fun `PersistingScopeObserver is not set to options, if scope persistence is disabled`() {
fun `PersistingScopeObserver is no-op, if scope persistence is disabled`() {
fixture.initSut(configureOptions = { isEnableScopePersistence = false })

assertTrue { fixture.sentryOptions.scopeObservers.none { it is PersistingScopeObserver } }
fixture.sentryOptions.findPersistingScopeObserver()?.setTags(mapOf("key" to "value"))
assertFalse(File(AndroidOptionsInitializer.getCacheDir(fixture.context), PersistingScopeObserver.SCOPE_CACHE).exists())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME
import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME
import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME
import io.sentry.cache.PersistingScopeObserver.USER_FILENAME
import io.sentry.cache.tape.QueueFile
import io.sentry.hints.AbnormalExit
import io.sentry.hints.Backfillable
import io.sentry.protocol.Browser
Expand All @@ -61,6 +62,7 @@ import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowActivityManager
import org.robolectric.shadows.ShadowBuild
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -98,6 +100,7 @@ class AnrV2EventProcessorTest {
options.cacheDirPath = dir.newFolder().absolutePath
options.environment = "release"
options.isSendDefaultPii = isSendDefaultPii
options.addScopeObserver(PersistingScopeObserver(options))

whenever(buildInfo.sdkInfoVersion).thenReturn(currentSdk)
whenever(buildInfo.isEmulator).thenReturn(true)
Expand Down Expand Up @@ -147,7 +150,16 @@ class AnrV2EventProcessorTest {
fun <T : Any> persistScope(filename: String, entity: T) {
val dir = File(options.cacheDirPath, SCOPE_CACHE).also { it.mkdirs() }
val file = File(dir, filename)
options.serializer.serialize(entity, file.writer())
if (filename == BREADCRUMBS_FILENAME) {
val queueFile = QueueFile.Builder(file).build()
(entity as List<Breadcrumb>).forEach { crumb ->
val baos = ByteArrayOutputStream()
options.serializer.serialize(crumb, baos.writer())
queueFile.add(baos.toByteArray())
}
} else {
options.serializer.serialize(entity, file.writer())
}
}

fun <T : Any> persistOptions(filename: String, entity: T) {
Expand Down Expand Up @@ -621,7 +633,7 @@ class AnrV2EventProcessorTest {
val processed = processor.process(SentryEvent(), hint)!!

assertEquals(replayId1.toString(), processed.contexts[Contexts.REPLAY_ID].toString())
assertEquals(replayId1.toString(), PersistingScopeObserver.read(fixture.options, REPLAY_FILENAME, String::class.java))
assertEquals(replayId1.toString(), fixture.options.findPersistingScopeObserver()?.read(fixture.options, REPLAY_FILENAME, String::class.java))
}

private fun processEvent(
Expand Down
Loading
Loading