Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ open class App internal constructor(
var versionCode: Number?
) : JsonStream.Streamable {

private var buildUuidProvider: Provider<String?>? = buildUuid
@get:JvmName("getBuildUuidProvider\$internal")
internal var buildUuidProvider: Provider<String?>? = buildUuid

var buildUuid: String? = null
get() = field ?: buildUuidProvider?.getOrNull()
Expand Down
19 changes: 19 additions & 0 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -114,14 +115,20 @@ public static Map<String, String> getUser() {
@NonNull
@SuppressWarnings("unused")
public static Map<String, Object> getApp() {
HashMap<String, Object> data = new HashMap<>();
AppDataCollector source = getClient().getAppDataCollector();
AppWithState app = source.generateAppWithState();

Provider<String> buildUuidProvider = app.getBuildUuidProvider$internal();
String buildUuid = buildUuidProvider != null && buildUuidProvider.isComplete()
? buildUuidProvider.getOrNull()
: null;

HashMap<String, Object> 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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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<String?>() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading