123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- 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: (
- <ExternalLink href="https://developer.android.com/topic/performance/vitals/launch-time#heavy-app" />
- ),
- }
- ),
- },
- {
- 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: (
- <ExternalLink href="https://developer.android.com/develop/ui/views/layout/improving-layouts" />
- ),
- }
- ),
- },
- ];
- 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: <Bold>{address}</Bold>,
- obj: <Bold>{obj}</Bold>,
- 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;
- `;
|