From 32134b8b64f85f8a2d3b8b82a6920e8ebbb66947 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 11 Jul 2023 16:00:23 +0200 Subject: [PATCH] added LazyEvaluator to evaluate a function lazily AndroidOptionsInitializer.installDefaultIntegrations now evaluate cache dir lazily --- .../core/AndroidOptionsInitializer.java | 8 ++-- .../core/SendCachedEnvelopeIntegration.java | 9 ++-- .../core/AndroidOptionsInitializerTest.kt | 19 +++++++++ .../core/SendCachedEnvelopeIntegrationTest.kt | 3 +- sentry/api/sentry.api | 9 ++++ .../java/io/sentry/util/LazyEvaluator.java | 42 +++++++++++++++++++ .../java/io/sentry/util/LazyEvaluatorTest.kt | 41 ++++++++++++++++++ 7 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/util/LazyEvaluator.java create mode 100644 sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 0cea294aba..f3cf00f96c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -26,6 +26,7 @@ import io.sentry.internal.gestures.GestureTargetLocator; import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; import io.sentry.transport.NoOpEnvelopeCache; +import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.io.File; import java.util.ArrayList; @@ -195,12 +196,13 @@ static void installDefaultIntegrations( // read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope // integrations below - final boolean hasStartupCrashMarker = AndroidEnvelopeCache.hasStartupCrashMarker(options); + LazyEvaluator startupCrashMarkerEvaluator = + new LazyEvaluator<>(() -> AndroidEnvelopeCache.hasStartupCrashMarker(options)); options.addIntegration( new SendCachedEnvelopeIntegration( new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath()), - hasStartupCrashMarker)); + startupCrashMarkerEvaluator)); // Integrations are registered in the same order. NDK before adding Watch outbox, // because sentry-native move files around and we don't want to watch that. @@ -220,7 +222,7 @@ static void installDefaultIntegrations( options.addIntegration( new SendCachedEnvelopeIntegration( new SendFireAndForgetOutboxSender(() -> options.getOutboxPath()), - hasStartupCrashMarker)); + startupCrashMarkerEvaluator)); // AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration // relies on AppState set by it diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java index bda8335822..e0d08325b4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java @@ -5,6 +5,7 @@ import io.sentry.SendCachedEnvelopeFireAndForgetIntegration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; @@ -16,13 +17,13 @@ final class SendCachedEnvelopeIntegration implements Integration { private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory; - private final boolean hasStartupCrashMarker; + private final @NotNull LazyEvaluator startupCrashMarkerEvaluator; public SendCachedEnvelopeIntegration( final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory, - final boolean hasStartupCrashMarker) { + final @NotNull LazyEvaluator startupCrashMarkerEvaluator) { this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required"); - this.hasStartupCrashMarker = hasStartupCrashMarker; + this.startupCrashMarkerEvaluator = startupCrashMarkerEvaluator; } @Override @@ -62,7 +63,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { } }); - if (hasStartupCrashMarker) { + if (startupCrashMarkerEvaluator.getValue()) { androidOptions .getLogger() .log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush."); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index bd99335482..7f48dcd7ef 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -22,6 +22,9 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.annotation.Config import java.io.File @@ -599,6 +602,22 @@ class AndroidOptionsInitializerTest { assertFalse(fixture.sentryOptions.scopeObservers.any { it is PersistingScopeObserver }) } + @Test + fun `installDefaultIntegrations does not evaluate cacheDir or outboxPath when called`() { + val mockOptions = spy(fixture.sentryOptions) + AndroidOptionsInitializer.installDefaultIntegrations( + fixture.context, + mockOptions, + mock(), + mock(), + mock(), + false, + false + ) + verify(mockOptions, never()).outboxPath + verify(mockOptions, never()).cacheDirPath + } + @Config(sdk = [30]) @Test fun `AnrV2Integration added to integrations list for API 30 and above`() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt index 0bf3b271fa..36094f78eb 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt @@ -5,6 +5,7 @@ import io.sentry.ILogger import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory import io.sentry.SentryLevel.DEBUG +import io.sentry.util.LazyEvaluator import org.awaitility.kotlin.await import org.mockito.kotlin.any import org.mockito.kotlin.eq @@ -53,7 +54,7 @@ class SendCachedEnvelopeIntegrationTest { } ) - return SendCachedEnvelopeIntegration(factory, hasStartupCrashMarker) + return SendCachedEnvelopeIntegration(factory, LazyEvaluator { hasStartupCrashMarker }) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3ec27fa85b..f636d6775b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4151,6 +4151,15 @@ public final class io/sentry/util/JsonSerializationUtils { public static fun calendarToMap (Ljava/util/Calendar;)Ljava/util/Map; } +public final class io/sentry/util/LazyEvaluator { + public fun (Lio/sentry/util/LazyEvaluator$Evaluator;)V + public fun getValue ()Ljava/lang/Object; +} + +public abstract interface class io/sentry/util/LazyEvaluator$Evaluator { + public abstract fun evaluate ()Ljava/lang/Object; +} + public final class io/sentry/util/LogUtils { public fun ()V public static fun logNotInstanceOf (Ljava/lang/Class;Ljava/lang/Object;Lio/sentry/ILogger;)V diff --git a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java new file mode 100644 index 0000000000..5db376e710 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java @@ -0,0 +1,42 @@ +package io.sentry.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Class that evaluates a function lazily. It means the evaluator function is called only when + * getValue is called, and it's cached. + */ +@ApiStatus.Internal +public final class LazyEvaluator { + private @Nullable T value = null; + private final @NotNull Evaluator evaluator; + + /** + * Class that evaluates a function lazily. It means the evaluator function is called only when + * getValue is called, and it's cached. + * + * @param evaluator The function to evaluate. + */ + public LazyEvaluator(final @NotNull Evaluator evaluator) { + this.evaluator = evaluator; + } + + /** + * Executes the evaluator function and caches its result, so that it's called only once. + * + * @return The result of the evaluator function. + */ + public synchronized @NotNull T getValue() { + if (value == null) { + value = evaluator.evaluate(); + } + return value; + } + + public interface Evaluator { + @NotNull + T evaluate(); + } +} diff --git a/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt b/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt new file mode 100644 index 0000000000..8f0e3bc0a7 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt @@ -0,0 +1,41 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertEquals + +class LazyEvaluatorTest { + + class Fixture { + var count = 0 + + fun getSut(): LazyEvaluator { + count = 0 + return LazyEvaluator { ++count } + } + } + + private val fixture = Fixture() + + @Test + fun `does not evaluate on instantiation`() { + fixture.getSut() + assertEquals(0, fixture.count) + } + + @Test + fun `evaluator is called on getValue`() { + val evaluator = fixture.getSut() + assertEquals(0, fixture.count) + assertEquals(1, evaluator.value) + assertEquals(1, fixture.count) + } + + @Test + fun `evaluates only once`() { + val evaluator = fixture.getSut() + assertEquals(0, fixture.count) + assertEquals(1, evaluator.value) + assertEquals(1, evaluator.value) + assertEquals(1, fixture.count) + } +}