diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6bc7c535..0cbed67dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified - Fix ensure Application Context is used even when SDK is initialized via Activity Context ([#3669](https://github.com/getsentry/sentry-java/pull/3669)) - Fix potential ANRs due to `Calendar.getInstance` usage in Breadcrumbs constructor ([#3736](https://github.com/getsentry/sentry-java/pull/3736)) +- Lazily initialize heavy `SentryOptions` members to avoid ANRs on app start ([#3749](https://github.com/getsentry/sentry-java/pull/3749)) *Breaking changes*: diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index fdab9f442d..0ea3fad6ab 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -13,6 +13,7 @@ import android.graphics.Rect import android.graphics.RectF import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import android.util.Log import android.view.PixelCopy import android.view.View import android.view.ViewGroup @@ -101,6 +102,7 @@ internal class ScreenshotRecorder( Bitmap.Config.ARGB_8888 ) + val timeStart = System.nanoTime() // postAtFrontOfQueue to ensure the view hierarchy and bitmap are ase close in-sync as possible mainLooperHandler.post { try { @@ -123,6 +125,8 @@ internal class ScreenshotRecorder( val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options) root.traverse(viewHierarchy) + val timeEnd = System.nanoTime() + Log.e("TIME", String.format("%.2f", ((timeEnd - timeStart) / 1_000_000.0))) recorder.submitSafely(options, "screenshot_recorder.redact") { val canvas = Canvas(bitmap) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 8876efd66d..997fa5ff55 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -167,6 +167,5 @@ - diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 530d8241f5..059262d2c3 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -315,7 +315,7 @@ public abstract interface class io/sentry/EventProcessor { } public final class io/sentry/ExperimentalOptions { - public fun ()V + public fun (Z)V public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V } @@ -2712,8 +2712,8 @@ public final class io/sentry/SentryReplayEvent$ReplayType$Deserializer : io/sent public final class io/sentry/SentryReplayOptions { public static final field IMAGE_VIEW_CLASS_NAME Ljava/lang/String; public static final field TEXT_VIEW_CLASS_NAME Ljava/lang/String; - public fun ()V public fun (Ljava/lang/Double;Ljava/lang/Double;)V + public fun (Z)V public fun addIgnoreViewClass (Ljava/lang/String;)V public fun addRedactViewClass (Ljava/lang/String;)V public fun getErrorReplayDuration ()J @@ -5698,6 +5698,7 @@ public final class io/sentry/util/JsonSerializationUtils { public final class io/sentry/util/LazyEvaluator { public fun (Lio/sentry/util/LazyEvaluator$Evaluator;)V public fun getValue ()Ljava/lang/Object; + public fun setValue (Ljava/lang/Object;)V } public abstract interface class io/sentry/util/LazyEvaluator$Evaluator { diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index f587996bd8..4a0e7de78d 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -9,7 +9,11 @@ *

Beware that experimental options can change at any time. */ public final class ExperimentalOptions { - private @NotNull SentryReplayOptions sessionReplay = new SentryReplayOptions(); + private @NotNull SentryReplayOptions sessionReplay; + + public ExperimentalOptions(final boolean empty) { + this.sessionReplay = new SentryReplayOptions(empty); + } @NotNull public SentryReplayOptions getSessionReplay() { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3ff84c48de..61c721847c 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -19,13 +19,13 @@ import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.transport.NoOpTransportGate; +import io.sentry.util.LazyEvaluator; import io.sentry.util.Platform; import io.sentry.util.SampleRateUtils; import io.sentry.util.StringUtils; import io.sentry.util.thread.IMainThreadChecker; import io.sentry.util.thread.NoOpMainThreadChecker; import java.io.File; -import java.net.Proxy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -118,11 +118,13 @@ public class SentryOptions { /** minimum LogLevel to be used if debug is enabled */ private @NotNull SentryLevel diagnosticLevel = DEFAULT_DIAGNOSTIC_LEVEL; - /** Envelope reader interface */ - private @NotNull IEnvelopeReader envelopeReader = new EnvelopeReader(new JsonSerializer(this)); - /** Serializer interface to serialize/deserialize json events */ - private @NotNull ISerializer serializer = new JsonSerializer(this); + private final @NotNull LazyEvaluator serializer = + new LazyEvaluator<>(() -> new JsonSerializer(this)); + + /** Envelope reader interface */ + private final @NotNull LazyEvaluator envelopeReader = + new LazyEvaluator<>(() -> new EnvelopeReader(serializer.getValue())); /** Max depth when serializing object graphs with reflection. * */ private int maxDepth = 100; @@ -416,7 +418,8 @@ public class SentryOptions { /** Date provider to retrieve the current date from. */ @ApiStatus.Internal - private @NotNull SentryDateProvider dateProvider = new SentryAutoDateProvider(); + private final @NotNull LazyEvaluator dateProvider = + new LazyEvaluator<>(() -> new SentryAutoDateProvider()); private final @NotNull List performanceCollectors = new ArrayList<>(); @@ -479,7 +482,7 @@ public class SentryOptions { @ApiStatus.Experimental private @Nullable Cron cron = null; - private final @NotNull ExperimentalOptions experimental = new ExperimentalOptions(); + private final @NotNull ExperimentalOptions experimental; private @NotNull ReplayController replayController = NoOpReplayController.getInstance(); @@ -605,7 +608,7 @@ public void setDiagnosticLevel(@Nullable final SentryLevel diagnosticLevel) { * @return the serializer */ public @NotNull ISerializer getSerializer() { - return serializer; + return serializer.getValue(); } /** @@ -614,7 +617,7 @@ public void setDiagnosticLevel(@Nullable final SentryLevel diagnosticLevel) { * @param serializer the serializer */ public void setSerializer(@Nullable ISerializer serializer) { - this.serializer = serializer != null ? serializer : NoOpSerializer.getInstance(); + this.serializer.setValue(serializer != null ? serializer : NoOpSerializer.getInstance()); } /** @@ -636,12 +639,12 @@ public void setMaxDepth(int maxDepth) { } public @NotNull IEnvelopeReader getEnvelopeReader() { - return envelopeReader; + return envelopeReader.getValue(); } public void setEnvelopeReader(final @Nullable IEnvelopeReader envelopeReader) { - this.envelopeReader = - envelopeReader != null ? envelopeReader : NoOpEnvelopeReader.getInstance(); + this.envelopeReader.setValue( + envelopeReader != null ? envelopeReader : NoOpEnvelopeReader.getInstance()); } /** @@ -2212,7 +2215,7 @@ public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { /** Returns the current {@link SentryDateProvider} that is used to retrieve the current date. */ @ApiStatus.Internal public @NotNull SentryDateProvider getDateProvider() { - return dateProvider; + return dateProvider.getValue(); } /** @@ -2223,7 +2226,7 @@ public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { */ @ApiStatus.Internal public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { - this.dateProvider = dateProvider; + this.dateProvider.setValue(dateProvider); } /** @@ -2540,6 +2543,7 @@ public SentryOptions() { * @param empty if options should be empty. */ private SentryOptions(final boolean empty) { + experimental = new ExperimentalOptions(empty); if (!empty) { // SentryExecutorService should be initialized before any // SendCachedEventFireAndForgetIntegration diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 7656b088a1..097f72c921 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -96,14 +96,16 @@ public enum SentryReplayQuality { /** The maximum duration of a full session replay, defaults to 1h. */ private long sessionDuration = 60 * 60 * 1000L; - public SentryReplayOptions() { - setRedactAllText(true); - setRedactAllImages(true); + public SentryReplayOptions(final boolean empty) { + if (!empty) { + setRedactAllText(true); + setRedactAllImages(true); + } } public SentryReplayOptions( final @Nullable Double sessionSampleRate, final @Nullable Double onErrorSampleRate) { - this(); + this(false); this.sessionSampleRate = sessionSampleRate; this.onErrorSampleRate = onErrorSampleRate; } diff --git a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java index dbb6a49c19..d48cc3108d 100644 --- a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java +++ b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java @@ -10,6 +10,7 @@ import io.sentry.SentryOptions; import io.sentry.Session; import io.sentry.clientreport.DiscardReason; +import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.io.BufferedInputStream; import java.io.BufferedReader; @@ -36,8 +37,9 @@ abstract class CacheStrategy { @SuppressWarnings("CharsetObjectCanBeUsed") protected static final Charset UTF_8 = Charset.forName("UTF-8"); - protected final @NotNull SentryOptions options; - protected final @NotNull ISerializer serializer; + protected @NotNull SentryOptions options; + protected final @NotNull LazyEvaluator serializer = + new LazyEvaluator<>(() -> options.getSerializer()); protected final @NotNull File directory; private final int maxSize; @@ -48,7 +50,6 @@ abstract class CacheStrategy { Objects.requireNonNull(directoryPath, "Directory is required."); this.options = Objects.requireNonNull(options, "SentryOptions is required."); - this.serializer = options.getSerializer(); this.directory = new File(directoryPath); this.maxSize = maxSize; @@ -177,7 +178,7 @@ private void moveInitFlagIfNecessary( && currentSession.getSessionId().equals(session.getSessionId())) { session.setInitAsTrue(); try { - newSessionItem = SentryEnvelopeItem.fromSession(serializer, session); + newSessionItem = SentryEnvelopeItem.fromSession(serializer.getValue(), session); // remove item from envelope items so we can replace with the new one that has the // init flag true itemsIterator.remove(); @@ -216,7 +217,7 @@ private void moveInitFlagIfNecessary( private @Nullable SentryEnvelope readEnvelope(final @NotNull File file) { try (final InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { - return serializer.deserializeEnvelope(inputStream); + return serializer.getValue().deserializeEnvelope(inputStream); } catch (IOException e) { options.getLogger().log(ERROR, "Failed to deserialize the envelope.", e); } @@ -258,7 +259,7 @@ private boolean isSessionType(final @Nullable SentryEnvelopeItem item) { try (final Reader reader = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { - return serializer.deserialize(reader, Session.class); + return serializer.getValue().deserialize(reader, Session.class); } catch (Throwable e) { options.getLogger().log(ERROR, "Failed to deserialize the session.", e); } @@ -268,7 +269,7 @@ private boolean isSessionType(final @Nullable SentryEnvelopeItem item) { private void saveNewEnvelope( final @NotNull SentryEnvelope envelope, final @NotNull File file, final long timestamp) { try (final OutputStream outputStream = new FileOutputStream(file)) { - serializer.serialize(envelope, outputStream); + serializer.getValue().serialize(envelope, outputStream); // we need to set the same timestamp so the sorting from oldest to newest wont break. file.setLastModified(timestamp); } catch (Throwable e) { diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 3be857a4b2..82636ac6c1 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -116,7 +116,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi try (final Reader reader = new BufferedReader( new InputStreamReader(new FileInputStream(currentSessionFile), UTF_8))) { - final Session session = serializer.deserialize(reader, Session.class); + final Session session = serializer.getValue().deserialize(reader, Session.class); if (session != null) { writeSessionToDisk(previousSessionFile, session); } @@ -204,7 +204,7 @@ private void tryEndPreviousSession(final @NotNull Hint hint) { try (final Reader reader = new BufferedReader( new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { - final Session session = serializer.deserialize(reader, Session.class); + final Session session = serializer.getValue().deserialize(reader, Session.class); if (session != null) { final AbnormalExit abnormalHint = (AbnormalExit) sdkHint; final @Nullable Long abnormalExitTimestamp = abnormalHint.timestamp(); @@ -263,7 +263,7 @@ private void updateCurrentSession( try (final Reader reader = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { - final Session session = serializer.deserialize(reader, Session.class); + final Session session = serializer.getValue().deserialize(reader, Session.class); if (session == null) { options .getLogger() @@ -304,7 +304,7 @@ private void writeEnvelopeToDisk( } try (final OutputStream outputStream = new FileOutputStream(file)) { - serializer.serialize(envelope, outputStream); + serializer.getValue().serialize(envelope, outputStream); } catch (Throwable e) { options .getLogger() @@ -324,7 +324,7 @@ private void writeSessionToDisk(final @NotNull File file, final @NotNull Session try (final OutputStream outputStream = new FileOutputStream(file); final Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, UTF_8))) { - serializer.serialize(session, writer); + serializer.getValue().serialize(session, writer); } catch (Throwable e) { options .getLogger() @@ -388,7 +388,7 @@ public void discard(final @NotNull SentryEnvelope envelope) { for (final File file : allCachedEnvelopes) { try (final InputStream is = new BufferedInputStream(new FileInputStream(file))) { - ret.add(serializer.deserializeEnvelope(is)); + ret.add(serializer.getValue().deserializeEnvelope(is)); } catch (FileNotFoundException e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/clientreport/AtomicClientReportStorage.java b/sentry/src/main/java/io/sentry/clientreport/AtomicClientReportStorage.java index 2e7f0c27a7..fd1f9a9de9 100644 --- a/sentry/src/main/java/io/sentry/clientreport/AtomicClientReportStorage.java +++ b/sentry/src/main/java/io/sentry/clientreport/AtomicClientReportStorage.java @@ -1,10 +1,12 @@ package io.sentry.clientreport; import io.sentry.DataCategory; +import io.sentry.util.LazyEvaluator; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import org.jetbrains.annotations.ApiStatus; @@ -14,25 +16,28 @@ @ApiStatus.Internal final class AtomicClientReportStorage implements IClientReportStorage { - private final @NotNull Map lostEventCounts; + private final @NotNull LazyEvaluator> lostEventCounts = + new LazyEvaluator<>( + () -> { + final Map modifyableEventCountsForInit = + new ConcurrentHashMap<>(); - public AtomicClientReportStorage() { - final Map modifyableEventCountsForInit = new ConcurrentHashMap<>(); + for (final DiscardReason discardReason : DiscardReason.values()) { + for (final DataCategory category : DataCategory.values()) { + modifyableEventCountsForInit.put( + new ClientReportKey(discardReason.getReason(), category.getCategory()), + new AtomicLong(0)); + } + } - for (final DiscardReason discardReason : DiscardReason.values()) { - for (final DataCategory category : DataCategory.values()) { - modifyableEventCountsForInit.put( - new ClientReportKey(discardReason.getReason(), category.getCategory()), - new AtomicLong(0)); - } - } + return Collections.unmodifiableMap(modifyableEventCountsForInit); + }); - lostEventCounts = Collections.unmodifiableMap(modifyableEventCountsForInit); - } + public AtomicClientReportStorage() {} @Override public void addCount(ClientReportKey key, Long count) { - final @Nullable AtomicLong quantity = lostEventCounts.get(key); + final @Nullable AtomicLong quantity = lostEventCounts.getValue().get(key); if (quantity != null) { quantity.addAndGet(count); @@ -43,7 +48,8 @@ public void addCount(ClientReportKey key, Long count) { public List resetCountsAndGet() { final List discardedEvents = new ArrayList<>(); - for (final Map.Entry entry : lostEventCounts.entrySet()) { + Set> entrySet = lostEventCounts.getValue().entrySet(); + for (final Map.Entry entry : entrySet) { final Long quantity = entry.getValue().getAndSet(0); if (quantity > 0) { discardedEvents.add( diff --git a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java index 5db376e710..d540cbe508 100644 --- a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java +++ b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java @@ -10,7 +10,8 @@ */ @ApiStatus.Internal public final class LazyEvaluator { - private @Nullable T value = null; + + private volatile @Nullable T value = null; private final @NotNull Evaluator evaluator; /** @@ -28,13 +29,25 @@ public LazyEvaluator(final @NotNull Evaluator evaluator) { * * @return The result of the evaluator function. */ - public synchronized @NotNull T getValue() { + public @NotNull T getValue() { if (value == null) { - value = evaluator.evaluate(); + synchronized (this) { + if (value == null) { + value = evaluator.evaluate(); + } + } } + + //noinspection DataFlowIssue return value; } + public void setValue(final @Nullable T value) { + synchronized (this) { + this.value = value; + } + } + public interface Evaluator { @NotNull T evaluate(); diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index 01843dfc90..794a3dac09 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -7,7 +7,7 @@ class SentryReplayOptionsTest { @Test fun `uses medium quality as default`() { - val replayOptions = SentryReplayOptions() + val replayOptions = SentryReplayOptions(true) assertEquals(SentryReplayOptions.SentryReplayQuality.MEDIUM, replayOptions.quality) assertEquals(75_000, replayOptions.quality.bitRate) @@ -16,7 +16,7 @@ class SentryReplayOptionsTest { @Test fun `low quality`() { - val replayOptions = SentryReplayOptions().apply { quality = SentryReplayOptions.SentryReplayQuality.LOW } + val replayOptions = SentryReplayOptions(true).apply { quality = SentryReplayOptions.SentryReplayQuality.LOW } assertEquals(50_000, replayOptions.quality.bitRate) assertEquals(0.8f, replayOptions.quality.sizeScale) @@ -24,7 +24,7 @@ class SentryReplayOptionsTest { @Test fun `high quality`() { - val replayOptions = SentryReplayOptions().apply { quality = SentryReplayOptions.SentryReplayQuality.HIGH } + val replayOptions = SentryReplayOptions(true).apply { quality = SentryReplayOptions.SentryReplayQuality.HIGH } assertEquals(100_000, replayOptions.quality.bitRate) assertEquals(1.0f, replayOptions.quality.sizeScale) diff --git a/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt b/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt index e67a6616ce..3c3e6d18d0 100644 --- a/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt +++ b/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt @@ -108,13 +108,13 @@ class CacheStrategyTest { val files = createTempFilesSortByOldestToNewest() val okSession = createSessionMockData(Session.State.Ok, true) - val okEnvelope = SentryEnvelope.from(sut.serializer, okSession, null) - sut.serializer.serialize(okEnvelope, files[0].outputStream()) + val okEnvelope = SentryEnvelope.from(sut.serializer.value, okSession, null) + sut.serializer.value.serialize(okEnvelope, files[0].outputStream()) val updatedOkSession = okSession.clone() updatedOkSession.update(null, null, true) - val updatedOkEnvelope = SentryEnvelope.from(sut.serializer, updatedOkSession, null) - sut.serializer.serialize(updatedOkEnvelope, files[1].outputStream()) + val updatedOkEnvelope = SentryEnvelope.from(sut.serializer.value, updatedOkSession, null) + sut.serializer.value.serialize(updatedOkEnvelope, files[1].outputStream()) saveSessionToFile(files[2], sut, Session.State.Exited, null) @@ -178,17 +178,17 @@ class CacheStrategyTest { ) private fun getSessionFromFile(file: File, sut: CacheStrategy): Session { - val envelope = sut.serializer.deserializeEnvelope(file.inputStream()) + val envelope = sut.serializer.value.deserializeEnvelope(file.inputStream()) val item = envelope!!.items.first() val reader = InputStreamReader(ByteArrayInputStream(item.data), Charsets.UTF_8) - return sut.serializer.deserialize(reader, Session::class.java)!! + return sut.serializer.value.deserialize(reader, Session::class.java)!! } private fun saveSessionToFile(file: File, sut: CacheStrategy, state: Session.State = Session.State.Ok, init: Boolean? = true) { val okSession = createSessionMockData(state, init) - val okEnvelope = SentryEnvelope.from(sut.serializer, okSession, null) - sut.serializer.serialize(okEnvelope, file.outputStream()) + val okEnvelope = SentryEnvelope.from(sut.serializer.value, okSession, null) + sut.serializer.value.serialize(okEnvelope, file.outputStream()) } private fun getOptionsWithRealSerializer(): SentryOptions {