Reporting app hangs

When your app fails to respond to interactions in real time it causes user frustration and can lead them to abandon your app altogether.

An app hang or freeze is a specific instance of the main thread failing to respond in a reasonable amount of time and can be caused by performing CPU intensive work or performing blocking I/O on the main thread. If your app hangs for 5 seconds or longer, it will trigger an Application Not Responding (ANR) event, which is reported to your BugSnag dashboard by default.

You can also use the bugsnag-plugin-android-apphang plugin to detect and report app hangs that last for a shorter period of time, helping you to identify and fix performance issues before they impact your users. This plugin can either be considered an alternative to ANR detection, or can be used alongside it to provide additional visibility into shorter app hangs. Both signals will be detected and reported to your BugSnag dashboard, with hangs typically being reported as breadcrumbs leading up to the ANR event.

How it works

By monitoring the main thread looper, the app hang plugin sends a stack trace of the main thread when a processing delay over more than the configured threshold (appHangThresholdMillis) is detected. This is 3 seconds by default.

The plugin can optionally report a second “representative” stacktrace that it obtains by capturing stacktrace samples taken from when a small delay in the thread is first detected until the full hang threshold is reached. These samples are aggregated so that the most frequently sampled path is reported (if the full hang threshold is reached). If the thread recovers – a heartbeat is successfully detected before appHangThresholdMillis – the stack sampling is stopped and the data is discarded.

This additional stacktrace helps when the hang causes are not simple deadlocks: indicating where most of the activity occurred, rather than just the state at the point of detection. However it does introduce additional overhead on the blocked thread and so is disabled by default. The representative stacktrace is reported as well as the final stacktrace at the point of the hang and is shown as the second (“caused by”) exception in the dashboard and is used for grouping purposes.

For more information on this, see the stackSamplingThresholdMillis and stackSamplingIntervalMillis configuration options below.

Installation

Install bugsnag-plugin-android-apphang on devices running Android 11+ (API 30) by adding the dependency to your app/build.gradle or build.gradle.kts file:

dependencies {
  implementation("com.bugsnag:bugsnag-plugin-android-apphang:6.+")
}
dependencies {
  implementation("com.bugsnag:bugsnag-plugin-android-apphang:6.+")
}

Then add the plugin to your configuration:

BugsnagAppHangPlugin bugsnagAppHangPlugin = new BugsnagAppHangPlugin();
Configuration config = Configuration.load(this);
config.addPlugin(bugsnagAppHangPlugin);
Bugsnag.start(this, config);
Bugsnag.start(this, Configuration.load(this).apply {
  addPlugin(BugsnagAppHangPlugin())
})

Please note that v6.0.0 or above of bugsnag-android-core is required for this plugin.

Configuration

The plugin can be configured by passing it an AppHangPluginConfiguration object with one or more of the options detailed below. By default, stack sampling is disabled and the plugin will trigger an event when a hang of 3 seconds or more is detected.

See enabledErrorTypes for information on how to disable ANR detection if you wish to only report app hangs.

appHangThresholdMillis

The length of the app hang (in milliseconds) that will trigger an event:

AppHangConfiguration appHangPluginConfiguration = new AppHangConfiguration();
appHangPluginConfiguration.setAppHangThresholdMillis(5000);
Configuration config = Configuration.load(this);
config.addPlugin(new BugsnagAppHangPlugin(appHangPluginConfiguration));
Bugsnag.start(this, config);
Bugsnag.start(Configuration.load(this).apply {
  addPlugin(
    BugsnagAppHangPlugin(
      AppHangConfiguration(
        appHangThresholdMillis = 5000
      )
    )
  )
})

The default value is 3000 (3 seconds).

stackSamplingThresholdMillis and stackSamplingIntervalMillis

Defines how long the plugin will wait before starting to capture stack trace samples and the frequency with which stack samples are taken. stackSamplingThresholdMillis defaults to 0, which disables stack sampling.

AppHangConfiguration appHangPluginConfiguration = new AppHangConfiguration();
appHangPluginConfiguration.setStackSamplingThresholdMillis(1000);
appHangPluginConfiguration.setStackSamplingIntervalMillis(25);
Configuration config = Configuration.load(this);
config.addPlugin(new BugsnagAppHangPlugin(appHangPluginConfiguration));
Bugsnag.start(this, config);
Bugsnag.start(Configuration.load(this).apply {
  addPlugin(
    BugsnagAppHangPlugin(
      AppHangConfiguration(
        stackSamplingThresholdMillis = 1000,
        stackSamplingIntervalMillis = 25
      )
    )
  )
})

stackSamplingThresholdMillis must be less than the appHangThresholdMillis and ideally allows plenty of time for samples to be taken. A value of 1000 (1 second) is typically reasonable as it is long enough for a user to notice a pause and leaves time for the app to either recover or report a hang with a useful number of samples.

stackSamplingIntervalMillis defines the frequency with which stack samples are taken once stackSamplingThresholdMillis has passed and defaults to 50 milliseconds. This is a best-effort time and the real interval will vary depending on the system load (which may be high depending on the reason for the blocked thread). Setting stackSamplingIntervalMillis to smaller values can improve the quality of the reported error, but also increases the likelihood of full hang threshold being reached and an app hang triggered as each sample pauses the monitored thread briefly to take a stack trace.

watchedLooper

The Looper instance to monitor for app hangs. By default, this is set to the main thread looper:

AppHangConfiguration appHangPluginConfiguration = new AppHangConfiguration();
appHangPluginConfiguration.setWatchedLooper(Looper.myLooper());
BugsnagAppHangPlugin bugsnagAppHangPlugin =
  new BugsnagAppHangPlugin(appHangPluginConfiguration);
Configuration config = Configuration.load(this);
config.addPlugin(bugsnagAppHangPlugin);
Bugsnag.start(this, config);
Bugsnag.start(Configuration.load(this).apply {
  addPlugin(
    BugsnagAppHangPlugin(
      AppHangConfiguration(
        watchedLooper = Looper.myLooper()
      )
    )
  )
})