Skip to content

Commit b61429a

Browse files
authored
Reduce excessive CPU usage when serializing breadcrumbs to disk (#4181)
* WIP * WIP * Remove redundant line * Add Tests * api dump * Formatting * REset scope cache on new init * Clean up * Comment * Changelog * Workaround square/tape#173 * Add a comment to setBreadcrumbs * Address PR review * Update CHANGELOG.md
1 parent e5b840c commit b61429a

File tree

25 files changed

+2523
-124
lines changed

25 files changed

+2523
-124
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))
8+
39
## 8.4.0
410

511
### Fixes

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ object Config {
213213
val msgpack = "org.msgpack:msgpack-core:0.9.8"
214214
val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14"
215215
val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.6.8"
216+
val okio = "com.squareup.okio:okio:1.13.0"
216217
}
217218

218219
object QualityPlugins {

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ static void initializeIntegrationsAndProcessors(
148148
new AndroidConnectionStatusProvider(context, options.getLogger(), buildInfoProvider));
149149
}
150150

151+
if (options.getCacheDirPath() != null) {
152+
options.addScopeObserver(new PersistingScopeObserver(options));
153+
options.addOptionsObserver(new PersistingOptionsObserver(options));
154+
}
155+
151156
options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
152157
options.addEventProcessor(
153158
new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
@@ -225,13 +230,6 @@ static void initializeIntegrationsAndProcessors(
225230
}
226231
}
227232
options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options));
228-
229-
if (options.getCacheDirPath() != null) {
230-
if (options.isEnableScopePersistence()) {
231-
options.addScopeObserver(new PersistingScopeObserver(options));
232-
}
233-
options.addOptionsObserver(new PersistingOptionsObserver(options));
234-
}
235233
}
236234

