Memory & thread leaks
Leak detection is shipped as the bugsee-android-leak extension module. The core bugsee-android artefact does not include it — you must add the extension explicitly (or via the Gradle plugin DSL).
The bugsee-android-leak extension ships two detectors that share a single Gradle module:
- Memory leaks — retained
Activity/Fragment/ViewModel/ Compose nodes. Default: on once the module is on the classpath. - Thread leaks — thread-group growth as a proxy for leaking
ThreadPoolExecutor/ unbounded task scheduling. Default: off; Phase 1 is Java-only.
Add the extension
The simplest path is the DSL toggle on the Bugsee Gradle plugin — it pulls in bugsee-android-leak at the matching version:
bugsee {
appToken("<your-app-token>")
leak {
enabled.set(true)
}
}
Without the Gradle plugin, declare the extension manually:
- build.gradle.kts
- build.gradle
dependencies {
implementation("com.bugsee:bugsee-android-leak:7.x.x")
}
dependencies {
implementation 'com.bugsee:bugsee-android-leak:7.x.x'
}
Both detectors auto-register at process start via the extension's ContentProvider — no code changes in Application.onCreate().
Memory leaks
The memory-leak detector watches the four most common Android leak surfaces and reports when an object is retained past its expected lifetime:
- Activities — watch-source instrumentation hooks
Application.ActivityLifecycleCallbacks.onActivityDestroyed. After a short grace period and a forced GC, any still-retainedActivityis reported. - Fragments —
FragmentManager.FragmentLifecycleCallbacks.onFragmentDestroyed. ViewModels —ViewModel.onCleared().- Compose — composables retained beyond their disposal hook.
Confirmed leaks are reported as non-fatal issues, each carrying the leak signature (in FAST mode) or a full leak trace (in DEEP_* modes).
Modes
Three modes trade off reporting fidelity against runtime cost:
| Mode | What you get | When to use |
|---|---|---|
FAST (default) | A dump-free leak signature: the class name of the retained object and its watch-source category, enough to surface "this Activity leaked" in the dashboard. | Production builds — minimal runtime cost. |
DEEP_DEBUG_ONLY | A Shark-powered heap-dump + leak trace, but only on debug builds. | Internal / QA channels where you want full leak traces but no production overhead. |
DEEP_EVERYWHERE | Shark heap-dump + leak trace on every variant. Gated by battery (≥ 20%) and a frequency cap so the same surface isn't dumped repeatedly. | Pre-release smoke tests where you want production-shape coverage. Beware: heap-dumps are tens of seconds long and freeze the process. |
Configuration
| Option | Manifest meta-data key | Type | Default |
|---|---|---|---|
| Master switch | com.bugsee.option.detect.memory_leaks | boolean | true (when module present) |
| Mode | com.bugsee.option.detect.memory_leaks.mode | enum | FAST |
| Deep-dump sample rate | com.bugsee.option.detect.memory_leaks.deep_sample_rate | float [0, 1] | 1.0 |
The mode is the MemoryLeakMode enum (FAST, DEEP_DEBUG_ONLY, DEEP_EVERYWHERE). The manifest meta-data form accepts either the enum constant name (FAST) or its camelCase wire alias (Fast); the programmatic Bugsee.launch(...) map must use the typed MemoryLeakMode enum value.
- AndroidManifest.xml
- Programmatic
<meta-data android:name="com.bugsee.option.detect.memory_leaks.mode"
android:value="DEEP_DEBUG_ONLY" />
<meta-data android:name="com.bugsee.option.detect.memory_leaks.deep_sample_rate"
android:value="0.25" />
import com.bugsee.library.leak.contracts.options.LeakOptions;
import com.bugsee.library.leak.MemoryLeakMode;
HashMap<String, Object> options = new HashMap<>();
options.put(LeakOptions.DetectMemoryLeaks, true);
options.put(LeakOptions.DetectMemoryLeaksMode, MemoryLeakMode.DEEP_DEBUG_ONLY);
options.put(LeakOptions.DetectMemoryLeaksDeepSampleRate, 0.25f); // dump only 25% of eligible leaks
Bugsee.launch(this, "<APP_TOKEN>", options);
The deep-dump sample rate gates per-leak eligibility for the heap dump. The frequency caps and battery threshold still apply on top — a low sample rate compounds with them, so use it primarily to budget heap-dump cost on DEEP_EVERYWHERE.
Manual watch APIs
The four auto-watch surfaces above cover most leaks, but some objects the SDK can't hook automatically — presenters, controllers, long-lived singletons, navigation back-stack entries, or composition-scoped objects. For those, ask the leak extension to watch them explicitly.
These methods live on the Leak contract interface, which you retrieve from the SDK through Bugsee.ext(Leak.class). The call returns null when the bugsee-android-leak module is not on the classpath, so always null-check the result — your code keeps compiling even if a downstream consumer drops the extension. Each method is a no-op unless memory-leak detection is actually running (DetectMemoryLeaks enabled).
| Method | Signature | What it watches |
|---|---|---|
watchObject | void watchObject(Object object, String description) | Any object that should soon become collectable. description is an optional human-readable label shown in the report (pass null to omit). If the object is still strongly reachable after it should have been GC'd, it's reported as a suspected leak. |
watchNavController | void watchNavController(Object navController) | A Jetpack Navigation controller's back-stack entries (each owns a ViewModelStore), checked for retention after they're popped. The parameter is typed Object so apps without Jetpack Navigation incur no dependency — pass an androidx.navigation.NavController. No-op if Navigation isn't on the classpath. |
watchComposition | Object watchComposition(Object target) | A composition-scoped object. Returns a Compose RememberObserver (typed Object so apps without Compose incur no dependency) that you should remember { … } in the composition; when target leaves the composition it is checked for retention. Returns null if leak detection isn't running or the Compose runtime isn't on the classpath. |
reportStrictModeViolation | void reportStrictModeViolation(Throwable violation) | A StrictMode VM violation you forward from your own VmPolicy.Builder.penaltyListener(...). Bugsee never installs its own StrictMode policy. Leak-relevant violations (unclosed Closeables, unregistered receivers / ServiceConnections, leaked SQLite cursors, exceeded instance limits) surface as resource-leak reports with the violation's real acquisition stack. Non-leak violations are ignored. The parameter is typed Throwable (not the API 28 Violation type) so it's safe to reference on minSdk 21. |
- Java
- Kotlin
import com.bugsee.library.Bugsee;
import com.bugsee.library.contracts.extensions.Leak;
Leak leak = Bugsee.ext(Leak.class);
if (leak != null) {
// Watch an object you know should soon be collected.
leak.watchObject(myPresenter, "LoginPresenter after detach");
// Watch a Jetpack Navigation controller's popped back-stack entries.
leak.watchNavController(navController);
// Forward StrictMode VM violations from your own penalty listener.
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.penaltyListener(executor, leak::reportStrictModeViolation)
.build());
}
import com.bugsee.library.Bugsee
import com.bugsee.library.contracts.extensions.Leak
val leak = Bugsee.ext(Leak::class.java)
leak?.watchObject(myPresenter, "LoginPresenter after detach")
leak?.watchNavController(navController)
// In a composable, watch a composition-scoped object:
val observer = leak?.watchComposition(myScopedObject)
if (observer != null) {
remember { observer }
}
Thread leaks
The thread-leak detector watches the JVM's thread-group population and reports groups whose count grows steadily across snapshots — a proxy for a leaking ThreadPoolExecutor, an unbounded task scheduler, or a worker that never shuts down.
This is the Phase 1 Java-only implementation. A native pthread hook is planned for a later phase; today the detector only sees JVM threads.
A snapshot is taken every 30 s on the SDK's pool thread; thread groups whose count has grown across enough snapshots fire a single non-fatal issue report under the ThreadLeak domain. Dedup is per-group so a steadily-leaking pool is reported once, not on every snapshot.
Configuration
| Option | Manifest meta-data key | Type | Default |
|---|---|---|---|
| Master switch | com.bugsee.option.detect.thread_leaks | boolean | false |
- AndroidManifest.xml
- Programmatic
<meta-data android:name="com.bugsee.option.detect.thread_leaks"
android:value="true" />
HashMap<String, Object> options = new HashMap<>();
options.put(LeakOptions.DetectThreadLeaks, true);
Bugsee.launch(this, "<APP_TOKEN>", options);
Thread-leak detection is off by default because well-tuned background queues sit at a steady-state thread count that doesn't trip the detector, but ad-hoc thread creation patterns (new Thread(...) in a callback) commonly produce false positives during early integration. Enable it on internal / QA channels first to tune.
See also
- Configuration → Leak extension — the option-key reference for the leak extension.