Skip to main content

Memory & thread leaks

note

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:

app/build.gradle.kts
bugsee {
appToken("<your-app-token>")

leak {
enabled.set(true)
}
}

Without the Gradle plugin, declare the extension manually:

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-retained Activity is reported.
  • FragmentsFragmentManager.FragmentLifecycleCallbacks.onFragmentDestroyed.
  • ViewModelsViewModel.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:

ModeWhat you getWhen 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_ONLYA 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_EVERYWHEREShark 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

OptionManifest meta-data keyTypeDefault
Master switchcom.bugsee.option.detect.memory_leaksbooleantrue (when module present)
Modecom.bugsee.option.detect.memory_leaks.modeenumFAST
Deep-dump sample ratecom.bugsee.option.detect.memory_leaks.deep_sample_ratefloat [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.

<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" />

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).

MethodSignatureWhat it watches
watchObjectvoid 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.
watchNavControllervoid 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.
watchCompositionObject 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.
reportStrictModeViolationvoid 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.
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());
}

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

OptionManifest meta-data keyTypeDefault
Master switchcom.bugsee.option.detect.thread_leaksbooleanfalse
<meta-data android:name="com.bugsee.option.detect.thread_leaks"
android:value="true" />

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

Found an issue, typo, or wrong statement on this page? Report it now →