237235
static void installDefaultIntegrations(
@@ -277,6 +275,8 @@ static void installDefaultIntegrations(
277275
// AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration
278276
// relies on AppState set by it
279277
options.addIntegration(new AppLifecycleIntegration());
278+
// AnrIntegration must be installed before ReplayIntegration, as ReplayIntegration relies on
279+
// it to set the replayId in case of an ANR
280280
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
281281

282282
// registerActivityLifecycleCallbacks is only available if Context is an AppContext

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

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.sentry.SentryEvent;
3434
import io.sentry.SentryExceptionFactory;
3535
import io.sentry.SentryLevel;
36+
import io.sentry.SentryOptions;
3637
import io.sentry.SentryStackTraceFactory;
3738
import io.sentry.SpanContext;
3839
import io.sentry.android.core.internal.util.CpuInfoUtils;
@@ -83,13 +84,16 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor {
8384

8485
private final @NotNull SentryExceptionFactory sentryExceptionFactory;
8586

87+
private final @Nullable PersistingScopeObserver persistingScopeObserver;
88+
8689
public AnrV2EventProcessor(
8790
final @NotNull Context context,
8891
final @NotNull SentryAndroidOptions options,
8992
final @NotNull BuildInfoProvider buildInfoProvider) {
9093
this.context = ContextUtils.getApplicationContext(context);
9194
this.options = options;
9295
this.buildInfoProvider = buildInfoProvider;
96+
this.persistingScopeObserver = options.findPersistingScopeObserver();
9397

9498
final SentryStackTraceFactory sentryStackTraceFactory =
9599
new SentryStackTraceFactory(this.options);
@@ -188,8 +192,7 @@ private boolean sampleReplay(final @NotNull SentryEvent event) {
188192
}
189193

190194
private void setReplayId(final @NotNull SentryEvent event) {
191-
@Nullable
192-
String persistedReplayId = PersistingScopeObserver.read(options, REPLAY_FILENAME, String.class);
195+
@Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
193196
final @NotNull File replayFolder =
194197
new File(options.getCacheDirPath(), "replay_" + persistedReplayId);
195198
if (!replayFolder.exists()) {
@@ -224,8 +227,7 @@ private void setReplayId(final @NotNull SentryEvent event) {
224227
}
225228

226229
private void setTrace(final @NotNull SentryEvent event) {
227-
final SpanContext spanContext =
228-
PersistingScopeObserver.read(options, TRACE_FILENAME, SpanContext.class);
230+
final SpanContext spanContext = readFromDisk(options, TRACE_FILENAME, SpanContext.class);
229231
if (event.getContexts().getTrace() == null) {
230232
if (spanContext != null
231233
&& spanContext.getSpanId() != null
@@ -236,8 +238,7 @@ private void setTrace(final @NotNull SentryEvent event) {
236238
}
237239

238240
private void setLevel(final @NotNull SentryEvent event) {
239-
final SentryLevel level =
240-
PersistingScopeObserver.read(options, LEVEL_FILENAME, SentryLevel.class);
241+
final SentryLevel level = readFromDisk(options, LEVEL_FILENAME, SentryLevel.class);
241242
if (event.getLevel() == null) {
242243
event.setLevel(level);
243244
}
@@ -246,7 +247,7 @@ private void setLevel(final @NotNull SentryEvent event) {
246247
@SuppressWarnings("unchecked")
247248
private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Object hint) {
248249
final List<String> fingerprint =
249-
(List<String>) PersistingScopeObserver.read(options, FINGERPRINT_FILENAME, List.class);
250+
(List<String>) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
250251
if (event.getFingerprints() == null) {
251252
event.setFingerprints(fingerprint);
252253
}
@@ -262,16 +263,14 @@ private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Ob
262263
}
263264

264265
private void setTransaction(final @NotNull SentryEvent event) {
265-
final String transaction =
266-
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String.class);
266+
final String transaction = readFromDisk(options, TRANSACTION_FILENAME, String.class);
267267
if (event.getTransaction() == null) {
268268
event.setTransaction(transaction);
269269
}
270270
}
271271

272272
private void setContexts(final @NotNull SentryBaseEvent event) {
273-
final Contexts persistedContexts =
274-
PersistingScopeObserver.read(options, CONTEXTS_FILENAME, Contexts.class);
273+
final Contexts persistedContexts = readFromDisk(options, CONTEXTS_FILENAME, Contexts.class);
275274
if (persistedContexts == null) {
276275
return;
277276
}
@@ -291,7 +290,7 @@ private void setContexts(final @NotNull SentryBaseEvent event) {
291290
@SuppressWarnings("unchecked")
292291
private void setExtras(final @NotNull SentryBaseEvent event) {
293292
final Map<String, Object> extras =
294-
(Map<String, Object>) PersistingScopeObserver.read(options, EXTRAS_FILENAME, Map.class);
293+
(Map<String, Object>) readFromDisk(options, EXTRAS_FILENAME, Map.class);
295294
if (extras == null) {
296295
return;
297296
}
@@ -309,14 +308,12 @@ private void setExtras(final @NotNull SentryBaseEvent event) {
309308
@SuppressWarnings("unchecked")
310309
private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
311310
final List<Breadcrumb> breadcrumbs =
312-
(List<Breadcrumb>)
313-
PersistingScopeObserver.read(
314-
options, BREADCRUMBS_FILENAME, List.class, new Breadcrumb.Deserializer());
311+
(List<Breadcrumb>) readFromDisk(options, BREADCRUMBS_FILENAME, List.class);
315312
if (breadcrumbs == null) {
316313
return;
317314
}
318315
if (event.getBreadcrumbs() == null) {
319-
event.setBreadcrumbs(new ArrayList<>(breadcrumbs));
316+
event.setBreadcrumbs(breadcrumbs);
320317
} else {
321318
event.getBreadcrumbs().addAll(breadcrumbs);
322319
}
@@ -326,7 +323,7 @@ private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
326323
private void setScopeTags(final @NotNull SentryBaseEvent event) {
327324
final Map<String, String> tags =
328325
(Map<String, String>)
329-
PersistingScopeObserver.read(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
326+
readFromDisk(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
330327
if (tags == null) {
331328
return;
332329
}
@@ -343,19 +340,29 @@ private void setScopeTags(final @NotNull SentryBaseEvent event) {
343340

344341
private void setUser(final @NotNull SentryBaseEvent event) {
345342
if (event.getUser() == null) {
346-
final User user = PersistingScopeObserver.read(options, USER_FILENAME, User.class);
343+
final User user = readFromDisk(options, USER_FILENAME, User.class);
347344
event.setUser(user);
348345
}
349346
}
350347

351348
private void setRequest(final @NotNull SentryBaseEvent event) {
352349
if (event.getRequest() == null) {
353-
final Request request =
354-
PersistingScopeObserver.read(options, REQUEST_FILENAME, Request.class);
350+
final Request request = readFromDisk(options, REQUEST_FILENAME, Request.class);
355351
event.setRequest(request);
356352
}
357353
}
358354

355+
private <T> @Nullable T readFromDisk(
356+
final @NotNull SentryOptions options,
357+
final @NotNull String fileName,
358+
final @NotNull Class<T> clazz) {
359+
if (persistingScopeObserver == null) {
360+
return null;
361+
}
362+
363+
return persistingScopeObserver.read(options, fileName, clazz);
364+
}
365+
359366
// endregion
360367

361368
// region options persisted values

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.sentry.android.timber.SentryTimberIntegration
2020
import io.sentry.cache.PersistingOptionsObserver
2121
import io.sentry.cache.PersistingScopeObserver
2222
import io.sentry.compose.gestures.ComposeGestureTargetLocator
23+
import io.sentry.test.ImmediateExecutorService
2324
import org.junit.runner.RunWith
2425
import org.mockito.kotlin.any
2526
import org.mockito.kotlin.eq
@@ -55,6 +56,7 @@ class AndroidOptionsInitializerTest {
5556
configureContext: Context.() -> Unit = {},
5657
assets: AssetManager? = null
5758
) {
59+
sentryOptions.executorService = ImmediateExecutorService()
5860
mockContext = if (metadata != null) {
5961
ContextUtilsTestHelper.mockMetaData(
6062
mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext),
@@ -724,9 +726,10 @@ class AndroidOptionsInitializerTest {
724726
}
725727

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

730-
assertTrue { fixture.sentryOptions.scopeObservers.none { it is PersistingScopeObserver } }
732+
fixture.sentryOptions.findPersistingScopeObserver()?.setTags(mapOf("key" to "value"))
733+
assertFalse(File(AndroidOptionsInitializer.getCacheDir(fixture.context), PersistingScopeObserver.SCOPE_CACHE).exists())
731734
}
732735
}

sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME
3535
import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME
3636
import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME
3737
import io.sentry.cache.PersistingScopeObserver.USER_FILENAME
38+
import io.sentry.cache.tape.QueueFile
3839
import io.sentry.hints.AbnormalExit
3940
import io.sentry.hints.Backfillable
4041
import io.sentry.protocol.Browser
@@ -61,6 +62,7 @@ import org.robolectric.annotation.Config
6162
import org.robolectric.shadow.api.Shadow
6263
import org.robolectric.shadows.ShadowActivityManager
6364
import org.robolectric.shadows.ShadowBuild
65+
import java.io.ByteArrayOutputStream
6466
import java.io.File
6567
import kotlin.test.BeforeTest
6668
import kotlin.test.Test
@@ -98,6 +100,7 @@ class AnrV2EventProcessorTest {
98100
options.cacheDirPath = dir.newFolder().absolutePath
99101
options.environment = "release"
100102
options.isSendDefaultPii = isSendDefaultPii
103+
options.addScopeObserver(PersistingScopeObserver(options))
101104

102105
whenever(buildInfo.sdkInfoVersion).thenReturn(currentSdk)
103106
whenever(buildInfo.isEmulator).thenReturn(true)
@@ -147,7 +150,16 @@ class AnrV2EventProcessorTest {
147150
fun <T : Any> persistScope(filename: String, entity: T) {
148151
val dir = File(options.cacheDirPath, SCOPE_CACHE).also { it.mkdirs() }
149152
val file = File(dir, filename)
150-
options.serializer.serialize(entity, file.writer())
153+
if (filename == BREADCRUMBS_FILENAME) {
154+
val queueFile = QueueFile.Builder(file).build()
155+
(entity as List<Breadcrumb>).forEach { crumb ->
156+
val baos = ByteArrayOutputStream()
157+
options.serializer.serialize(crumb, baos.writer())
158+
queueFile.add(baos.toByteArray())
159+
}
160+
} else {
161+
options.serializer.serialize(entity, file.writer())
162+
}
151163
}
152164

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

623635
assertEquals(replayId1.toString(), processed.contexts[Contexts.REPLAY_ID].toString())
624-
assertEquals(replayId1.toString(), PersistingScopeObserver.read(fixture.options, REPLAY_FILENAME, String::class.java))
636+
assertEquals(replayId1.toString(), fixture.options.findPersistingScopeObserver()?.read(fixture.options, REPLAY_FILENAME, String::class.java))
625637
}
626638

627639
private fun processEvent(

0 commit comments

Comments
 (0)