diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a1115f8a..dce5e8ec3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Performance + +- Start the frame metrics thread lazily on first collection instead of during SDK init ([#5641](https://github.com/getsentry/sentry-java/pull/5641)) + ## 8.45.0 ### Features diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java index 241ab1e4cc..4f76a51e86 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java @@ -14,12 +14,14 @@ import android.view.Window; import androidx.annotation.RequiresApi; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SentryUUID; import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.ContextUtils; import io.sentry.android.core.SentryFramesDelayResult; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.lang.ref.WeakReference; import java.lang.reflect.Field; @@ -45,7 +47,8 @@ public final class SentryFrameMetricsCollector implements Application.ActivityLi private final @NotNull Set trackedWindows = new CopyOnWriteArraySet<>(); private final @NotNull ILogger logger; - private @Nullable Handler handler; + private volatile @Nullable Handler handler; + private final @NotNull AutoClosableReentrantLock handlerLock = new AutoClosableReentrantLock(); private @Nullable WeakReference currentWindow; private final @NotNull Map listenerMap = new ConcurrentHashMap<>(); @@ -113,12 +116,8 @@ public SentryFrameMetricsCollector( } isAvailable = true; - HandlerThread handlerThread = - new HandlerThread("io.sentry.android.core.internal.util.SentryFrameMetricsCollector"); - handlerThread.setUncaughtExceptionHandler( - (thread, e) -> logger.log(SentryLevel.ERROR, "Error during frames measurements.", e)); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); + // The frame metrics HandlerThread is started lazily on the first startCollection() call. + // Starting it here would block the main thread on HandlerThread.getLooper() during SDK init. // We have to register the lifecycle callback, even if no profile is started, otherwise when we // start a profile, we wouldn't have the current activity and couldn't get the frameMetrics. @@ -281,12 +280,34 @@ public void onActivityDestroyed(@NotNull Activity activity) {} if (!isAvailable) { return null; } + ensureHandlerThreadStarted(); final String uid = SentryUUID.generateSentryId(); listenerMap.put(uid, listener); trackCurrentWindow(); return uid; } + /** + * Lazily starts the background HandlerThread used to receive frame metrics. Deferred out of the + * constructor because {@link HandlerThread#getLooper()} blocks the caller (the main thread during + * SDK init) until the thread is ready, and the handler is only needed once collection starts. + */ + private void ensureHandlerThreadStarted() { + if (handler != null) { + return; + } + try (final @NotNull ISentryLifecycleToken ignored = handlerLock.acquire()) { + if (handler == null) { + final HandlerThread handlerThread = + new HandlerThread("io.sentry.android.core.internal.util.SentryFrameMetricsCollector"); + handlerThread.setUncaughtExceptionHandler( + (thread, e) -> logger.log(SentryLevel.ERROR, "Error during frames measurements.", e)); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + } + } + public void stopCollection(final @Nullable String listenerId) { if (!isAvailable) { return; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt index 02f65665a9..f90c07b70e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt @@ -141,6 +141,16 @@ class SentryFrameMetricsCollectorTest { assertNotNull(id) } + @Test + fun `handler thread is started lazily on first startCollection`() { + val collector = fixture.getSut(context) + // not started during construction (would block the main thread on getLooper at SDK init) + assertNull(collector.getProperty("handler")) + + collector.startCollection(mock()) + assertNotNull(collector.getProperty("handler")) + } + @Test fun `collector calls addOnFrameMetricsAvailableListener when an activity starts`() { val collector = fixture.getSut(context)