import styled from '@emotion/styled';
import {
getMappedThreadState,
ThreadStates,
} from 'sentry/components/events/interfaces/threads/threadSelector/threadStates';
import {getCurrentThread} from 'sentry/components/events/interfaces/utils';
import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import type {EntryException, Event, Frame, Lock, Thread} from 'sentry/types';
import {EntryType} from 'sentry/types';
import {defined} from 'sentry/utils';
type SuspectFrame = {
module: string | RegExp;
resources: React.ReactNode;
exceptionMessage?: string;
functions?: (string | RegExp)[];
offendingThreadStates?: ThreadStates[];
};
const CULPRIT_FRAMES: SuspectFrame[] = [
{
module: 'libcore.io.Linux',
functions: [
'read',
'write',
'fstat',
'fsync',
'fdatasync',
'access',
'open',
'chmod',
],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
'File I/O operations, such as reading from or writing to files on disk, can be time-consuming, especially if the file size is large or the storage medium is slow. Move File I/O off the main thread to avoid this ANR.'
),
},
{
module: 'android.database.sqlite.SQLiteConnection',
functions: [
'nativeOpen',
'nativeExecute',
/nativeExecuteFor[a-zA-Z]+/,
/nativeBind[a-zA-Z]+/,
/nativeGet[a-zA-Z]+/,
'nativePrepareStatement',
],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
'Database operations, such as querying, inserting, updating, or deleting data, can involve disk I/O, processing, and potentially long-running operations. Move database operations off the main thread to avoid this ANR.'
),
},
{
module: 'android.app.SharedPreferencesImpl$EditorImpl',
functions: ['commit'],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
"If you have a particularly large or complex SharedPreferences file or if you're performing multiple simultaneous commits in quick succession, this can lead to ANR. Switch to SharedPreferences.apply or move commit to a background thread to avoid this ANR."
),
},
{
module: /^android\.app\.SharedPreferencesImpl\$EditorImpl\$[0-9]/,
functions: ['run'],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
'SharedPreferences.apply will save data on background thread only if it happens before the activity/service finishes. Switch to SharedPreferences.commit and move commit to a background thread.'
),
},
{
module: 'android.app.Instrumentation',
functions: ['callApplicationOnCreate'],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: tct(
'The app is initializing too many things on the main thread during app launch. To avoid this ANR, optimize cold/warm app starts by offloading operations off the main thread and [link:lazily initializing] components.',
{
link: (
),
}
),
},
{
module: 'android.content.res.AssetManager',
functions: [
'nativeOpenAsset',
'nativeOpenAssetFd',
'nativeOpenNonAsset',
'nativeOpenNonAssetFd',
],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
'If the AssetManager operation involves reading or loading a large asset file on the main thread, this can lead to ANR. Move loading heavy assets off the main thread to avoid this ANR.'
),
},
{
module: 'android.content.res.AssetManager',
functions: [/^nativeGetResource[a-zA-Z]+/],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: t(
"If you're reading a particularly large raw file (for example, a video file) on the main thread, this can lead to ANR. Look for heavy resources in the '/res' or '/res/raw; folders to avoid this ANR."
),
},
{
module: 'android.view.LayoutInflater',
functions: ['inflate'],
offendingThreadStates: [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.RUNNABLE,
],
resources: tct(
'The app is potentially inflating a heavy, deeply-nested layout. [link:Optimize view hierarchy], use view stubs, use include/merge tags for reusing inflated views to avoid this ANR.',
{
link: (
),
}
),
},
];
function satisfiesModuleCondition(frame: Frame, suspect: SuspectFrame) {
if (suspect.module === null || suspect.module === undefined) {
return true;
}
const matchFuction = suspect.module;
return typeof matchFuction === 'string'
? frame.module?.startsWith(matchFuction)
: frame.module && matchFuction.test(frame.module);
}
function satisfiesFunctionCondition(frame: Frame, suspect: SuspectFrame) {
if (
suspect.functions === undefined ||
suspect.functions === null ||
suspect.functions.length === 0
) {
return true;
}
if (frame.function === null || frame.function === undefined) {
return false;
}
for (let index = 0; index < suspect.functions.length; index++) {
const matchFuction = suspect.functions[index];
const match =
typeof matchFuction === 'string'
? frame.function === matchFuction
: matchFuction.test(frame.function);
if (match) {
return true;
}
}
return false;
}
function satisfiesOffendingThreadCondition(
threadState: string | undefined | null,
offendingThreadStates?: ThreadStates[]
) {
if (offendingThreadStates === undefined || offendingThreadStates.length === 0) {
return true;
}
const mappedState = getMappedThreadState(threadState);
if (mappedState === undefined) {
return false;
}
return offendingThreadStates.includes(mappedState);
}
export function analyzeFramesForRootCause(event: Event): {
culprit: string | Lock;
resources: React.ReactNode;
} | null {
const exception = event.entries.find(entry => entry.type === EntryType.EXCEPTION) as
| EntryException
| undefined;
if (exception === undefined) {
return null;
}
const exceptionFrames = exception.data.values?.[0]?.stacktrace?.frames;
if (exceptionFrames === undefined) {
return null;
}
const currentThread = getCurrentThread(event);
// iterating the frames in reverse order, because the topmost frames most like the root cause
for (let index = exceptionFrames.length - 1; index >= 0; index--) {
const frame = exceptionFrames[index];
const rootCause = analyzeFrameForRootCause(frame, currentThread);
if (defined(rootCause)) {
return rootCause;
}
}
return null;
}
function lockRootCauseCulprit(lock: Lock): {
culprit: string | Lock;
resources: React.ReactNode;
} {
const address = lock.address;
const obj = `${lock.package_name}.${lock.class_name}`;
const tid = lock.thread_id;
return {
culprit: lock,
resources: tct(
'The main thread is blocked/waiting, trying to acquire lock [address] ([obj]) [heldByThread]',
{
address: {address},
obj: {obj},
heldByThread: tid ? 'held by the suspect frame of this thread.' : '.',
}
),
};
}
export function analyzeFrameForRootCause(
frame: Frame,
currentThread?: Thread,
lockAddress?: string
): {
culprit: string | Lock;
resources: React.ReactNode;
} | null {
if (defined(lockAddress) && frame.lock?.address === lockAddress) {
// if we are provided with a lockAddress, we just have to analyze if the frame's lock
// address is equal to the one provided to mark the frame as suspect
return lockRootCauseCulprit(frame.lock);
}
if (
defined(frame.lock) &&
currentThread?.current &&
satisfiesOffendingThreadCondition(currentThread?.state, [
ThreadStates.WAITING,
ThreadStates.TIMED_WAITING,
ThreadStates.BLOCKED,
])
) {
// if the current (main) thread contains a lock and not in a RUNNABLE state, we return early
// with the lock being the culprit
return lockRootCauseCulprit(frame.lock);
}
// otherwise, we analyze for common patterns
for (const possibleCulprit of CULPRIT_FRAMES) {
if (
satisfiesModuleCondition(frame, possibleCulprit) &&
satisfiesFunctionCondition(frame, possibleCulprit) &&
satisfiesOffendingThreadCondition(
currentThread?.state,
possibleCulprit.offendingThreadStates
)
) {
return {
culprit:
typeof possibleCulprit.module === 'string'
? possibleCulprit.module
: possibleCulprit.module.toString(),
resources: possibleCulprit.resources,
};
}
}
return null;
}
const Bold = styled('span')`
font-weight: bold;
`;