Skip to content

Commit ca480b4

Browse files
authored
Merge d976459 into 1e5daad
2 parents 1e5daad + d976459 commit ca480b4

23 files changed

Lines changed: 2427 additions & 124 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- `SentryOptions.setTracePropagationTargets` is no longer marked internal ([#4170](https://github.com/getsentry/sentry-java/pull/4170))
8+
- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))
89

910
### Behavioural Changes
1011

buildSrc/src/main/java/Config.kt

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

215216
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(

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

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.os.SystemClock
1111
import androidx.test.core.app.ApplicationProvider
1212
import androidx.test.ext.junit.runners.AndroidJUnit4
1313
import io.sentry.Breadcrumb
14+
import io.sentry.DateUtils
1415
import io.sentry.Hint
1516
import io.sentry.ILogger
1617
import io.sentry.ISentryClient
@@ -37,10 +38,13 @@ import io.sentry.cache.PersistingOptionsObserver
3738
import io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME
3839
import io.sentry.cache.PersistingOptionsObserver.OPTIONS_CACHE
3940
import io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME
40-
import io.sentry.cache.PersistingScopeObserver
4141
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
42+
import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME
4243
import io.sentry.cache.PersistingScopeObserver.SCOPE_CACHE
4344
import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME
45+
import io.sentry.cache.tape.QueueFile
46+
import io.sentry.protocol.Contexts
47+
import io.sentry.protocol.SentryId
4448
import io.sentry.test.applyTestOptions
4549
import io.sentry.test.initForTest
4650
import io.sentry.transport.NoOpEnvelopeCache
@@ -64,6 +68,7 @@ import org.robolectric.annotation.Config
6468
import org.robolectric.shadow.api.Shadow
6569
import org.robolectric.shadows.ShadowActivityManager
6670
import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder
71+
import java.io.ByteArrayOutputStream
6772
import java.io.File
6873
import java.nio.file.Files
6974
import java.util.concurrent.TimeUnit
@@ -417,27 +422,31 @@ class SentryAndroidTest {
417422
assertEquals("Debug!", event.breadcrumbs!![0].message)
418423
assertEquals("staging", event.environment)
419424
assertEquals("[email protected]", event.release)
425+
assertEquals("afcb46b1140ade5187c4bbb5daa804df", event.contexts[Contexts.REPLAY_ID])
420426
asserted.set(true)
421427
null
422428
}
423429

424430
// have to do it after the cacheDir is set to options, because it adds a dsn hash after
425431
prefillOptionsCache(it.cacheDirPath!!)
426-
prefillScopeCache(it.cacheDirPath!!)
432+
prefillScopeCache(it, it.cacheDirPath!!)
427433

428434
it.release = "[email protected]+220"
429435
it.environment = "debug"
430-
// this is necessary to delay the AnrV2Integration processing to execute the configure
431-
// scope block below (otherwise it won't be possible as scopes is no-op before .init)
432-
it.executorService.submit {
433-
Sentry.configureScope { scope ->
434-
// make sure the scope values changed to test that we're still using previously
435-
// persisted values for the old ANR events
436-
assertEquals("TestActivity", scope.transactionName)
437-
}
438-
}
439436
options = it
440437
}
438+
options.executorService.submit {
439+
// verify we reset the persisted scope values after the init bg tasks have run to ensure
440+
// clean state for a new process.
441+
assertEquals(
442+
emptyList<Breadcrumb>(),
443+
options.findPersistingScopeObserver()?.read(options, BREADCRUMBS_FILENAME, List::class.java)
444+
)
445+
assertEquals(
446+
SentryId.EMPTY_ID.toString(),
447+
options.findPersistingScopeObserver()?.read(options, REPLAY_FILENAME, String::class.java)
448+
)
449+
}
441450
Sentry.configureScope {
442451
it.setTransaction("TestActivity")
443452
it.addBreadcrumb(Breadcrumb.error("Error!"))
@@ -451,7 +460,7 @@ class SentryAndroidTest {
451460
// assert that persisted values have changed
452461
assertEquals(
453462
"TestActivity",
454-
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java)
463+
options.findPersistingScopeObserver()?.read(options, TRANSACTION_FILENAME, String::class.java)
455464
)
456465
assertEquals(
457466
@@ -532,19 +541,22 @@ class SentryAndroidTest {
532541
assertTrue(optionsRef.eventProcessors.any { it is AnrV2EventProcessor })
533542
}
534543

535-
private fun prefillScopeCache(cacheDir: String) {
544+
private fun prefillScopeCache(options: SentryOptions, cacheDir: String) {
536545
val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() }
537-
File(scopeDir, BREADCRUMBS_FILENAME).writeText(
538-
"""
539-
[{
540-
"timestamp": "2009-11-16T01:08:47.000Z",
541-
"message": "Debug!",
542-
"type": "debug",
543-
"level": "debug"
544-
}]
545-
""".trimIndent()
546+
val queueFile = QueueFile.Builder(File(scopeDir, BREADCRUMBS_FILENAME)).build()
547+
val baos = ByteArrayOutputStream()
548+
options.serializer.serialize(
549+
Breadcrumb(DateUtils.getDateTime("2009-11-16T01:08:47.000Z")).apply {
550+
message = "Debug!"
551+
type = "debug"
552+
level = DEBUG
553+
},
554+
baos.writer()
546555
)
556+
queueFile.add(baos.toByteArray())
547557
File(scopeDir, TRANSACTION_FILENAME).writeText("\"MainActivity\"")
558+
File(scopeDir, REPLAY_FILENAME).writeText("\"afcb46b1140ade5187c4bbb5daa804df\"")
559+
File(options.getCacheDirPath(), "replay_afcb46b1140ade5187c4bbb5daa804df").mkdirs()
548560
}
549561

550562
private fun prefillOptionsCache(cacheDir: String) {

0 commit comments

Comments
 (0)