diff --git a/CHANGELOG.md b/CHANGELOG.md index 7192558727..ed15772427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## TBD + +### Enhancements + +* Build UUIDs derived from dex file signatures no longer block NDK startup, reducing the overall startup time. + [#2401](https://github.com/bugsnag/bugsnag-android/pull/2401) + ## 6.25.0 (2026-03-02) ### Enhancements diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt index 78c9a3d5ab..96238b4480 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt @@ -50,7 +50,8 @@ open class App internal constructor( var versionCode: Number? ) : JsonStream.Streamable { - private var buildUuidProvider: Provider? = buildUuid + @get:JvmName("getBuildUuidProvider\$internal") + internal var buildUuidProvider: Provider? = buildUuid var buildUuid: String? = null get() = field ?: buildUuidProvider?.getOrNull() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index a248fc1735..e15c651ee7 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -427,6 +427,25 @@ void setupNdkPlugin() { clientObservable.postNdkInstall(immutableConfig, lastRunInfoPath, crashes); syncInitialState(); clientObservable.postNdkDeliverPending(); + + // If the buildUuid is still being computed (DexBuildIdGenerator running on IO thread), + // schedule a deferred SynchronizeState so the NDK layer picks up the value once ready. + Provider buildUuid = immutableConfig.getBuildUuid(); + if (buildUuid != null && !buildUuid.isComplete()) { + try { + bgTaskService.submitTask(TaskType.IO, new Runnable() { + @Override + public void run() { + // This blocks on the IO thread until dex generation finishes, + // then syncs the result to the NDK layer. + immutableConfig.getBuildUuid().getOrNull(); + clientObservable.postSynchronizeState(); + } + }); + } catch (RejectedExecutionException exc) { + logger.w("Failed to schedule deferred NDK build UUID sync", exc); + } + } } private boolean setupNdkDirectory() { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt index 4811f29578..0f5d20c9e7 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -18,7 +18,8 @@ internal class ClientObservable : BaseObservable() { conf.apiKey, conf.enabledErrorTypes.ndkCrashes, conf.appVersion, - conf.buildUuid?.getOrNull(), + if (conf.buildUuid?.isComplete == true) conf.buildUuid.getOrNull() + else null, conf.releaseStage, lastRunInfoPath, consecutiveLaunchCrashes, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java index 4b55144127..d1e18cee27 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java @@ -2,6 +2,7 @@ import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.JsonHelper; +import com.bugsnag.android.internal.dag.Provider; import android.annotation.SuppressLint; @@ -114,14 +115,20 @@ public static Map getUser() { @NonNull @SuppressWarnings("unused") public static Map getApp() { - HashMap data = new HashMap<>(); AppDataCollector source = getClient().getAppDataCollector(); AppWithState app = source.generateAppWithState(); + + Provider buildUuidProvider = app.getBuildUuidProvider$internal(); + String buildUuid = buildUuidProvider != null && buildUuidProvider.isComplete() + ? buildUuidProvider.getOrNull() + : null; + + HashMap data = new HashMap<>(); data.put("version", app.getVersion()); data.put("releaseStage", app.getReleaseStage()); data.put("id", app.getId()); data.put("type", app.getType()); - data.put("buildUUID", app.getBuildUuid()); + data.put("buildUUID", buildUuid); data.put("duration", app.getDuration()); data.put("durationInForeground", app.getDurationInForeground()); data.put("versionCode", app.getVersionCode()); diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt index a1b3626e7a..d236eff30e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt @@ -1,7 +1,11 @@ package com.bugsnag.android import com.bugsnag.android.internal.StateObserver +import com.bugsnag.android.internal.convertToImmutableConfig +import com.bugsnag.android.internal.dag.RunnableProvider +import com.bugsnag.android.internal.dag.ValueProvider import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -22,12 +26,61 @@ class ClientObservableTest { @Test fun postNdkInstall() { - clientObservable.postNdkInstall(BugsnagTestUtils.generateImmutableConfig(), "/foo", 0) clientObservable.addObserver( StateObserver { assertTrue(it is StateEvent.Install) } ) + clientObservable.postNdkInstall(BugsnagTestUtils.generateImmutableConfig(), "/foo", 0) + } + + @Test + fun postNdkInstallBuildUuidNull() { + // When buildUuid is null, Install event should have null buildUuid + val config = BugsnagTestUtils.generateImmutableConfig() + var installEvent: StateEvent.Install? = null + clientObservable.addObserver { + if (it is StateEvent.Install) installEvent = it + } + clientObservable.postNdkInstall(config, "/foo", 0) + assertNull(installEvent?.buildUuid) + } + + @Test + fun postNdkInstallBuildUuidComplete() { + // When buildUuid is a ValueProvider (already complete), Install should include it + val config = convertToImmutableConfig( + BugsnagTestUtils.generateConfiguration(), + ValueProvider("test-uuid") + ) + var installEvent: StateEvent.Install? = null + clientObservable.addObserver( + StateObserver { + if (it is StateEvent.Install) installEvent = it + } + ) + clientObservable.postNdkInstall(config, "/foo", 0) + assertEquals("test-uuid", installEvent?.buildUuid) + } + + @Test + fun postNdkInstallBuildUuidPending() { + // When buildUuid is a pending RunnableProvider, Install should have null buildUuid + val pendingProvider = object : RunnableProvider() { + override fun invoke(): String? = "deferred-uuid" + } + val config = convertToImmutableConfig( + BugsnagTestUtils.generateConfiguration(), + pendingProvider + ) + var installEvent: StateEvent.Install? = null + clientObservable.addObserver( + StateObserver { + if (it is StateEvent.Install) installEvent = it + } + ) + clientObservable.postNdkInstall(config, "/foo", 0) + assertNull(installEvent?.buildUuid) } @Test diff --git a/bugsnag-plugin-android-apphang/src/androidTest/java/com/bugsnag/android/LooperMonitorThreadTest.kt b/bugsnag-plugin-android-apphang/src/androidTest/java/com/bugsnag/android/LooperMonitorThreadTest.kt index d013d6101b..b54afca716 100644 --- a/bugsnag-plugin-android-apphang/src/androidTest/java/com/bugsnag/android/LooperMonitorThreadTest.kt +++ b/bugsnag-plugin-android-apphang/src/androidTest/java/com/bugsnag/android/LooperMonitorThreadTest.kt @@ -52,25 +52,6 @@ class LooperMonitorThreadTest { assertEquals("no AppHangs expected", 0, appHangCount) } - @Test - fun testBelowThresholdEvents() { - val countDownLatch = CountDownLatch(10) - val task = object : Runnable { - override fun run() { - JThread.sleep(APP_HANG_THRESHOLD / 2) - countDownLatch.countDown() - - if (countDownLatch.count > 0) { - handler.postDelayed(this, 1L) - } - } - } - handler.postDelayed(task, 1) - - countDownLatch.await() - assertEquals("no AppHangs expected", 0, appHangCount) - } - @Test fun appHang() { val countDownLatch = CountDownLatch(1)