diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a26ff514..375b9736 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -69,6 +69,10 @@ jobs: - name: Inject the CI crash driver into the example entry point run: cat .github/scripts/ci-crash-driver.js >> examples/sdk/reactNative/index.js + - name: Run SDK unit tests + working-directory: examples/sdk/reactNative/android + run: ./gradlew :backtrace_react-native:testDebugUnitTest -PnewArchEnabled=${{ matrix.new-arch }} --console=plain + - name: Build example app working-directory: examples/sdk/reactNative/android run: ./gradlew assembleRelease -PbtCiDebuggable -PnewArchEnabled=${{ matrix.new-arch }} -x uploadSourceMapsToBacktrace --console=plain diff --git a/examples/sdk/reactNative/android/app/src/main/java/com/reactnative/ErrorGenerator.java b/examples/sdk/reactNative/android/app/src/main/java/com/reactnative/ErrorGenerator.java index e019b13a..b56114fb 100644 --- a/examples/sdk/reactNative/android/app/src/main/java/com/reactnative/ErrorGenerator.java +++ b/examples/sdk/reactNative/android/app/src/main/java/com/reactnative/ErrorGenerator.java @@ -8,6 +8,8 @@ import java.io.FileReader; import java.io.IOException; +import android.os.Handler; +import android.os.Looper; import android.util.Log; public class ErrorGenerator extends ReactContextBaseJavaModule { @@ -25,6 +27,16 @@ public void throwError() throws IOException { readUserConfiguration(); } + @ReactMethod + public void blockMainThread(int durationMs) { + new Handler(Looper.getMainLooper()).post(() -> { + try { + Thread.sleep(durationMs); + } catch (InterruptedException ignored) { + } + }); + } + private void readUserConfiguration() throws IOException { // I know for sure this file is there (spoiler alert, it's not) File mConfiguration = new File("configuration.json"); diff --git a/examples/sdk/reactNative/index.js b/examples/sdk/reactNative/index.js index 79ca62ca..c54a45b2 100644 --- a/examples/sdk/reactNative/index.js +++ b/examples/sdk/reactNative/index.js @@ -15,6 +15,9 @@ BacktraceClient.initialize({ prop2: 123, }, }, + anr: { + enable: true, + }, database: { enable: true, captureNativeCrashes: true, diff --git a/examples/sdk/reactNative/metro.config.js b/examples/sdk/reactNative/metro.config.js index e4a564eb..6774a96b 100644 --- a/examples/sdk/reactNative/metro.config.js +++ b/examples/sdk/reactNative/metro.config.js @@ -9,6 +9,16 @@ const backtraceSourceMapProcessor = require('@backtrace/react-native/scripts/pro * @type {import('metro-config').MetroConfig} */ const config = { + resolver: { + blockList: [ + // a second react-native copy splits the JS event registry from the native one + new RegExp(path.resolve(__dirname, '../../../packages/react-native/node_modules/react-native').replace(/[/\\]/g, '[/\\\\]') + '[/\\\\].*'), + new RegExp(path.resolve(__dirname, '../../../node_modules/react-native').replace(/[/\\]/g, '[/\\\\]') + '[/\\\\].*'), + ], + extraNodeModules: { + 'react-native': path.resolve(__dirname, 'node_modules/react-native'), + }, + }, watchFolders: [ path.resolve('../../../packages/react-native'), path.resolve('../../../packages/react-native/node_modules'), diff --git a/examples/sdk/reactNative/src/actions/android/action.ts b/examples/sdk/reactNative/src/actions/android/action.ts index 05f43165..6476cb33 100644 --- a/examples/sdk/reactNative/src/actions/android/action.ts +++ b/examples/sdk/reactNative/src/actions/android/action.ts @@ -13,11 +13,7 @@ export const actions: DemoAction[] = __DEV__ throw new Error('Native modules are not enabled.'); } - await new Promise((res) => { - setTimeout(() => { - res(); - }, 1000); - }); + errorGenerator.blockMainThread(10000); }, }, { diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 0c5382b7..5e629c35 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -19,6 +19,7 @@ and easy, after which you can explore the rich set of Backtrace features. - [Application Stability Metrics](#application-stability-metrics) - [Metrics Configuration](#metrics-configuration) - [Metrics Usage](#metrics-usage) + - [ANR Detection](#anr-detection) - [Offline Database support](#offline-database-support) - [Database Configuration](#database-configuration) - [Native crash support](#native-crash-support) @@ -291,6 +292,43 @@ client.metrics?.send(); --- +### ANR Detection + +The Backtrace react-native SDK can detect Application Not Responding errors on Android and report them with the +`Hang` error type. The report carries the stack traces of every thread, with the blocked main thread as the +faulting one. + +Two detection mechanisms are available: + +- `BacktraceAnrType.Threshold` monitors the main thread from a separate thread. If the main thread remains + unresponsive for longer than the configured timeout (default: 5 seconds), an ANR is reported immediately, while + the application is still hung. It works on all supported Android versions. +- `BacktraceAnrType.ApplicationExit` retrieves the ANRs the system recorded for previous runs of the process from + `ApplicationExitInfo` and reports them on the next application start, using the thread dump the system captured + when it declared the ANR. The full dump is also attached as `anr-stacktrace.txt`. It requires API 30 or above + and survives the process kill. Reported records are remembered, so each ANR is reported once. + +```ts +import { BacktraceAnrType, BacktraceConfiguration } from '@backtrace/react-native'; + +const options: BacktraceConfiguration = { + url: SUBMISSION_URL, + anr: { + enable: true, + type: BacktraceAnrType.Threshold, + }, +}; +``` + +| Option Name | Type | Description | Default | Required? | +| ----------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------ | +| `enable` | Boolean | Determines if ANR detection is enabled. | `false` | | +| `type` | `BacktraceAnrType` | Detection mechanism: `Threshold` or `ApplicationExit`. | `Threshold` | | +| `timeout` | Number | Time in milliseconds the main thread stays blocked before an ANR is reported. Applies to the `Threshold` type only. | `5000` | | +| `disableWhenDebuggerAttached` | Boolean | When true, detection is disabled while a debugger is attached. Applies to the `Threshold` type only. | `false` | | + +--- + ### Offline database support The Backtrace react-native SDK can cache generated reports and crashes to local disk before sending them to Backtrace. diff --git a/packages/react-native/android/build.gradle b/packages/react-native/android/build.gradle index bb72b403..eb640544 100644 --- a/packages/react-native/android/build.gradle +++ b/packages/react-native/android/build.gradle @@ -81,6 +81,7 @@ dependencies { // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" + testImplementation "junit:junit:4.13.2" } if (isNewArchitectureEnabled()) { diff --git a/packages/react-native/android/src/main/java/backtrace/library/AnrExitInfoReader.java b/packages/react-native/android/src/main/java/backtrace/library/AnrExitInfoReader.java new file mode 100644 index 00000000..d946cde1 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/AnrExitInfoReader.java @@ -0,0 +1,147 @@ +package backtraceio.library; + +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import backtraceio.library.anr.ActivityManagerExitInfoProvider; +import backtraceio.library.anr.AppExitInfoDetailsExtractor; +import backtraceio.library.anr.ExitInfo; +import backtraceio.library.anr.ExitInfoStackTraceParser; +import backtraceio.library.anr.ProcessExitInfoProvider; +import backtraceio.library.anr.StackFrameMapper; + +class AnrExitInfoReader { + private final static transient String LOG_TAG = AnrExitInfoReader.class.getSimpleName(); + + private static final int ALL_EXIT_RECORDS = 0; + + private final Context context; + private final ProcessExitInfoProvider exitInfoProvider; + + AnrExitInfoReader(Context context) { + this(context, new ActivityManagerExitInfoProvider(context)); + } + + AnrExitInfoReader(Context context, ProcessExitInfoProvider exitInfoProvider) { + this.context = context; + this.exitInfoProvider = exitInfoProvider; + } + + WritableArray read(long sinceEpochMillis) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return new WritableNativeArray(); + } + return readAnrRecords(sinceEpochMillis); + } + + @RequiresApi(Build.VERSION_CODES.R) + private WritableArray readAnrRecords(long sinceEpochMillis) { + WritableArray records = new WritableNativeArray(); + + List exitInfos = this.exitInfoProvider.getHistoricalExitInfo( + this.context.getPackageName(), 0, ALL_EXIT_RECORDS); + List supportedReasons = this.exitInfoProvider.getSupportedTypesOfExitInfo(); + + // oldest first, so a caller advancing a last-seen timestamp cannot skip a record + List ordered = new ArrayList<>(exitInfos); + Collections.reverse(ordered); + + for (ExitInfo exitInfo : ordered) { + if (!supportedReasons.contains(exitInfo.getReason())) { + continue; + } + if (exitInfo.getTimestamp() <= sinceEpochMillis) { + continue; + } + records.pushMap(describe(exitInfo)); + } + + return records; + } + + @RequiresApi(Build.VERSION_CODES.R) + private WritableMap describe(ExitInfo exitInfo) { + String stackTrace = AppExitInfoDetailsExtractor.getStackTraceInfo(exitInfo); + + WritableMap record = new WritableNativeMap(); + // putInt would overflow epoch millis + record.putDouble("timestamp", (double) exitInfo.getTimestamp()); + record.putString("message", AppExitInfoDetailsExtractor.getANRMessage(exitInfo)); + record.putMap("attributes", toWritableMap(AppExitInfoDetailsExtractor.getANRAttributes(exitInfo))); + record.putString("stackTrace", stackTrace); + + Map parsed = parseStackTrace(stackTrace); + + StackTraceElement[] frames = ExitInfoStackTraceParser.parseMainThreadStackTrace(parsed); + if (frames.length > 0) { + record.putArray("mainThreadFrames", StackFrameMapper.toWritableFrames(frames)); + } + + WritableArray threads = toWritableThreads(ExitInfoStackTraceParser.parseOtherThreadStackTraces(parsed)); + if (threads.size() > 0) { + record.putArray("threads", threads); + } + return record; + } + + private Map parseStackTrace(String stackTrace) { + if (stackTrace == null || stackTrace.isEmpty()) { + return new HashMap<>(); + } + + try { + return ExitInfoStackTraceParser.parseANRStackTrace(stackTrace); + } catch (Exception e) { + Log.e(LOG_TAG, "Could not parse the ANR stack trace", e); + return new HashMap<>(); + } + } + + private WritableArray toWritableThreads(List threads) { + WritableArray result = new WritableNativeArray(); + for (ExitInfoStackTraceParser.ThreadStackTrace thread : threads) { + WritableMap map = new WritableNativeMap(); + map.putString("name", thread.getName()); + map.putArray("frames", StackFrameMapper.toWritableFrames(thread.getFrames())); + result.pushMap(map); + } + return result; + } + + private WritableMap toWritableMap(Map values) { + WritableMap map = new WritableNativeMap(); + for (Map.Entry entry : values.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + map.putString(key, (String) value); + } else if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else if (value instanceof Long || value instanceof Double || value instanceof Float) { + map.putDouble(key, ((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else if (value == null) { + map.putNull(key); + } else { + map.putString(key, value.toString()); + } + } + return map; + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/BacktraceAnrWatchdog.java b/packages/react-native/android/src/main/java/backtrace/library/BacktraceAnrWatchdog.java new file mode 100644 index 00000000..2aaf0e4d --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/BacktraceAnrWatchdog.java @@ -0,0 +1,103 @@ +package backtraceio.library; + +import android.os.Looper; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.module.annotations.ReactModule; + +import java.util.Map; + +import backtraceio.library.anr.AnrWatchdog; +import backtraceio.library.anr.StackFrameMapper; + +@ReactModule(name = BacktraceAnrWatchdog.NAME) +public class BacktraceAnrWatchdog extends ReactContextBaseJavaModule { + public static final String NAME = "BacktraceAnrWatchdog"; + public static final String ANR_DETECTED_EVENT = "BacktraceAnrDetected"; + + private AnrWatchdog watchdog; + + public BacktraceAnrWatchdog(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + @NonNull + public String getName() { + return NAME; + } + + @ReactMethod() + public void start(int timeout, boolean debug) { + if (this.watchdog != null) { + return; + } + + this.watchdog = new AnrWatchdog( + timeout > 0 ? timeout : AnrWatchdog.DEFAULT_ANR_TIMEOUT, + debug, + this::emitAnrDetected); + } + + @ReactMethod() + public void stop() { + if (this.watchdog == null) { + return; + } + + this.watchdog.stopMonitoring(); + this.watchdog = null; + } + + @Override + public void invalidate() { + stop(); + super.invalidate(); + } + + @ReactMethod() + public void addListener(String eventName) {} + + @ReactMethod() + public void removeListeners(Integer count) {} + + private void emitAnrDetected(Map allThreads) { + ReactApplicationContext context = getReactApplicationContext(); + if (!context.hasActiveReactInstance()) { + return; + } + + Thread mainThread = Looper.getMainLooper().getThread(); + StackTraceElement[] mainThreadFrames = allThreads.get(mainThread); + if (mainThreadFrames == null) { + mainThreadFrames = mainThread.getStackTrace(); + } + + WritableArray threads = new WritableNativeArray(); + for (Map.Entry entry : allThreads.entrySet()) { + if (entry.getKey() == mainThread) { + continue; + } + WritableMap thread = new WritableNativeMap(); + thread.putString("name", entry.getKey().getName()); + thread.putArray("frames", StackFrameMapper.toWritableFrames(entry.getValue())); + threads.pushMap(thread); + } + + WritableMap event = new WritableNativeMap(); + event.putString("stackTrace", StackFrameMapper.toFormattedString(mainThreadFrames)); + event.putArray("frames", StackFrameMapper.toWritableFrames(mainThreadFrames)); + event.putArray("threads", threads); + + // getJSModule(RCTDeviceEventEmitter) drops events silently in bridgeless mode + context.emitDeviceEvent(ANR_DETECTED_EVENT, event); + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/BacktraceReactNative.java b/packages/react-native/android/src/main/java/backtrace/library/BacktraceReactNative.java index 4c3577e4..5232c50d 100644 --- a/packages/react-native/android/src/main/java/backtrace/library/BacktraceReactNative.java +++ b/packages/react-native/android/src/main/java/backtrace/library/BacktraceReactNative.java @@ -2,6 +2,7 @@ import androidx.annotation.NonNull; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -110,4 +111,14 @@ public void useAttachments(ReadableArray attachmentPaths) { public void crash() { BacktraceBase.crash(); } + + @ReactMethod() + public void getAnrExitInfo(double sinceEpochMillis, Promise promise) { + try { + promise.resolve(new AnrExitInfoReader(this.context).read((long) sinceEpochMillis)); + } catch (Exception e) { + Log.w(this.NAME, "Could not read ANR exit info", e); + promise.reject("backtrace_anr_exit_info", e); + } + } } diff --git a/packages/react-native/android/src/main/java/backtrace/library/ReactNativePackage.java b/packages/react-native/android/src/main/java/backtrace/library/ReactNativePackage.java index 19e4bb54..aa15d892 100644 --- a/packages/react-native/android/src/main/java/backtrace/library/ReactNativePackage.java +++ b/packages/react-native/android/src/main/java/backtrace/library/ReactNativePackage.java @@ -23,6 +23,7 @@ public List createNativeModules(@NonNull ReactApplicationContext r modules.add(new backtraceio.library.MemoryInformationAttributeProvider(reactContext)); modules.add(new backtraceio.library.ProcessAttributeProvider(reactContext)); modules.add(new backtraceio.library.BacktraceAndroidBackgroundUnhandledExceptionHandler(reactContext)); + modules.add(new backtraceio.library.BacktraceAnrWatchdog(reactContext)); modules.add(new backtrace.library.BacktraceFileSystemProvider(reactContext)); modules.add(new backtrace.library.BacktraceDirectoryProvider(reactContext)); diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/ActivityManagerExitInfoProvider.java b/packages/react-native/android/src/main/java/backtrace/library/anr/ActivityManagerExitInfoProvider.java new file mode 100644 index 00000000..47b36d32 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/ActivityManagerExitInfoProvider.java @@ -0,0 +1,48 @@ +package backtraceio.library.anr; + +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.content.Context; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ActivityManagerExitInfoProvider implements ProcessExitInfoProvider { + private final ActivityManager activityManager; + + public ActivityManagerExitInfoProvider(Context context) { + this.activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public List getHistoricalExitInfo(String packageName, int startIndex, int maxCount) { + if (activityManager == null) { + return Collections.emptyList(); + } + + List systemExitInfoList = + activityManager.getHistoricalProcessExitReasons(packageName, startIndex, maxCount); + + if (systemExitInfoList.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(systemExitInfoList.size()); + for (ApplicationExitInfo info : systemExitInfoList) { + result.add(new ExitInfoAdapter(info)); + } + + return result; + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public List getSupportedTypesOfExitInfo() { + return Collections.singletonList(ApplicationExitInfo.REASON_ANR); + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/AnrWatchdog.java b/packages/react-native/android/src/main/java/backtrace/library/anr/AnrWatchdog.java new file mode 100644 index 00000000..b0275f4a --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/AnrWatchdog.java @@ -0,0 +1,72 @@ +package backtraceio.library.anr; + +import android.os.Debug; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.Map; + +public class AnrWatchdog extends Thread { + public interface OnAnrDetected { + void onAnrDetected(Map threads); + } + + public static final int DEFAULT_ANR_TIMEOUT = 5000; + + private static final String LOG_TAG = AnrWatchdog.class.getSimpleName(); + + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + private final int timeout; + private final boolean debug; + private final OnAnrDetected listener; + + private volatile boolean shouldStop = false; + + public AnrWatchdog(int timeout, boolean debug, OnAnrDetected listener) { + super("BacktraceAnrWatchdog"); + this.timeout = timeout; + this.debug = debug; + this.listener = listener; + this.start(); + } + + @Override + public void run() { + if (this.debug && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) { + Log.w(LOG_TAG, "Detected a debugger connection. ANR watchdog is disabled"); + return; + } + + boolean reported = false; + while (!this.shouldStop && !isInterrupted()) { + final BacktraceThreadWatcher threadWatcher = new BacktraceThreadWatcher(); + this.mainThreadHandler.post(threadWatcher::tickCounter); + + try { + Thread.sleep(this.timeout); + } catch (InterruptedException e) { + return; + } + threadWatcher.tickPrivateCounter(); + + if (threadWatcher.getCounter() == threadWatcher.getPrivateCounter()) { + reported = false; + continue; + } + + // one report per hang, not one per polling interval + if (reported) { + continue; + } + reported = true; + + this.listener.onAnrDetected(Thread.getAllStackTraces()); + } + } + + public void stopMonitoring() { + this.shouldStop = true; + this.interrupt(); + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/AppExitInfoDetailsExtractor.java b/packages/react-native/android/src/main/java/backtrace/library/anr/AppExitInfoDetailsExtractor.java new file mode 100644 index 00000000..9789ec92 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/AppExitInfoDetailsExtractor.java @@ -0,0 +1,88 @@ +package backtraceio.library.anr; + +import android.app.ApplicationExitInfo; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; + +public class AppExitInfoDetailsExtractor { + private static final String LOG_TAG = AppExitInfoDetailsExtractor.class.getSimpleName(); + + @RequiresApi(api = Build.VERSION_CODES.R) + public static HashMap getANRAttributes(ExitInfo appExitInfo) { + if (appExitInfo == null) { + return new HashMap<>(); + } + + final HashMap attributes = new HashMap<>(); + attributes.put("description", appExitInfo.getDescription()); + attributes.put("timestamp", getANRTimestamp(appExitInfo)); + attributes.put("reason-code", appExitInfo.getReason()); + attributes.put("reason", reasonCodeToDescription(appExitInfo.getReason())); + attributes.put("PID", appExitInfo.getPid()); + attributes.put("Importance", appExitInfo.getImportance()); + attributes.put("PSS", appExitInfo.getPss()); + attributes.put("RSS", appExitInfo.getRss()); + return attributes; + } + + @RequiresApi(api = Build.VERSION_CODES.R) + public static String getANRMessage(ExitInfo appExitInfo) { + return "Application Not Responding" + " | " + "Description: " + + appExitInfo.getDescription() + " | " + "Timestamp: " + + getANRTimestamp(appExitInfo); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + private static String getANRTimestamp(ExitInfo appExitInfo) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + return dateFormat.format(new Date(appExitInfo.getTimestamp())); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + public static String getStackTraceInfo(ExitInfo exitInfo) { + InputStream traceStream = getStreamOrNull(exitInfo); + if (traceStream == null) { + Log.w(LOG_TAG, "Unexpected null trace stream"); + return null; + } + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(traceStream))) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + } catch (Exception exception) { + Log.e(LOG_TAG, "Unexpected exception on getting stacktrace from exitInfo", exception); + return ""; + } + + return builder.toString(); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + private static InputStream getStreamOrNull(ExitInfo exitInfo) { + try { + return exitInfo.getTraceInputStream(); + } catch (IOException e) { + return null; + } + } + + private static String reasonCodeToDescription(int reasonCode) { + if (reasonCode == ApplicationExitInfo.REASON_ANR) { + return "anr"; + } + return "unsupported code"; + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/BacktraceThreadWatcher.java b/packages/react-native/android/src/main/java/backtrace/library/anr/BacktraceThreadWatcher.java new file mode 100644 index 00000000..071588e5 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/BacktraceThreadWatcher.java @@ -0,0 +1,22 @@ +package backtraceio.library.anr; + +public class BacktraceThreadWatcher { + private int counter; + private int privateCounter; + + public void tickPrivateCounter() { + privateCounter++; + } + + public int getPrivateCounter() { + return privateCounter; + } + + public synchronized int getCounter() { + return counter; + } + + public synchronized void tickCounter() { + counter++; + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfo.java b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfo.java new file mode 100644 index 00000000..ebd10ec8 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfo.java @@ -0,0 +1,24 @@ +package backtraceio.library.anr; + +import java.io.IOException; +import java.io.InputStream; + +public interface ExitInfo { + int getReason(); + + long getTimestamp(); + + String getDescription(); + + int getPid(); + + int getUid(); + + int getImportance(); + + long getPss(); + + long getRss(); + + InputStream getTraceInputStream() throws IOException; +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoAdapter.java b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoAdapter.java new file mode 100644 index 00000000..61c78a9a --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoAdapter.java @@ -0,0 +1,71 @@ +package backtraceio.library.anr; + +import android.app.ApplicationExitInfo; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.io.IOException; +import java.io.InputStream; + +public class ExitInfoAdapter implements ExitInfo { + private final ApplicationExitInfo systemExitInfo; + + public ExitInfoAdapter(ApplicationExitInfo systemExitInfo) { + this.systemExitInfo = systemExitInfo; + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public int getReason() { + return systemExitInfo.getReason(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public long getTimestamp() { + return systemExitInfo.getTimestamp(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public String getDescription() { + return systemExitInfo.getDescription(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public int getPid() { + return systemExitInfo.getPid(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public int getUid() { + return systemExitInfo.getDefiningUid(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public int getImportance() { + return systemExitInfo.getImportance(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public long getPss() { + return systemExitInfo.getPss(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public long getRss() { + return systemExitInfo.getRss(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public InputStream getTraceInputStream() throws IOException { + return systemExitInfo.getTraceInputStream(); + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoStackTraceParser.java b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoStackTraceParser.java new file mode 100644 index 00000000..4cc3a66c --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/ExitInfoStackTraceParser.java @@ -0,0 +1,297 @@ +package backtraceio.library.anr; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ExitInfoStackTraceParser { + private static final Pattern JAVA_FRAME_PATTERN = Pattern.compile("\\s*at\\s+(.*?)\\((.*?):(\\d+)\\)"); + private static final Pattern NATIVE_METHOD_FRAME_PATTERN = Pattern.compile("\\s*at\\s+(.*?)\\(Native [Mm]ethod\\)"); + private static final Pattern FILE_ONLY_FRAME_PATTERN = Pattern.compile("\\s*at\\s+(.*?)\\(([^)]+)\\)"); + private static final String UNKNOWN_SOURCE_FILE = "Unknown Source"; + private static final int NATIVE_METHOD_LINE_NUMBER = -2; + private static final int UNKNOWN_LINE_NUMBER = -1; + private static final String MAIN_THREAD_NAME = "main"; + private static final int NATIVE_STACK_ELEMENTS_NUMBER = 6; + + static StackTraceElement parseFrame(String frame) { + StackTraceElement javaFrame = parseJavaFrame(frame); + if (javaFrame != null) { + return javaFrame; + } + + StackTraceElement nativeMethodFrame = parseNativeMethodFrame(frame); + if (nativeMethodFrame != null) { + return nativeMethodFrame; + } + + StackTraceElement nativeFrame = parseNativeFrame(frame); + if (nativeFrame != null) { + return nativeFrame; + } + + return parseFileOnlyFrame(frame); + } + + static StackTraceElement parseFileOnlyFrame(String frame) { + Matcher matcher = FILE_ONLY_FRAME_PATTERN.matcher(frame); + if (!matcher.find()) { + return null; + } + + String fileName = matcher.group(2); + if (UNKNOWN_SOURCE_FILE.equals(fileName)) { + fileName = null; + } + return toStackTraceElement(matcher.group(1), fileName, UNKNOWN_LINE_NUMBER); + } + + static StackTraceElement parseNativeMethodFrame(String frame) { + Matcher matcher = NATIVE_METHOD_FRAME_PATTERN.matcher(frame); + if (!matcher.find()) { + return null; + } + + // isNativeMethod, and so the reported library, keys off this line number + return toStackTraceElement(matcher.group(1), null, NATIVE_METHOD_LINE_NUMBER); + } + + static StackTraceElement parseNativeFrame(String frame) { + if (!frame.startsWith("native")) { + return null; + } + String[] parts = frame.split("\\s+", NATIVE_STACK_ELEMENTS_NUMBER); + + if (parts.length < NATIVE_STACK_ELEMENTS_NUMBER) { + return null; + } + + String address = parts[3]; + String library = parts[4]; + String funcName = parts[5]; + + return new StackTraceElement(library, funcName, "address: " + address, 0); + } + + static StackTraceElement parseJavaFrame(String frame) { + Matcher matcher = JAVA_FRAME_PATTERN.matcher(frame); + if (!matcher.find()) { + return null; + } + + return toStackTraceElement(matcher.group(1), matcher.group(2), Integer.parseInt(matcher.group(3))); + } + + private static StackTraceElement toStackTraceElement(String fullClassNameMethod, String fileName, int lineNumber) { + int lastDot = fullClassNameMethod.lastIndexOf('.'); + String className = (lastDot == -1) ? fullClassNameMethod : fullClassNameMethod.substring(0, lastDot); + String methodName = (lastDot == -1) ? "" : fullClassNameMethod.substring(lastDot + 1); + + return new StackTraceElement(className, methodName, fileName, lineNumber); + } + + public static StackTraceElement[] parseMainThreadStackTrace(Map parsedData) { + Map mainThreadInfo = (Map) parsedData.get("main_thread"); + + if (mainThreadInfo == null) { + return new StackTraceElement[0]; + } + + return toStackTraceElements((List) mainThreadInfo.get("stack_trace")); + } + + // the dump repeats names across pool threads, so these stay a list and are deduplicated by the caller + public static List parseOtherThreadStackTraces(Map parsedData) { + List result = new ArrayList<>(); + List> threads = (List>) parsedData.get("threads"); + + if (threads == null) { + return result; + } + + for (Map thread : threads) { + Object name = thread.get("name"); + if (name == null || MAIN_THREAD_NAME.equals(name)) { + continue; + } + result.add(new ThreadStackTrace(name.toString(), + toStackTraceElements((List) thread.get("stack_trace")))); + } + return result; + } + + public static class ThreadStackTrace { + private final String name; + private final StackTraceElement[] frames; + + ThreadStackTrace(String name, StackTraceElement[] frames) { + this.name = name; + this.frames = frames; + } + + public String getName() { + return this.name; + } + + public StackTraceElement[] getFrames() { + return this.frames; + } + } + + private static StackTraceElement[] toStackTraceElements(List stackFrames) { + if (stackFrames == null) { + return new StackTraceElement[0]; + } + + List elements = new ArrayList<>(); + for (String frame : stackFrames) { + StackTraceElement element = parseFrame(frame); + if (element != null) { + elements.add(element); + } + } + return elements.toArray(new StackTraceElement[0]); + } + + public static Map parseANRStackTrace(String stackTrace) { + Map parsedData = new HashMap<>(); + + if (stackTrace == null || stackTrace.isEmpty()) { + return parsedData; + } + + parsedData.put("timestamp", parseTimestamp(stackTrace)); + + Pattern pidPattern = Pattern.compile("----- pid (\\d+) at"); + Matcher pidMatcher = pidPattern.matcher(stackTrace); + if (pidMatcher.find()) { + parsedData.put("pid", Integer.parseInt(pidMatcher.group(1))); + } + + Pattern cmdLinePattern = Pattern.compile("Cmd line: (.*)"); + Matcher cmdLineMatcher = cmdLinePattern.matcher(stackTrace); + if (cmdLineMatcher.find()) { + parsedData.put("command_line", cmdLineMatcher.group(1)); + } + + Pattern fingerprintPattern = Pattern.compile("Build fingerprint: '(.*?)'"); + Matcher fingerprintMatcher = fingerprintPattern.matcher(stackTrace); + if (fingerprintMatcher.find()) { + parsedData.put("build_fingerprint", fingerprintMatcher.group(1)); + } + + Pattern abiPattern = Pattern.compile("ABI: '(.*?)'"); + Matcher abiMatcher = abiPattern.matcher(stackTrace); + if (abiMatcher.find()) { + parsedData.put("abi", abiMatcher.group(1)); + } + + Pattern buildTypePattern = Pattern.compile("Build type: (.*)"); + Matcher buildTypeMatcher = buildTypePattern.matcher(stackTrace); + if (buildTypeMatcher.find()) { + parsedData.put("build_type", buildTypeMatcher.group(1)); + } + + Pattern heapPattern = Pattern.compile("Heap: (.*)"); + Matcher heapMatcher = heapPattern.matcher(stackTrace); + if (heapMatcher.find()) { + parsedData.put("heap_info", heapMatcher.group(1)); + } + + List> threads = parseThreadDumps(stackTrace); + parsedData.put("threads", threads); + + Map mainThreadInfo = getMainThreadInfo(threads); + parsedData.put("main_thread", mainThreadInfo); + + return parsedData; + } + + private static Object parseTimestamp(String stackTrace) { + Pattern timestampPattern = Pattern.compile("----- pid \\d+ at (.*?) -----"); + Matcher timestampMatcher = timestampPattern.matcher(stackTrace); + if (timestampMatcher.find()) { + return timestampMatcher.group(1); + } + return null; + } + + @Nullable + private static Map getMainThreadInfo(List> threads) { + Map mainThreadInfo = null; + for (Map thread : threads) { + if (MAIN_THREAD_NAME.equals(thread.get("name"))) { + mainThreadInfo = thread; + break; + } + } + return mainThreadInfo; + } + + private static Map parseThreadInformation(String threadDump) { + Map result = new HashMap<>(); + + Pattern headerPattern = Pattern.compile("\"([^\"]+)\"\\s*(daemon)?\\s*prio=(\\d+)\\s*tid=(\\d+)\\s*([^\\n]+)"); + Matcher headerMatcher = headerPattern.matcher(threadDump); + + if (headerMatcher.find()) { + result.put("name", headerMatcher.group(1)); + result.put("isDaemon", headerMatcher.group(2) != null); + result.put("prio", Integer.parseInt(headerMatcher.group(3))); + result.put("tid", Integer.parseInt(headerMatcher.group(4))); + result.put("status", headerMatcher.group(5).trim()); + } + + result.put("stack_trace", parseThreadStackTrace(threadDump)); + + return result; + } + + @NonNull + private static List parseThreadStackTrace(String threadDump) { + List stackTrace = new ArrayList<>(); + String[] lines = threadDump.split("\n"); + boolean isStackTrace = false; + + for (String line : lines) { + line = line.trim(); + + if (isStackTrace && (line.isEmpty() || line.startsWith("\""))) { + break; + } + + if (line.isEmpty() || line.startsWith("\"")) { + continue; + } + + if (line.startsWith("at ") || line.startsWith("native:")) { + isStackTrace = true; + stackTrace.add(line); + } + } + return stackTrace; + } + + public static List> parseThreadDumps(String input) { + List> threads = new ArrayList<>(); + + String regex = "\\\"(.*?)\\\" (daemon )?prio=(\\d+) tid=(\\d+) (\\w+)(.*)\\n(?s)((.*?\\n))(?=(?:\\n\\n)|$)"; + + Pattern threadStartPattern = Pattern.compile(regex, Pattern.MULTILINE); + Matcher threadMatcher = threadStartPattern.matcher(input); + + while (threadMatcher.find()) { + String threadDump = threadMatcher.group(); + Map threadInfo = parseThreadInformation(threadDump); + threads.add(threadInfo); + } + + return threads; + } +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/ProcessExitInfoProvider.java b/packages/react-native/android/src/main/java/backtrace/library/anr/ProcessExitInfoProvider.java new file mode 100644 index 00000000..fdc383ce --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/ProcessExitInfoProvider.java @@ -0,0 +1,9 @@ +package backtraceio.library.anr; + +import java.util.List; + +public interface ProcessExitInfoProvider { + List getHistoricalExitInfo(String packageName, int startIndex, int maxCount); + + List getSupportedTypesOfExitInfo(); +} diff --git a/packages/react-native/android/src/main/java/backtrace/library/anr/StackFrameMapper.java b/packages/react-native/android/src/main/java/backtrace/library/anr/StackFrameMapper.java new file mode 100644 index 00000000..601fe618 --- /dev/null +++ b/packages/react-native/android/src/main/java/backtrace/library/anr/StackFrameMapper.java @@ -0,0 +1,56 @@ +package backtraceio.library.anr; + +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; + +public class StackFrameMapper { + private static final String NATIVE_FILE_PREFIX = "address: "; + private static final String UNKNOWN_LIBRARY = "unknown"; + + public static WritableArray toWritableFrames(StackTraceElement[] frames) { + WritableArray result = new WritableNativeArray(); + for (StackTraceElement frame : frames) { + result.pushMap(toWritableFrame(frame)); + } + return result; + } + + public static String toFormattedString(StackTraceElement[] frames) { + StringBuilder formatted = new StringBuilder(); + for (StackTraceElement frame : frames) { + formatted.append(frame.toString()).append('\n'); + } + return formatted.toString(); + } + + private static WritableMap toWritableFrame(StackTraceElement frame) { + WritableMap map = new WritableNativeMap(); + + String fileName = frame.getFileName(); + if (fileName != null && fileName.startsWith(NATIVE_FILE_PREFIX)) { + // parseNativeFrame stores the symbol in the method name and the library in the class name + map.putString("funcName", frame.getMethodName()); + map.putString("library", frame.getClassName()); + map.putString("address", fileName.substring(NATIVE_FILE_PREFIX.length())); + return map; + } + + String className = frame.getClassName(); + map.putString("funcName", className.isEmpty() + ? frame.getMethodName() + : className + "." + frame.getMethodName()); + + if (frame.isNativeMethod()) { + map.putString("library", "Native"); + } else { + map.putString("library", fileName != null ? fileName : UNKNOWN_LIBRARY); + } + + if (frame.getLineNumber() > 0) { + map.putInt("line", frame.getLineNumber()); + } + return map; + } +} diff --git a/packages/react-native/android/src/test/java/backtraceio/library/anr/ExitInfoStackTraceParserTest.java b/packages/react-native/android/src/test/java/backtraceio/library/anr/ExitInfoStackTraceParserTest.java new file mode 100644 index 00000000..2d0306ed --- /dev/null +++ b/packages/react-native/android/src/test/java/backtraceio/library/anr/ExitInfoStackTraceParserTest.java @@ -0,0 +1,292 @@ +package backtraceio.library.anr; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +// The report stack comes entirely from parsing the text dump the system writes for an ANR, so +// a parsing mistake ships reports with a wrong or empty stack. The cases run against a real +// dump captured from a device and cover both the Java and the native frame formats. +public class ExitInfoStackTraceParserTest { + private static final String ANR_APPEXIT_STACKTRACE_FILE = "anrAppExitInfoStacktrace.txt"; + + @Test + public void parseFrameJava() { + // GIVEN + String frame = "at backtraceio.backtraceio.MainActivity.handledException(MainActivity.java:157)"; + // WHEN + StackTraceElement stackTraceElement = ExitInfoStackTraceParser.parseFrame(frame); + // THEN + assertEquals("backtraceio.backtraceio.MainActivity", stackTraceElement.getClassName()); + assertEquals("MainActivity.java", stackTraceElement.getFileName()); + assertEquals(157, stackTraceElement.getLineNumber()); + assertEquals("handledException", stackTraceElement.getMethodName()); + } + + @Test + public void parseFrameNative() { + // GIVEN + String frame = + "native: #19 pc 00630008 /apex/com.android.art/lib/libart.so (art::InvokeMethod(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned int)+1464)"; + // WHEN + StackTraceElement stackTraceElement = ExitInfoStackTraceParser.parseFrame(frame); + // THEN + assertEquals("/apex/com.android.art/lib/libart.so", stackTraceElement.getClassName()); + assertEquals("address: 00630008", stackTraceElement.getFileName()); + assertEquals(0, stackTraceElement.getLineNumber()); + assertEquals( + "(art::InvokeMethod(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned int)+1464)", + stackTraceElement.getMethodName()); + } + + @Test + public void parseAnrStackTrace() { + // GIVEN + String anrStacktraceString = readResource(ANR_APPEXIT_STACKTRACE_FILE); + + // WHEN + Map anrStacktrace = ExitInfoStackTraceParser.parseANRStackTrace(anrStacktraceString); + + // THEN + assertNotNull(anrStacktrace); + assertNotNull(anrStacktrace.get("main_thread")); + assertEquals("x86", anrStacktrace.get("abi")); + assertEquals("74% free, 6892KB/25MB; 138095 objects", anrStacktrace.get("heap_info")); + assertEquals( + "google/sdk_gphone_x86/generic_x86_arm:11/RSR1.201013.001/6903271:user/release-keys", + anrStacktrace.get("build_fingerprint")); + assertEquals("optimized", anrStacktrace.get("build_type")); + assertEquals("backtraceio.backtraceio", anrStacktrace.get("command_line")); + assertEquals("2025-03-27 21:02:38", anrStacktrace.get("timestamp")); + assertEquals(9207, anrStacktrace.get("pid")); + + // THEN THREADS + List> threads = (List>) anrStacktrace.get("threads"); + assertEquals(20, threads.size()); + + Map customThread4 = threads.get(18); + List thread4StackTrace = (List) customThread4.get("stack_trace"); + + assertEquals("Thread-4", threads.get(18).get("name")); + assertEquals("at java.lang.Thread.sleep(Native method)", thread4StackTrace.get(0)); + assertEquals("at java.lang.Thread.sleep(Thread.java:442)", thread4StackTrace.get(1)); + assertEquals("at java.lang.Thread.sleep(Thread.java:358)", thread4StackTrace.get(2)); + assertEquals( + "at backtraceio.library.watchdog.BacktraceANRHandlerWatchdog.run(BacktraceANRHandlerWatchdog.java:118)", + thread4StackTrace.get(3)); + + // THEN MAIN THREAD + Map mainThread = (Map) anrStacktrace.get("main_thread"); + assertEquals(5, mainThread.get("prio")); + assertEquals(1, mainThread.get("tid")); + assertNull(mainThread.get("daemon")); + + ArrayList stackTrace = (ArrayList) mainThread.get("stack_trace"); + assertEquals(36, stackTrace.size()); + + assertEquals( + "native: #20 pc 005886a0 /apex/com.android.art/lib/libart.so (art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)+80)", + stackTrace.get(20)); + assertEquals( + "at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:468)", + stackTrace.get(24)); + } + + @Test + public void parseAnrMainThreadStackTrace() { + // GIVEN + String anrStacktraceString = readResource(ANR_APPEXIT_STACKTRACE_FILE); + Map anrStacktrace = ExitInfoStackTraceParser.parseANRStackTrace(anrStacktraceString); + + // WHEN + StackTraceElement[] anrMainThreadStacktrace = ExitInfoStackTraceParser.parseMainThreadStackTrace(anrStacktrace); + + // THEN + assertEquals(36, anrMainThreadStacktrace.length); + + assertEquals("(__kernel_vsyscall+7)", anrMainThreadStacktrace[0].getMethodName()); + assertEquals(0, anrMainThreadStacktrace[0].getLineNumber()); + assertEquals("address: 00000b97", anrMainThreadStacktrace[0].getFileName()); + assertEquals("[vdso]", anrMainThreadStacktrace[0].getClassName()); + + assertEquals( + "(art::interpreter::EnterInterpreterFromEntryPoint(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*)+176)", + anrMainThreadStacktrace[14].getMethodName()); + assertEquals(0, anrMainThreadStacktrace[14].getLineNumber()); + assertEquals("address: 00379b00", anrMainThreadStacktrace[14].getFileName()); + assertEquals("/apex/com.android.art/lib/libart.so", anrMainThreadStacktrace[14].getClassName()); + + assertEquals("yield", anrMainThreadStacktrace[21].getMethodName()); + assertEquals("java.lang.Thread", anrMainThreadStacktrace[21].getClassName()); + assertTrue(anrMainThreadStacktrace[21].isNativeMethod()); + + assertEquals("handledException", anrMainThreadStacktrace[22].getMethodName()); + assertEquals(157, anrMainThreadStacktrace[22].getLineNumber()); + assertEquals("MainActivity.java", anrMainThreadStacktrace[22].getFileName()); + assertEquals("backtraceio.backtraceio.MainActivity", anrMainThreadStacktrace[22].getClassName()); + } + + @Test + public void parseFrameNativeMethod() { + // GIVEN + String frame = " at java.lang.Thread.sleep(Native method)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("java.lang.Thread", element.getClassName()); + assertEquals("sleep", element.getMethodName()); + assertNull(element.getFileName()); + assertTrue(element.isNativeMethod()); + } + + @Test + public void parseFrameNativeMethodCapitalized() { + // GIVEN + String frame = " at java.lang.Thread.yield(Native Method)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("yield", element.getMethodName()); + assertTrue(element.isNativeMethod()); + } + + @Test + public void parseFrameUnknownSource() { + // GIVEN + String frame = " at com.example.Foo.bar(Unknown Source)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("com.example.Foo", element.getClassName()); + assertEquals("bar", element.getMethodName()); + assertNull(element.getFileName()); + assertFalse(element.isNativeMethod()); + } + + @Test + public void parseFrameFileWithoutLine() { + // GIVEN + String frame = " at com.example.Foo.bar(SourceFile)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("SourceFile", element.getFileName()); + assertEquals(-1, element.getLineNumber()); + } + + @Test + public void parseFrameFileWithColonAndNoLine() { + // GIVEN + String frame = " at com.example.Foo.bar(weird:file)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("weird:file", element.getFileName()); + assertEquals(-1, element.getLineNumber()); + } + + @Test + public void parseFrameWithTabAfterAt() { + // GIVEN + String frame = "\tat\tjava.lang.Thread.sleep(Thread.java:580)"; + + // WHEN + StackTraceElement element = ExitInfoStackTraceParser.parseFrame(frame); + + // THEN + assertNotNull(element); + assertEquals("sleep", element.getMethodName()); + assertEquals(580, element.getLineNumber()); + } + + @Test + public void parseAnrOtherThreadStackTraces() { + // GIVEN + String stacktrace = readResource(ANR_APPEXIT_STACKTRACE_FILE); + Map parsed = ExitInfoStackTraceParser.parseANRStackTrace(stacktrace); + + // WHEN + List threads = + ExitInfoStackTraceParser.parseOtherThreadStackTraces(parsed); + + // THEN + assertNotNull(threads); + assertFalse(threads.isEmpty()); + + List names = new ArrayList<>(); + for (ExitInfoStackTraceParser.ThreadStackTrace thread : threads) { + names.add(thread.getName()); + } + assertFalse(names.contains("main")); + assertTrue(names.contains("Signal Catcher")); + } + + @Test + public void parseOtherThreadStackTracesKeepsRepeatedNames() { + // GIVEN + String dump = "\"OkHttp Dispatcher\" daemon prio=5 tid=12 Waiting\n" + + " at java.lang.Object.wait(Object.java:405)\n" + + "\n" + + "\"OkHttp Dispatcher\" daemon prio=5 tid=13 Waiting\n" + + " at java.lang.Thread.run(Thread.java:1572)\n" + + "\n"; + Map parsed = ExitInfoStackTraceParser.parseANRStackTrace(dump); + + // WHEN + List threads = + ExitInfoStackTraceParser.parseOtherThreadStackTraces(parsed); + + // THEN + assertEquals(2, threads.size()); + assertEquals("OkHttp Dispatcher", threads.get(0).getName()); + assertEquals("OkHttp Dispatcher", threads.get(1).getName()); + assertEquals("wait", threads.get(0).getFrames()[0].getMethodName()); + assertEquals("run", threads.get(1).getFrames()[0].getMethodName()); + } + + private static String readResource(String fileName) { + ClassLoader classLoader = ExitInfoStackTraceParserTest.class.getClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream(fileName); + assertNotNull(inputStream); + + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + if (reader.ready()) { + builder.append('\n'); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return builder.toString(); + } +} diff --git a/packages/react-native/android/src/test/resources/anrAppExitInfoStacktrace.txt b/packages/react-native/android/src/test/resources/anrAppExitInfoStacktrace.txt new file mode 100644 index 00000000..43b52ead --- /dev/null +++ b/packages/react-native/android/src/test/resources/anrAppExitInfoStacktrace.txt @@ -0,0 +1,521 @@ +----- pid 9207 at 2025-03-27 21:02:38 ----- +Cmd line: backtraceio.backtraceio +Build fingerprint: 'google/sdk_gphone_x86/generic_x86_arm:11/RSR1.201013.001/6903271:user/release-keys' +ABI: 'x86' +Build type: optimized +Zygote loaded classes=15747 post zygote classes=1481 +Dumping registered class loaders +#0 dalvik.system.PathClassLoader: [], parent #1 +#1 java.lang.BootClassLoader: [], no parent +#2 dalvik.system.PathClassLoader: [/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes15.dex:/data/data/backtraceio.backtraceio/code_cache/.overlay/base.apk/classes16.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes10.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes8.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes2.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes4.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes3.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes17.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes11.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes14.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes9.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes7.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes12.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes6.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes13.dex:/data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!classes5.dex], parent #1 +Done dumping class loaders +Classes initialized: 760 in 140.777ms +Intern table: 32164 strong; 519 weak +JNI: CheckJNI is on; globals=640 (plus 60 weak) +Libraries: /data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!/lib/x86/libbacktrace-native.so /data/app/~~lbMB2_9XcyZkiC41RLVuMg==/backtraceio.backtraceio-1XRWT_mS3AbJ9vbjbaCtkw==/base.apk!/lib/x86/libnative-lib.so libandroid.so libaudioeffect_jni.so libcompiler_rt.so libicu_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libopenjdk.so librs_jni.so libsfplugin_ccodec.so libsoundpool.so libstats_jni.so libwebviewchromium_loader.so (16) +Heap: 74% free, 6892KB/25MB; 138095 objects +Dumping cumulative Gc timings +Average major GC reclaim bytes ratio inf over 0 GC cycles +Average major GC copied live bytes ratio 0.72272 over 4 major GCs +Cumulative bytes moved 11312128 +Cumulative objects moved 212604 +Peak regions allocated 27 (6912KB) / 768 (192MB) +Start Dumping histograms for 1 iterations for young concurrent copying +GrayAllDirtyImmuneObjects: Sum: 12.004ms 99% C.I. 12.004ms-12.004ms Avg: 12.004ms Max: 12.004ms +InitializePhase: Sum: 11.934ms 99% C.I. 11.934ms-11.934ms Avg: 11.934ms Max: 11.934ms +ProcessMarkStack: Sum: 9.977ms 99% C.I. 9.977ms-9.977ms Avg: 9.977ms Max: 9.977ms +ScanImmuneSpaces: Sum: 4.691ms 99% C.I. 4.691ms-4.691ms Avg: 4.691ms Max: 4.691ms +EnqueueFinalizerReferences: Sum: 4.233ms 99% C.I. 4.233ms-4.233ms Avg: 4.233ms Max: 4.233ms +ScanCardsForSpace: Sum: 3.451ms 99% C.I. 3.451ms-3.451ms Avg: 3.451ms Max: 3.451ms +VisitConcurrentRoots: Sum: 3.005ms 99% C.I. 3.005ms-3.005ms Avg: 3.005ms Max: 3.005ms +ResetStack: Sum: 1.680ms 99% C.I. 1.680ms-1.680ms Avg: 1.680ms Max: 1.680ms +SweepSystemWeaks: Sum: 1.050ms 99% C.I. 1.050ms-1.050ms Avg: 1.050ms Max: 1.050ms +ClearFromSpace: Sum: 423us 99% C.I. 423us-423us Avg: 423us Max: 423us +FlipOtherThreads: Sum: 333us 99% C.I. 333us-333us Avg: 333us Max: 333us +ForwardSoftReferences: Sum: 289us 99% C.I. 289us-289us Avg: 289us Max: 289us +SweepArray: Sum: 202us 99% C.I. 202us-202us Avg: 202us Max: 202us +EmptyRBMarkBitStack: Sum: 121us 99% C.I. 121us-121us Avg: 121us Max: 121us +VisitNonThreadRoots: Sum: 42us 99% C.I. 42us-42us Avg: 42us Max: 42us +FlipThreadRoots: Sum: 38us 99% C.I. 38us-38us Avg: 38us Max: 38us +ProcessReferences: Sum: 26us 99% C.I. 1us-25us Avg: 13us Max: 25us +(Paused)GrayAllNewlyDirtyImmuneObjects: Sum: 25us 99% C.I. 25us-25us Avg: 25us Max: 25us +(Paused)ClearCards: Sum: 19us 99% C.I. 0.250us-13us Avg: 1.461us Max: 13us +CopyingPhase: Sum: 17us 99% C.I. 17us-17us Avg: 17us Max: 17us +MarkZygoteLargeObjects: Sum: 16us 99% C.I. 16us-16us Avg: 16us Max: 16us +ThreadListFlip: Sum: 14us 99% C.I. 14us-14us Avg: 14us Max: 14us +SwapBitmaps: Sum: 9us 99% C.I. 9us-9us Avg: 9us Max: 9us +FreeList: Sum: 6us 99% C.I. 6us-6us Avg: 6us Max: 6us +ResumeRunnableThreads: Sum: 4us 99% C.I. 4us-4us Avg: 4us Max: 4us +(Paused)FlipCallback: Sum: 2us 99% C.I. 2us-2us Avg: 2us Max: 2us +(Paused)SetFromSpace: Sum: 1us 99% C.I. 1us-1us Avg: 1us Max: 1us +Done Dumping histograms +young concurrent copying paused: Sum: 63us 99% C.I. 63us-63us Avg: 63us Max: 63us +young concurrent copying freed-bytes: Avg: 1905KB Max: 1905KB Min: 1905KB +Freed-bytes histogram: 1600:1 +young concurrent copying total time: 53.653ms mean time: 53.653ms +young concurrent copying freed: 25923 objects with total size 1905KB +young concurrent copying throughput: 489113/s / 35MB/s per cpu-time: 47582634/s / 45MB/s +Average minor GC reclaim bytes ratio 0.92539 over 1 GC cycles +Average minor GC copied live bytes ratio 0.133529 over 2 minor GCs +Cumulative bytes moved 870264 +Cumulative objects moved 16602 +Peak regions allocated 27 (6912KB) / 768 (192MB) +Total time spent in GC: 53.653ms +Mean GC size throughput: 34MB/s per cpu-time: 44MB/s +Mean GC object throughput: 483160 objects/s +Total number of allocations 164018 +Total bytes allocated 8797KB +Total bytes freed 1905KB +Free memory 19MB +Free memory until GC 19MB +Free memory until OOME 185MB +Total memory 25MB +Max memory 192MB +Zygote space size 2980KB +Total mutator paused time: 63us +Total time waiting for GC to complete: 4.459us +Total GC count: 1 +Total GC time: 53.653ms +Total blocking GC count: 0 +Total blocking GC time: 0 +Native bytes total: 15962696 registered: 108600 +Total native bytes at last GC: 4019588 +/system/framework/oat/x86/android.hidl.manager-V1.0-java.odex: quicken +/system/framework/oat/x86/android.test.base.odex: quicken +/system/framework/oat/x86/android.hidl.base-V1.0-java.odex: quicken +Current JIT code cache size (used / resident): 52KB / 60KB +Current JIT data cache size (used / resident): 61KB / 96KB +Zygote JIT code cache size (at point of fork): 42KB / 44KB +Zygote JIT data cache size (at point of fork): 31KB / 36KB +Current JIT mini-debug-info size: 50KB +Current JIT capacity: 128KB +Current number of JIT JNI stub entries: 2 +Current number of JIT code cache entries: 207 +Total number of JIT compilations: 172 +Total number of JIT compilations for on stack replacement: 7 +Total number of JIT code cache collections: 1 +Memory used for stack maps: Avg: 85B Max: 660B Min: 12B +Memory used for compiled code: Avg: 324B Max: 2604B Min: 17B +Memory used for profiling info: Avg: 74B Max: 764B Min: 20B +Start Dumping histograms for 212 iterations for JIT timings +Compiling: Sum: 540.855ms 99% C.I. 0.030ms-21.292ms Avg: 2.563ms Max: 24.444ms +TrimMaps: Sum: 77.018ms 99% C.I. 1.850us-4245us Avg: 365.014us Max: 9813us +Code cache collection: Sum: 3.847ms 99% C.I. 3.847ms-3.847ms Avg: 3.847ms Max: 3.847ms +Done Dumping histograms +Memory used for compilation: Avg: 36KB Max: 322KB Min: 0B +ProfileSaver total_bytes_written=4974 +ProfileSaver total_number_of_writes=1 +ProfileSaver total_number_of_code_cache_queries=1 +ProfileSaver total_number_of_skipped_writes=0 +ProfileSaver total_number_of_failed_writes=0 +ProfileSaver total_ms_of_sleep=45000 +ProfileSaver total_ms_of_work=10 +ProfileSaver total_number_of_hot_spikes=4 +ProfileSaver total_number_of_wake_ups=3 + +suspend all histogram: Sum: 301us 99% C.I. 0.090us-104us Avg: 11.148us Max: 104us +DALVIK THREADS (20): +"Signal Catcher" daemon prio=10 tid=4 Runnable + | group="system" sCount=0 dsCount=0 flags=0 obj=0x13040b28 self=0xe738a810 + | sysTid=9216 nice=-20 cgrp=top-app sched=0/0 handle=0xdc5da1e0 + | state=R schedstat=( 17573906 201748 7 ) utm=0 stm=1 core=0 HZ=100 + | stack=0xdc4df000-0xdc4e1000 stackSize=1008KB + | held mutexes= "mutator lock"(shared held) + native: #00 pc 00542d9e /apex/com.android.art/lib/libart.so (art::DumpNativeStack(std::__1::basic_ostream >&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+110) + native: #01 pc 006a0897 /apex/com.android.art/lib/libart.so (art::Thread::DumpStack(std::__1::basic_ostream >&, bool, BacktraceMap*, bool) const+1015) + native: #02 pc 0069a171 /apex/com.android.art/lib/libart.so (art::Thread::Dump(std::__1::basic_ostream >&, bool, BacktraceMap*, bool) const+65) + native: #03 pc 006c61b4 /apex/com.android.art/lib/libart.so (art::DumpCheckpoint::Run(art::Thread*)+1172) + native: #04 pc 006bf266 /apex/com.android.art/lib/libart.so (art::ThreadList::RunCheckpoint(art::Closure*, art::Closure*)+630) + native: #05 pc 006be1ce /apex/com.android.art/lib/libart.so (art::ThreadList::Dump(std::__1::basic_ostream >&, bool)+2446) + native: #06 pc 006bd70c /apex/com.android.art/lib/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream >&)+1644) + native: #07 pc 0064d654 /apex/com.android.art/lib/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream >&)+212) + native: #08 pc 00665b6a /apex/com.android.art/lib/libart.so (art::SignalCatcher::HandleSigQuit()+1818) + native: #09 pc 0066496b /apex/com.android.art/lib/libart.so (art::SignalCatcher::Run(void*)+587) + native: #10 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #11 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + + +//expected api request to coroner api +// java stacktrace +(String declaringClass, String methodName, String fileName, int lineNumber) + + +// native stack +{ +"funcName": "syscall", +"library": "/apex/com.android.runtime/lib/bionic/libc.so", +"address": "0005ad68" +} + + + +"main" prio=5 tid=1 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x72287300 self=0xe7380e10 + | sysTid=9207 nice=-10 cgrp=top-app sched=0/0 handle=0xf583f478 + | state=S schedstat=( 44727379364 813394028 1292 ) utm=1691 stm=2781 core=1 HZ=100 + | stack=0xff020000-0xff022000 stackSize=8192KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 0005ad68 /apex/com.android.runtime/lib/bionic/libc.so (syscall+40) + native: #02 pc 001d82ec /apex/com.android.art/lib/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+108) + native: #03 pc 001d8273 /apex/com.android.art/lib/libart.so (art::ConditionVariable::Wait(art::Thread*)+35) + native: #04 pc 007894db /apex/com.android.art/lib/libart.so (art::GoToRunnable(art::Thread*)+507) + native: #05 pc 007892a1 /apex/com.android.art/lib/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+33) + native: #06 pc 02008de3 /memfd:jit-cache (deleted) (offset 2000000) (art_jni_trampoline+163) + native: #07 pc 02008cee /memfd:jit-cache (deleted) (offset 2000000) (backtraceio.backtraceio.MainActivity.handledException+46) + native: #08 pc 00142c04 /apex/com.android.art/lib/libart.so (art_quick_osr_stub+36) + native: #09 pc 003b138f /apex/com.android.art/lib/libart.so (art::jit::Jit::MaybeDoOnStackReplacement(art::Thread*, art::ArtMethod*, unsigned int, int, art::JValue*)+415) + native: #10 pc 007b2bf9 /apex/com.android.art/lib/libart.so (MterpMaybeDoOnStackReplacement+185) + native: #11 pc 0013a2f5 /apex/com.android.art/lib/libart.so (MterpHelpers+294) + native: #12 pc 00001034 /data/data/backtraceio.backtraceio/code_cache/.overlay/base.apk/classes16.dex (backtraceio.backtraceio.MainActivity.handledException+8) + native: #13 pc 0036fb02 /apex/com.android.art/lib/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.llvm.16375758241455872412)+370) + native: #14 pc 00379b00 /apex/com.android.art/lib/libart.so (art::interpreter::EnterInterpreterFromEntryPoint(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*)+176) + native: #15 pc 0078b325 /apex/com.android.art/lib/libart.so (artQuickToInterpreterBridge+1061) + native: #16 pc 0014220d /apex/com.android.art/lib/libart.so (art_quick_to_interpreter_bridge+77) + native: #17 pc 0013b922 /apex/com.android.art/lib/libart.so (art_quick_invoke_stub+338) + native: #18 pc 001d0381 /apex/com.android.art/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+241) + native: #19 pc 00630008 /apex/com.android.art/lib/libart.so (art::InvokeMethod(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned int)+1464) + native: #20 pc 005886a0 /apex/com.android.art/lib/libart.so (art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)+80) + at java.lang.Thread.yield(Native method) + at backtraceio.backtraceio.MainActivity.handledException(MainActivity.java:157) + at java.lang.reflect.Method.invoke(Native method) + at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:468) + at android.view.View.performClick(View.java:7448) + at android.view.View.performClickInternal(View.java:7425) + at android.view.View.access$3600(View.java:810) + at android.view.View$PerformClick.run(View.java:28305) + at android.os.Handler.handleCallback(Handler.java:938) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loop(Looper.java:223) + at android.app.ActivityThread.main(ActivityThread.java:7656) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) + +"ADB-JDWP Connection Control Thread" daemon prio=0 tid=5 WaitingInMainDebuggerLoop + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040ba0 self=0xe738c410 + | sysTid=9218 nice=-20 cgrp=top-app sched=0/0 handle=0xdc3d81e0 + | state=S schedstat=( 4694215 13986760 9 ) utm=0 stm=0 core=1 HZ=100 + | stack=0xdc2dd000-0xdc2df000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b99 [vdso] (__kernel_vsyscall+9) + native: #01 pc 000cf496 /apex/com.android.runtime/lib/bionic/libc.so (__ppoll+38) + native: #02 pc 00083979 /apex/com.android.runtime/lib/bionic/libc.so (poll+105) + native: #03 pc 0000a493 /apex/com.android.art/lib/libadbconnection.so (adbconnection::AdbConnectionState::RunPollLoop(art::Thread*)+1171) + native: #04 pc 000086d2 /apex/com.android.art/lib/libadbconnection.so (adbconnection::CallbackFunction(void*)+1666) + native: #05 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #06 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"perfetto_hprof_listener" prio=10 tid=6 Native (still starting up) + | group="" sCount=1 dsCount=0 flags=1 obj=0x0 self=0xe7385410 + | sysTid=9217 nice=-20 cgrp=top-app sched=0/0 handle=0xdc4d91e0 + | state=S schedstat=( 6280850 9339243 10 ) utm=0 stm=0 core=0 HZ=100 + | stack=0xdc3de000-0xdc3e0000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 000ccf9c /apex/com.android.runtime/lib/bionic/libc.so (read+28) + native: #02 pc 0001aca2 /apex/com.android.art/lib/libperfetto_hprof.so (void* std::__1::__thread_proxy >, ArtPlugin_Initialize::$_29> >(void*)+306) + native: #03 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #04 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"HeapTaskDaemon" daemon prio=5 tid=7 WaitingForTaskProcessor + | group="system" sCount=1 dsCount=0 flags=1 obj=0x130410c8 self=0xe7384610 + | sysTid=9220 nice=4 cgrp=top-app sched=0/0 handle=0xc74921e0 + | state=S schedstat=( 43450902 17904055 20 ) utm=1 stm=2 core=1 HZ=100 + | stack=0xc738f000-0xc7391000 stackSize=1040KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 0005ad68 /apex/com.android.runtime/lib/bionic/libc.so (syscall+40) + native: #02 pc 001d82ec /apex/com.android.art/lib/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+108) + native: #03 pc 001d8273 /apex/com.android.art/lib/libart.so (art::ConditionVariable::Wait(art::Thread*)+35) + native: #04 pc 0034a036 /apex/com.android.art/lib/libart.so (art::gc::TaskProcessor::GetTask(art::Thread*)+630) + native: #05 pc 0034aa64 /apex/com.android.art/lib/libart.so (art::gc::TaskProcessor::RunAllTasks(art::Thread*)+84) + native: #06 pc 005591f5 /apex/com.android.art/lib/libart.so (art::VMRuntime_runHeapTasks(_JNIEnv*, _jobject*)+53) + at dalvik.system.VMRuntime.runHeapTasks(Native method) + at java.lang.Daemons$HeapTaskDaemon.runInternal(Daemons.java:531) + at java.lang.Daemons$Daemon.run(Daemons.java:139) + at java.lang.Thread.run(Thread.java:923) + +"Binder:9207_1" prio=5 tid=8 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x13040c18 self=0xe7380010 + | sysTid=9224 nice=0 cgrp=top-app sched=0/0 handle=0xc6f701e0 + | state=S schedstat=( 1496174 7615537 6 ) utm=0 stm=0 core=1 HZ=100 + | stack=0xc6e75000-0xc6e77000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 000cd46c /apex/com.android.runtime/lib/bionic/libc.so (__ioctl+28) + native: #02 pc 00080e6a /apex/com.android.runtime/lib/bionic/libc.so (ioctl+58) + native: #03 pc 00050edb /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+331) + native: #04 pc 0005117a /system/lib/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+42) + native: #05 pc 00051cb8 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+72) + native: #06 pc 0007e309 /system/lib/libbinder.so (android::PoolThread::threadLoop()+41) + native: #07 pc 00015116 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+374) + native: #08 pc 00098fee /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+174) + native: #09 pc 000147d9 /system/lib/libutils.so (thread_data_t::trampoline(thread_data_t const*)+457) + native: #10 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #11 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"FinalizerWatchdogDaemon" daemon prio=5 tid=9 Waiting + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040c90 self=0xe7382a10 + | sysTid=9223 nice=4 cgrp=top-app sched=0/0 handle=0xc71771e0 + | state=S schedstat=( 398422 6565402 4 ) utm=0 stm=0 core=1 HZ=100 + | stack=0xc7074000-0xc7076000 stackSize=1040KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x02d3c777> (a java.lang.Daemons$FinalizerWatchdogDaemon) + at java.lang.Object.wait(Object.java:442) + at java.lang.Object.wait(Object.java:568) + at java.lang.Daemons$FinalizerWatchdogDaemon.sleepUntilNeeded(Daemons.java:341) + - locked <0x02d3c777> (a java.lang.Daemons$FinalizerWatchdogDaemon) + at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:321) + at java.lang.Daemons$Daemon.run(Daemons.java:139) + at java.lang.Thread.run(Thread.java:923) + +"ReferenceQueueDaemon" daemon prio=5 tid=10 Waiting + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040d08 self=0xe7388c10 + | sysTid=9221 nice=4 cgrp=top-app sched=0/0 handle=0xc73891e0 + | state=S schedstat=( 533566 3015911 3 ) utm=0 stm=0 core=1 HZ=100 + | stack=0xc7286000-0xc7288000 stackSize=1040KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x07fc5fe4> (a java.lang.Class) + at java.lang.Object.wait(Object.java:442) + at java.lang.Object.wait(Object.java:568) + at java.lang.Daemons$ReferenceQueueDaemon.runInternal(Daemons.java:217) + - locked <0x07fc5fe4> (a java.lang.Class) + at java.lang.Daemons$Daemon.run(Daemons.java:139) + at java.lang.Thread.run(Thread.java:923) + +"Jit thread pool worker thread 0" daemon prio=5 tid=11 Native + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040d80 self=0xe7387e10 + | sysTid=9219 nice=0 cgrp=top-app sched=0/0 handle=0xc7596d60 + | state=S schedstat=( 468316720 500776964 211 ) utm=6 stm=39 core=0 HZ=100 + | stack=0xc7498000-0xc749a000 stackSize=1023KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 0005ad68 /apex/com.android.runtime/lib/bionic/libc.so (syscall+40) + native: #02 pc 001d82ec /apex/com.android.art/lib/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+108) + native: #03 pc 001d8273 /apex/com.android.art/lib/libart.so (art::ConditionVariable::Wait(art::Thread*)+35) + native: #04 pc 006c838f /apex/com.android.art/lib/libart.so (art::ThreadPool::GetTask(art::Thread*)+143) + native: #05 pc 006c73e5 /apex/com.android.art/lib/libart.so (art::ThreadPoolWorker::Run()+133) + native: #06 pc 006c6e9d /apex/com.android.art/lib/libart.so (art::ThreadPoolWorker::Callback(void*)+269) + native: #07 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #08 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"FinalizerDaemon" daemon prio=5 tid=12 Waiting + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040df8 self=0xe7386210 + | sysTid=9222 nice=4 cgrp=top-app sched=0/0 handle=0xc72801e0 + | state=S schedstat=( 1102483 7783564 5 ) utm=0 stm=0 core=1 HZ=100 + | stack=0xc717d000-0xc717f000 stackSize=1040KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0a047c4d> (a java.lang.Object) + at java.lang.Object.wait(Object.java:442) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:190) + - locked <0x0a047c4d> (a java.lang.Object) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:211) + at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:273) + at java.lang.Daemons$Daemon.run(Daemons.java:139) + at java.lang.Thread.run(Thread.java:923) + +"Binder:9207_2" prio=5 tid=13 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x13040e70 self=0xe7389a10 + | sysTid=9225 nice=0 cgrp=top-app sched=0/0 handle=0xc6e6f1e0 + | state=S schedstat=( 210561432 574003421 20 ) utm=10 stm=10 core=1 HZ=100 + | stack=0xc6d74000-0xc6d76000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 000cd46c /apex/com.android.runtime/lib/bionic/libc.so (__ioctl+28) + native: #02 pc 00080e6a /apex/com.android.runtime/lib/bionic/libc.so (ioctl+58) + native: #03 pc 00050edb /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+331) + native: #04 pc 0005117a /system/lib/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+42) + native: #05 pc 00051cb8 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+72) + native: #06 pc 0007e309 /system/lib/libbinder.so (android::PoolThread::threadLoop()+41) + native: #07 pc 00015116 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+374) + native: #08 pc 00098fee /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+174) + native: #09 pc 000147d9 /system/lib/libutils.so (thread_data_t::trampoline(thread_data_t const*)+457) + native: #10 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #11 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"Binder:9207_3" prio=5 tid=14 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x13040ee8 self=0xe7391810 + | sysTid=9226 nice=0 cgrp=top-app sched=0/0 handle=0xc6d6e1e0 + | state=S schedstat=( 17000557 50151632 14 ) utm=0 stm=1 core=1 HZ=100 + | stack=0xc6c73000-0xc6c75000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 000cd46c /apex/com.android.runtime/lib/bionic/libc.so (__ioctl+28) + native: #02 pc 00080e6a /apex/com.android.runtime/lib/bionic/libc.so (ioctl+58) + native: #03 pc 00050edb /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+331) + native: #04 pc 0005117a /system/lib/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+42) + native: #05 pc 00051cb8 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+72) + native: #06 pc 0007e309 /system/lib/libbinder.so (android::PoolThread::threadLoop()+41) + native: #07 pc 00015116 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+374) + native: #08 pc 00098fee /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+174) + native: #09 pc 000147d9 /system/lib/libutils.so (thread_data_t::trampoline(thread_data_t const*)+457) + native: #10 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #11 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"Binder:9207_4" prio=5 tid=15 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x13040f60 self=0xe7390a10 + | sysTid=9227 nice=0 cgrp=top-app sched=0/0 handle=0xc68411e0 + | state=S schedstat=( 8392907 55881465 12 ) utm=0 stm=0 core=0 HZ=100 + | stack=0xc6746000-0xc6748000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 000cd46c /apex/com.android.runtime/lib/bionic/libc.so (__ioctl+28) + native: #02 pc 00080e6a /apex/com.android.runtime/lib/bionic/libc.so (ioctl+58) + native: #03 pc 00050edb /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+331) + native: #04 pc 0005117a /system/lib/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+42) + native: #05 pc 00051cb8 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+72) + native: #06 pc 0007e309 /system/lib/libbinder.so (android::PoolThread::threadLoop()+41) + native: #07 pc 00015116 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+374) + native: #08 pc 00098fee /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+174) + native: #09 pc 000147d9 /system/lib/libutils.so (thread_data_t::trampoline(thread_data_t const*)+457) + native: #10 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #11 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"Profile Saver" daemon prio=5 tid=16 Native + | group="system" sCount=1 dsCount=0 flags=1 obj=0x13040fd8 self=0xe738ee10 + | sysTid=9228 nice=9 cgrp=top-app sched=0/0 handle=0xc67401e0 + | state=S schedstat=( 14918009 13675595 16 ) utm=1 stm=0 core=1 HZ=100 + | stack=0xc6645000-0xc6647000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7) + native: #01 pc 0005ad68 /apex/com.android.runtime/lib/bionic/libc.so (syscall+40) + native: #02 pc 001d82ec /apex/com.android.art/lib/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+108) + native: #03 pc 001d8273 /apex/com.android.art/lib/libart.so (art::ConditionVariable::Wait(art::Thread*)+35) + native: #04 pc 003cc829 /apex/com.android.art/lib/libart.so (art::ProfileSaver::Run()+633) + native: #05 pc 003d2e8f /apex/com.android.art/lib/libart.so (art::ProfileSaver::RunProfileSaverThread(void*)+175) + native: #06 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #07 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"RenderThread" daemon prio=7 tid=17 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x13041050 self=0xe7392610 + | sysTid=9229 nice=-10 cgrp=top-app sched=0/0 handle=0xc663f1e0 + | state=S schedstat=( 236695461 71606987 112 ) utm=4 stm=19 core=1 HZ=100 + | stack=0xc6544000-0xc6546000 stackSize=1008KB + | held mutexes= + native: #00 pc 00000b99 [vdso] (__kernel_vsyscall+9) + native: #01 pc 000cf2cb /apex/com.android.runtime/lib/bionic/libc.so (__epoll_pwait+43) + native: #02 pc 00088f9d /apex/com.android.runtime/lib/bionic/libc.so (epoll_wait+45) + native: #03 pc 0001a003 /system/lib/libutils.so (android::Looper::pollInner(int)+259) + native: #04 pc 00019e96 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+118) + native: #05 pc 002452c5 /system/lib/libhwui.so (android::uirenderer::ThreadBase::waitForWork()+149) + native: #06 pc 0026cab7 /system/lib/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop()+119) + native: #07 pc 00015116 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+374) + native: #08 pc 000147d9 /system/lib/libutils.so (thread_data_t::trampoline(thread_data_t const*)+457) + native: #09 pc 000e6974 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+100) + native: #10 pc 00078567 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+71) + (no managed stack frames) + +"BacktraceHandlerThread" prio=5 tid=18 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x12cf4660 self=0xe738fc10 + | sysTid=9232 nice=0 cgrp=top-app sched=0/0 handle=0xc64281e0 + | state=S schedstat=( 313706628 570346891 185 ) utm=20 stm=11 core=0 HZ=100 + | stack=0xc6325000-0xc6327000 stackSize=1040KB + | held mutexes= + native: #00 pc 00000b99 [vdso] (__kernel_vsyscall+9) + native: #01 pc 000cf2cb /apex/com.android.runtime/lib/bionic/libc.so (__epoll_pwait+43) + native: #02 pc 00088f9d /apex/com.android.runtime/lib/bionic/libc.so (epoll_wait+45) + native: #03 pc 0001a003 /system/lib/libutils.so (android::Looper::pollInner(int)+259) + native: #04 pc 00019e96 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+118) + native: #05 pc 0010ef8b /system/lib/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long long, int)+59) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loop(Looper.java:183) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"Timer-0" prio=5 tid=19 TimedWaiting + | group="main" sCount=1 dsCount=0 flags=1 obj=0x12cf5498 self=0xe738e010 + | sysTid=9233 nice=0 cgrp=top-app sched=0/0 handle=0xc62461e0 + | state=S schedstat=( 7524451 0 14 ) utm=0 stm=0 core=0 HZ=100 + | stack=0xc6143000-0xc6145000 stackSize=1040KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x02569302> (a java.util.TaskQueue) + at java.lang.Object.wait(Object.java:442) + at java.util.TimerThread.mainLoop(Timer.java:559) + - locked <0x02569302> (a java.util.TaskQueue) + at java.util.TimerThread.run(Timer.java:512) + +"WifiManagerThread" prio=5 tid=20 Native + | group="main" sCount=1 dsCount=0 flags=1 obj=0x12cff1c0 self=0xe7399610 + | sysTid=9236 nice=0 cgrp=top-app sched=0/0 handle=0xc613d1e0 + | state=S schedstat=( 581694 0 1 ) utm=0 stm=0 core=0 HZ=100 + | stack=0xc603a000-0xc603c000 stackSize=1040KB + | held mutexes= + native: #00 pc 00000b99 [vdso] (__kernel_vsyscall+9) + native: #01 pc 000cf2cb /apex/com.android.runtime/lib/bionic/libc.so (__epoll_pwait+43) + native: #02 pc 00088f9d /apex/com.android.runtime/lib/bionic/libc.so (epoll_wait+45) + native: #03 pc 0001a003 /system/lib/libutils.so (android::Looper::pollInner(int)+259) + native: #04 pc 00019e96 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+118) + native: #05 pc 0010ef8b /system/lib/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long long, int)+59) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loop(Looper.java:183) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"Thread-4" prio=5 tid=21 Sleeping + | group="main" sCount=1 dsCount=0 flags=1 obj=0x12d8a710 self=0xe7394210 + | sysTid=9237 nice=0 cgrp=top-app sched=0/0 handle=0xc60341e0 + | state=S schedstat=( 76232117 47640734 71 ) utm=5 stm=2 core=0 HZ=100 + | stack=0xc5f31000-0xc5f33000 stackSize=1040KB + | held mutexes= + at java.lang.Thread.sleep(Native method) + - sleeping on <0x05336413> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:442) + - locked <0x05336413> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:358) + at backtraceio.library.watchdog.BacktraceANRHandlerWatchdog.run(BacktraceANRHandlerWatchdog.java:118) + +"OkHttp ConnectionPool" daemon prio=5 tid=22 TimedWaiting + | group="main" sCount=1 dsCount=0 flags=1 obj=0x12d39c50 self=0xe739b210 + | sysTid=9239 nice=0 cgrp=top-app sched=0/0 handle=0xc56251e0 + | state=S schedstat=( 442059 1414330 1 ) utm=0 stm=0 core=0 HZ=100 + | stack=0xc5522000-0xc5524000 stackSize=1040KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x09119650> (a com.android.okhttp.ConnectionPool) + at com.android.okhttp.ConnectionPool$1.run(ConnectionPool.java:106) + - locked <0x09119650> (a com.android.okhttp.ConnectionPool) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) + at java.lang.Thread.run(Thread.java:923) + +----- end 9207 ----- + +----- Waiting Channels: pid 9207 at 2025-03-27 21:02:38 ----- +Cmd line: backtraceio.backtraceio + +sysTid=9207 0 +sysTid=9216 do_sigtimedwait +sysTid=9217 pipe_read +sysTid=9218 do_sys_poll +sysTid=9219 futex_wait_queue_me +sysTid=9220 futex_wait_queue_me +sysTid=9221 futex_wait_queue_me +sysTid=9222 futex_wait_queue_me +sysTid=9223 futex_wait_queue_me +sysTid=9224 binder_thread_read +sysTid=9225 binder_thread_read +sysTid=9226 binder_thread_read +sysTid=9227 binder_thread_read +sysTid=9228 futex_wait_queue_me +sysTid=9229 do_epoll_wait +sysTid=9232 do_epoll_wait +sysTid=9233 futex_wait_queue_me +sysTid=9236 do_epoll_wait +sysTid=9237 futex_wait_queue_me +sysTid=9239 futex_wait_queue_me + +----- end 9207 ----- diff --git a/packages/react-native/src/BacktraceClient.ts b/packages/react-native/src/BacktraceClient.ts index f7acd98b..b3711609 100644 --- a/packages/react-native/src/BacktraceClient.ts +++ b/packages/react-native/src/BacktraceClient.ts @@ -9,7 +9,9 @@ import { type DebugIdContainer, } from '@backtrace/sdk-core'; import { NativeModules, Platform } from 'react-native'; -import { type BacktraceConfiguration } from './BacktraceConfiguration'; +import { AnrReporter } from './anr/AnrReporter'; +import { AnrWatchdogHandler } from './anr/AnrWatchdogHandler'; +import { BacktraceAnrType, type BacktraceConfiguration } from './BacktraceConfiguration'; import { FileBreadcrumbsStorage } from './breadcrumbs/FileBreadcrumbsStorage'; import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder'; import type { BacktraceClientSetup } from './builder/BacktraceClientSetup'; @@ -24,6 +26,7 @@ import { type FileSystem } from './storage/FileSystem'; export class BacktraceClient extends BacktraceCoreClient { private _crashReporter?: CrashReporter; + private _anrWatchdogHandler?: AnrWatchdogHandler; private readonly _exceptionHandler: ExceptionHandler = generateUnhandledExceptionHandler(); public crash(): void { @@ -81,6 +84,7 @@ export class BacktraceClient extends BacktraceCoreClient ); this._crashReporter = this.initializeNativeCrashReporter(); + this.reportApplicationNotResponding(); } finally { lockId && this.sessionFiles?.unlockPreviousSessions(lockId); } @@ -88,6 +92,7 @@ export class BacktraceClient extends BacktraceCoreClient public dispose(): void { this._exceptionHandler.dispose(); + this._anrWatchdogHandler?.dispose(); this._crashReporter?.dispose(); super.dispose(); } @@ -133,6 +138,26 @@ export class BacktraceClient extends BacktraceCoreClient } } + private reportApplicationNotResponding(): void { + const anr = this.options.anr; + if (!anr?.enable) { + return; + } + + if (anr.type === BacktraceAnrType.ApplicationExit) { + const fileSystem = this.fileSystem as FileSystem | undefined; + if (!fileSystem) { + return; + } + + AnrReporter.create(fileSystem)?.report(this); + return; + } + + this._anrWatchdogHandler = AnrWatchdogHandler.create(); + this._anrWatchdogHandler?.start(this, anr.timeout ?? 0, anr.disableWhenDebuggerAttached ?? false); + } + private initializeNativeCrashReporter(): CrashReporter | undefined { if (!this.options.database?.enable) { return; diff --git a/packages/react-native/src/BacktraceConfiguration.ts b/packages/react-native/src/BacktraceConfiguration.ts index 46f3ba97..56b092ec 100644 --- a/packages/react-native/src/BacktraceConfiguration.ts +++ b/packages/react-native/src/BacktraceConfiguration.ts @@ -1,2 +1,41 @@ import { type BacktraceConfiguration as SdkConfiguration } from '@backtrace/sdk-core'; -export interface BacktraceConfiguration extends SdkConfiguration {} + +export enum BacktraceAnrType { + Threshold = 'threshold', + ApplicationExit = 'applicationExit', +} + +export interface BacktraceAnrConfiguration { + /** + * Determines if Application Not Responding detection is enabled. Android only. + * By default the value is set to false. + */ + enable?: boolean; + + /** + * Detection mechanism. `Threshold` watches the main thread and reports as soon as it is + * blocked, on all supported Android versions. `ApplicationExit` reports ANRs the system recorded, + * on the next application start, and requires API 30 or above. + * By default the value is set to `Threshold`. + */ + type?: BacktraceAnrType; + + /** + * Time in milliseconds the main thread must be blocked before an ANR is reported. + * Applies to the `Threshold` type only. By default the value is set to 5000. + */ + timeout?: number; + + /** + * When true, detection is disabled while a debugger is attached. + * Applies to the `Threshold` type only. By default the value is set to false. + */ + disableWhenDebuggerAttached?: boolean; +} + +export interface BacktraceConfiguration extends SdkConfiguration { + /** + * Application Not Responding settings + */ + anr?: BacktraceAnrConfiguration; +} diff --git a/packages/react-native/src/anr/AnrException.ts b/packages/react-native/src/anr/AnrException.ts new file mode 100644 index 00000000..a5c02e00 --- /dev/null +++ b/packages/react-native/src/anr/AnrException.ts @@ -0,0 +1,10 @@ +export class AnrException extends Error { + public readonly name = 'Hang'; + + constructor( + public readonly message: string, + public readonly stack: string, + ) { + super(message); + } +} diff --git a/packages/react-native/src/anr/AnrReporter.ts b/packages/react-native/src/anr/AnrReporter.ts new file mode 100644 index 00000000..2ff93667 --- /dev/null +++ b/packages/react-native/src/anr/AnrReporter.ts @@ -0,0 +1,122 @@ +import { + BacktraceReport, + BacktraceStringAttachment, + type BacktraceStackFrame, + type BacktraceSubmissionStatus, +} from '@backtrace/sdk-core'; +import { NativeModules, Platform } from 'react-native'; +import type { BacktraceClient } from '../BacktraceClient'; +import { DebuggerHelper } from '../common/DebuggerHelper'; +import type { FileSystem } from '../storage/FileSystem'; +import { AnrException } from './AnrException'; +import { addOtherThreads, type AnrThread } from './AnrThreads'; + +export interface AnrExitInfoRecord { + timestamp: number; + message: string; + attributes: Record; + stackTrace?: string; + mainThreadFrames?: BacktraceStackFrame[]; + threads?: AnrThread[]; +} + +export class AnrReporter { + private static readonly LastTimestampFileName = 'backtrace-anr-last-timestamp'; + private static readonly DatabaseRetriedStatuses: BacktraceSubmissionStatus[] = [ + 'Network Error', + 'Server Error', + 'Invalid token', + 'Unknown', + 'Unsupported', + ]; + + private constructor( + private readonly _fileSystem: FileSystem, + private readonly _lastTimestampPath: string, + ) {} + + public static create(fileSystem: FileSystem): AnrReporter | undefined { + if (Platform.OS !== 'android') { + return undefined; + } + + if (!DebuggerHelper.isNativeBridgeEnabled()) { + return undefined; + } + + if (!NativeModules.BacktraceReactNative?.getAnrExitInfo) { + return undefined; + } + + const applicationDirectory = fileSystem.applicationDirectory(); + if (!applicationDirectory) { + return undefined; + } + + return new AnrReporter(fileSystem, `${applicationDirectory}/${AnrReporter.LastTimestampFileName}`); + } + + public async report(client: BacktraceClient): Promise { + try { + const records: AnrExitInfoRecord[] = await NativeModules.BacktraceReactNative.getAnrExitInfo( + await this.readLastTimestamp(), + ); + + // records arrive oldest first, so stopping leaves the rest for the next launch + for (const record of records) { + if (!record.mainThreadFrames?.length) { + // no groupable stack, but still save its timestamp or it is re-read on every launch + await this.saveLastTimestamp(record.timestamp); + continue; + } + + const result = await client.send(this.buildReport(record)); + if (this.needsRetry(client, result.status)) { + return; + } + + await this.saveLastTimestamp(record.timestamp); + } + } catch (err) { + console.warn('Backtrace: cannot report ANRs from application exit info.', err); + } + } + + // the database stores every report before the send attempt and retries failures from there + private needsRetry(client: BacktraceClient, status: BacktraceSubmissionStatus): boolean { + if (status === 'Ok') { + return false; + } + return !client.database || !AnrReporter.DatabaseRetriedStatuses.includes(status); + } + + private buildReport(record: AnrExitInfoRecord): BacktraceReport { + // an exception stack is copied into the error annotation, so the dump goes only to the attachment + const report = new BacktraceReport( + new AnrException(record.message, ''), + { + ...record.attributes, + 'error.type': 'Hang', + }, + record.stackTrace ? [new BacktraceStringAttachment('anr-stacktrace.txt', record.stackTrace)] : [], + { timestamp: record.timestamp }, + ); + + report.addStackTrace('main', record.mainThreadFrames as BacktraceStackFrame[]); + addOtherThreads(report, record.threads); + return report; + } + + private async readLastTimestamp(): Promise { + if (!(await this._fileSystem.exists(this._lastTimestampPath))) { + return 0; + } + + const timestamp = parseInt(await this._fileSystem.readFile(this._lastTimestampPath), 10); + return Number.isFinite(timestamp) ? timestamp : 0; + } + + private saveLastTimestamp(timestamp: number): Promise { + return this._fileSystem.writeFile(this._lastTimestampPath, timestamp.toString()); + } +} diff --git a/packages/react-native/src/anr/AnrThreads.ts b/packages/react-native/src/anr/AnrThreads.ts new file mode 100644 index 00000000..778849e3 --- /dev/null +++ b/packages/react-native/src/anr/AnrThreads.ts @@ -0,0 +1,19 @@ +import { BacktraceReport, type BacktraceStackFrame } from '@backtrace/sdk-core'; + +export interface AnrThread { + name: string; + frames: BacktraceStackFrame[]; +} + +export function addOtherThreads(report: BacktraceReport, threads?: AnrThread[]): void { + const usedNames = new Set(['main']); + for (const thread of threads ?? []) { + const base = thread.name || 'unknown'; + let name = base; + for (let ordinal = 2; usedNames.has(name); ordinal++) { + name = `${base}-${ordinal}`; + } + usedNames.add(name); + report.addStackTrace(name, thread.frames); + } +} diff --git a/packages/react-native/src/anr/AnrWatchdogHandler.ts b/packages/react-native/src/anr/AnrWatchdogHandler.ts new file mode 100644 index 00000000..4afcb1ca --- /dev/null +++ b/packages/react-native/src/anr/AnrWatchdogHandler.ts @@ -0,0 +1,82 @@ +import { BacktraceReport, type BacktraceStackFrame } from '@backtrace/sdk-core'; +import { + AppState, + NativeEventEmitter, + NativeModules, + Platform, + type EmitterSubscription, + type NativeEventSubscription, +} from 'react-native'; +import type { BacktraceClient } from '../BacktraceClient'; +import { DebuggerHelper } from '../common/DebuggerHelper'; +import { AnrException } from './AnrException'; +import { addOtherThreads, type AnrThread } from './AnrThreads'; + +const AnrDetectedEvent = 'BacktraceAnrDetected'; + +interface AnrDetectedPayload { + stackTrace: string; + frames: BacktraceStackFrame[]; + threads?: AnrThread[]; +} + +export class AnrWatchdogHandler { + private _subscription?: EmitterSubscription; + private _appStateSubscription?: NativeEventSubscription; + + private constructor(private readonly _watchdog: NonNullable) {} + + public static create(): AnrWatchdogHandler | undefined { + if (Platform.OS !== 'android') { + return undefined; + } + + if (!DebuggerHelper.isNativeBridgeEnabled()) { + return undefined; + } + + const watchdog = NativeModules.BacktraceAnrWatchdog; + if (!watchdog?.start) { + return undefined; + } + + return new AnrWatchdogHandler(watchdog); + } + + public start(client: BacktraceClient, timeout: number, disableWhenDebuggerAttached: boolean): void { + this._subscription = new NativeEventEmitter(this._watchdog).addListener( + AnrDetectedEvent, + (payload: AnrDetectedPayload) => { + client.breadcrumbs?.info('ANR detected - thread is blocked'); + + const report = new BacktraceReport( + new AnrException('Application Not Responding | Blocked thread detected', payload.stackTrace), + { 'error.type': 'Hang' }, + [], + ); + report.addStackTrace('main', payload.frames); + addOtherThreads(report, payload.threads); + client.send(report); + }, + ); + + // Android freezes backgrounded apps, and the watchdog would misread the resume as a hang + this._appStateSubscription = AppState.addEventListener('change', (state) => { + if (state === 'background') { + this._watchdog.stop(); + } else if (state === 'active') { + this._watchdog.start(timeout, disableWhenDebuggerAttached); + } + }); + + this._watchdog.start(timeout, disableWhenDebuggerAttached); + } + + public dispose(): void { + this._subscription?.remove(); + this._subscription = undefined; + this._appStateSubscription?.remove(); + this._appStateSubscription = undefined; + this._watchdog.stop?.(); + } +} diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 1a187999..b19a5ab1 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -13,7 +13,11 @@ export { } from '@backtrace/sdk-core'; export * from './attachment/'; export { BacktraceClient } from './BacktraceClient'; -export { type BacktraceConfiguration } from './BacktraceConfiguration'; +export { + BacktraceAnrType, + type BacktraceAnrConfiguration, + type BacktraceConfiguration, +} from './BacktraceConfiguration'; export { BacktraceClientBuilder } from './builder/BacktraceClientBuilder'; export { ErrorBoundary } from './ErrorBoundary'; export * from './storage/'; diff --git a/packages/react-native/tests/anrReporterTests.spec.ts b/packages/react-native/tests/anrReporterTests.spec.ts new file mode 100644 index 00000000..4c3d852f --- /dev/null +++ b/packages/react-native/tests/anrReporterTests.spec.ts @@ -0,0 +1,177 @@ +// The reporter sends each ANR the system recorded exactly once: it saves the timestamp of the +// last accepted report and resumes from it on the next launch. Faked native records and send +// results walk the retry and loss orderings a device cannot produce on demand. +import { NativeModules, Platform } from 'react-native'; + +// This package's jest config replaces the react-native preset's setupFiles, so the real Platform throws. +jest.mock('react-native', () => ({ + NativeModules: {}, + Platform: { + OS: 'android', + select: (options: Record) => + options.android !== undefined ? options.android : options.default, + }, +})); + +const getAnrExitInfo = jest.fn(); +NativeModules.BacktraceReactNative = { getAnrExitInfo }; +(globalThis as unknown as { RN$Bridgeless: boolean }).RN$Bridgeless = true; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { AnrReporter } = require('../src/anr/AnrReporter'); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const LAST_TIMESTAMP_PATH = '/data/backtrace-anr-last-timestamp'; + +function createFileSystem(files: Record = {}) { + const storage = new Map(Object.entries(files)); + return { + storage, + applicationDirectory: () => '/data', + exists: async (path: string) => storage.has(path), + readFile: async (path: string) => storage.get(path) as string, + writeFile: async (path: string, content: string) => { + storage.set(path, content); + }, + }; +} + +function createClient(send = jest.fn().mockResolvedValue({ status: 'Ok' })) { + return { send } as never; +} + +function record(timestamp: number, frames?: object[]) { + return { + timestamp, + message: `anr at ${timestamp}`, + attributes: { PID: 123 }, + stackTrace: 'raw dump', + mainThreadFrames: frames, + } as { threads?: object[] } & Record; +} + +const FRAMES = [{ funcName: 'android.os.MessageQueue.next', library: 'MessageQueue.java', line: 335 }]; + +const OTHER_THREADS = [ + { name: 'FinalizerDaemon', frames: [{ funcName: 'java.lang.Object.wait', library: 'Object.java', line: 405 }] }, + { name: 'FinalizerDaemon', frames: [{ funcName: 'java.lang.Thread.run', library: 'Thread.java', line: 1572 }] }, +]; + +describe('AnrReporter', () => { + beforeEach(() => { + getAnrExitInfo.mockReset(); + Platform.OS = 'android'; + }); + + it('does not create on platforms other than android', () => { + Platform.OS = 'ios'; + expect(AnrReporter.create(createFileSystem())).toBeUndefined(); + }); + + it('does not create without the native module method', () => { + const saved = NativeModules.BacktraceReactNative; + NativeModules.BacktraceReactNative = {}; + expect(AnrReporter.create(createFileSystem())).toBeUndefined(); + NativeModules.BacktraceReactNative = saved; + }); + + it('passes the stored last timestamp to the native reader', async () => { + const fileSystem = createFileSystem({ [LAST_TIMESTAMP_PATH]: '1234' }); + getAnrExitInfo.mockResolvedValue([]); + + await AnrReporter.create(fileSystem)?.report(createClient()); + + expect(getAnrExitInfo).toHaveBeenCalledWith(1234); + }); + + it('sends records oldest first and saves the timestamp after each accepted send', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(1, FRAMES), record(2, FRAMES)]); + const send = jest.fn().mockResolvedValue({ status: 'Ok' }); + + await AnrReporter.create(fileSystem)?.report(createClient(send)); + + expect(send).toHaveBeenCalledTimes(2); + expect(send.mock.calls[0][0].attributes['error.type']).toBe('Hang'); + expect(send.mock.calls[0][0].timestamp).toBe(1); + expect(send.mock.calls[1][0].timestamp).toBe(2); + expect(fileSystem.storage.get(LAST_TIMESTAMP_PATH)).toBe('2'); + }); + + it('stops on a failed send and keeps the last timestamp at the last accepted record', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(1, FRAMES), record(2, FRAMES), record(3, FRAMES)]); + const send = jest + .fn() + .mockResolvedValueOnce({ status: 'Ok' }) + .mockResolvedValueOnce({ status: 'Network Error' }); + + await AnrReporter.create(fileSystem)?.report(createClient(send)); + + expect(send).toHaveBeenCalledTimes(2); + expect(fileSystem.storage.get(LAST_TIMESTAMP_PATH)).toBe('1'); + }); + + it('saves the timestamp on a failed send that the database stores for retry', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(1, FRAMES), record(2, FRAMES)]); + const send = jest.fn().mockResolvedValue({ status: 'Network Error' }); + const client = { send, database: {} } as never; + + await AnrReporter.create(fileSystem)?.report(client); + + expect(send).toHaveBeenCalledTimes(2); + expect(fileSystem.storage.get(LAST_TIMESTAMP_PATH)).toBe('2'); + }); + + it('keeps the last timestamp on a rate-limited send even with a database', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(1, FRAMES)]); + const send = jest.fn().mockResolvedValue({ status: 'Limit reached' }); + const client = { send, database: {} } as never; + + await AnrReporter.create(fileSystem)?.report(client); + + expect(fileSystem.storage.get(LAST_TIMESTAMP_PATH)).toBeUndefined(); + }); + + it('skips a record without frames but still saves its timestamp', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(7, undefined)]); + const send = jest.fn(); + + await AnrReporter.create(fileSystem)?.report(createClient(send)); + + expect(send).not.toHaveBeenCalled(); + expect(fileSystem.storage.get(LAST_TIMESTAMP_PATH)).toBe('7'); + }); + + it('attaches the parsed frames as the main thread and the raw dump as an attachment', async () => { + const fileSystem = createFileSystem(); + getAnrExitInfo.mockResolvedValue([record(1, FRAMES)]); + const send = jest.fn().mockResolvedValue({ status: 'Ok' }); + + await AnrReporter.create(fileSystem)?.report(createClient(send)); + + const report = send.mock.calls[0][0]; + expect(report.stackTrace['main']).toEqual(FRAMES); + expect(report.attachments).toHaveLength(1); + expect(report.attachments[0].name).toBe('anr-stacktrace.txt'); + expect(report.attributes['PID']).toBe(123); + }); + + it('attaches the other threads from the dump and renames collisions', async () => { + const fileSystem = createFileSystem(); + const anr = record(1, FRAMES); + anr.threads = OTHER_THREADS; + getAnrExitInfo.mockResolvedValue([anr]); + const send = jest.fn().mockResolvedValue({ status: 'Ok' }); + + await AnrReporter.create(fileSystem)?.report(createClient(send)); + + const report = send.mock.calls[0][0]; + expect(report.stackTrace['main']).toEqual(FRAMES); + expect(report.stackTrace['FinalizerDaemon']).toEqual(OTHER_THREADS[0].frames); + expect(report.stackTrace['FinalizerDaemon-2']).toEqual(OTHER_THREADS[1].frames); + }); +}); diff --git a/packages/react-native/tests/anrWatchdogHandlerTests.spec.ts b/packages/react-native/tests/anrWatchdogHandlerTests.spec.ts new file mode 100644 index 00000000..c7abc9e8 --- /dev/null +++ b/packages/react-native/tests/anrWatchdogHandlerTests.spec.ts @@ -0,0 +1,134 @@ +// A hang report only exists if the handler hears the native event and sends it; a broken +// subscription loses every report with no error anywhere. Mocked native modules and faked +// app-state changes cover the event flow and the background pause. +import { AppState, NativeModules, Platform } from 'react-native'; + +// This package's jest config replaces the react-native preset's setupFiles, so the real Platform throws. +jest.mock('react-native', () => { + const appStateListeners: Array<(state: string) => void> = []; + let emitterListener: ((payload: unknown) => void) | undefined; + + class NativeEventEmitter { + public addListener(_: string, callback: (payload: unknown) => void) { + emitterListener = callback; + return { remove: jest.fn() }; + } + + public static emit(payload: unknown) { + emitterListener?.(payload); + } + } + + return { + NativeModules: {}, + Platform: { + OS: 'android', + select: (options: Record) => + options.android !== undefined ? options.android : options.default, + }, + NativeEventEmitter, + AppState: { + __listeners: appStateListeners, + addEventListener: (_: string, callback: (state: string) => void) => { + appStateListeners.push(callback); + return { remove: jest.fn() }; + }, + }, + }; +}); + +const watchdogMock = { + start: jest.fn(), + stop: jest.fn(), +}; +NativeModules.BacktraceAnrWatchdog = watchdogMock; +(globalThis as unknown as { RN$Bridgeless: boolean }).RN$Bridgeless = true; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { AnrWatchdogHandler } = require('../src/anr/AnrWatchdogHandler'); +const { NativeEventEmitter: EmitterMock } = require('react-native'); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const PAYLOAD = { + stackTrace: 'android.os.MessageQueue.next(MessageQueue.java:335)\n', + frames: [{ funcName: 'android.os.MessageQueue.next', library: 'MessageQueue.java', line: 335 }], + threads: [ + { name: 'worker', frames: [{ funcName: 'java.lang.Object.wait', library: 'Object.java', line: 405 }] }, + { name: 'worker', frames: [{ funcName: 'java.lang.Thread.run', library: 'Thread.java', line: 1572 }] }, + { name: 'main', frames: [{ funcName: 'not.the.fault.thread', library: 'Fake.java', line: 1 }] }, + ], +}; + +function createClient() { + return { + send: jest.fn().mockResolvedValue({ status: 'Ok' }), + breadcrumbs: { info: jest.fn() }, + }; +} + +function appStateListeners(): Array<(state: string) => void> { + return (AppState as unknown as { __listeners: Array<(state: string) => void> }).__listeners; +} + +describe('AnrWatchdogHandler', () => { + beforeEach(() => { + watchdogMock.start.mockClear(); + watchdogMock.stop.mockClear(); + appStateListeners().length = 0; + Platform.OS = 'android'; + }); + + it('does not create on platforms other than android', () => { + Platform.OS = 'ios'; + expect(AnrWatchdogHandler.create()).toBeUndefined(); + }); + + it('starts the native watchdog with the configured values', () => { + AnrWatchdogHandler.create()?.start(createClient() as never, 3000, true); + expect(watchdogMock.start).toHaveBeenCalledWith(3000, true); + }); + + it('sends a Hang report with the emitted frames and adds a breadcrumb', () => { + const client = createClient(); + AnrWatchdogHandler.create()?.start(client as never, 0, false); + + EmitterMock.emit(PAYLOAD); + + expect(client.breadcrumbs.info).toHaveBeenCalledWith('ANR detected - thread is blocked'); + const report = client.send.mock.calls[0][0]; + expect(report.attributes['error.type']).toBe('Hang'); + expect(report.stackTrace['main']).toEqual(PAYLOAD.frames); + }); + + it('adds the other threads without overwriting the fault thread, renaming duplicates', () => { + const client = createClient(); + AnrWatchdogHandler.create()?.start(client as never, 0, false); + + EmitterMock.emit(PAYLOAD); + + const report = client.send.mock.calls[0][0]; + expect(report.stackTrace['main']).toEqual(PAYLOAD.frames); + expect(report.stackTrace['worker']).toEqual(PAYLOAD.threads[0].frames); + expect(report.stackTrace['worker-2']).toEqual(PAYLOAD.threads[1].frames); + expect(report.stackTrace['main-2']).toEqual(PAYLOAD.threads[2].frames); + }); + + it('pauses detection in the background and resumes it in the foreground', () => { + AnrWatchdogHandler.create()?.start(createClient() as never, 3000, true); + const [onAppStateChange] = appStateListeners(); + + onAppStateChange('background'); + expect(watchdogMock.stop).toHaveBeenCalledTimes(1); + + onAppStateChange('active'); + expect(watchdogMock.start).toHaveBeenLastCalledWith(3000, true); + expect(watchdogMock.start).toHaveBeenCalledTimes(2); + }); + + it('stops the native watchdog on dispose', () => { + const handler = AnrWatchdogHandler.create(); + handler?.start(createClient() as never, 0, false); + handler?.dispose(); + expect(watchdogMock.stop).toHaveBeenCalled(); + }); +});