analyzeFrames.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import styled from '@emotion/styled';
  2. import {
  3. getMappedThreadState,
  4. ThreadStates,
  5. } from 'sentry/components/events/interfaces/threads/threadSelector/threadStates';
  6. import {getCurrentThread} from 'sentry/components/events/interfaces/utils';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import {t, tct} from 'sentry/locale';
  9. import type {EntryException, Event, Frame, Lock, Thread} from 'sentry/types';
  10. import {EntryType} from 'sentry/types';
  11. import {defined} from 'sentry/utils';
  12. type SuspectFrame = {
  13. module: string | RegExp;
  14. resources: React.ReactNode;
  15. exceptionMessage?: string;
  16. functions?: (string | RegExp)[];
  17. offendingThreadStates?: ThreadStates[];
  18. };
  19. const CULPRIT_FRAMES: SuspectFrame[] = [
  20. {
  21. module: 'libcore.io.Linux',
  22. functions: [
  23. 'read',
  24. 'write',
  25. 'fstat',
  26. 'fsync',
  27. 'fdatasync',
  28. 'access',
  29. 'open',
  30. 'chmod',
  31. ],
  32. offendingThreadStates: [
  33. ThreadStates.WAITING,
  34. ThreadStates.TIMED_WAITING,
  35. ThreadStates.RUNNABLE,
  36. ],
  37. resources: t(
  38. '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.'
  39. ),
  40. },
  41. {
  42. module: 'android.database.sqlite.SQLiteConnection',
  43. functions: [
  44. 'nativeOpen',
  45. 'nativeExecute',
  46. /nativeExecuteFor[a-zA-Z]+/,
  47. /nativeBind[a-zA-Z]+/,
  48. /nativeGet[a-zA-Z]+/,
  49. 'nativePrepareStatement',
  50. ],
  51. offendingThreadStates: [
  52. ThreadStates.WAITING,
  53. ThreadStates.TIMED_WAITING,
  54. ThreadStates.RUNNABLE,
  55. ],
  56. resources: t(
  57. '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.'
  58. ),
  59. },
  60. {
  61. module: 'android.app.SharedPreferencesImpl$EditorImpl',
  62. functions: ['commit'],
  63. offendingThreadStates: [
  64. ThreadStates.WAITING,
  65. ThreadStates.TIMED_WAITING,
  66. ThreadStates.RUNNABLE,
  67. ],
  68. resources: t(
  69. "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."
  70. ),
  71. },
  72. {
  73. module: /^android\.app\.SharedPreferencesImpl\$EditorImpl\$[0-9]/,
  74. functions: ['run'],
  75. offendingThreadStates: [
  76. ThreadStates.WAITING,
  77. ThreadStates.TIMED_WAITING,
  78. ThreadStates.RUNNABLE,
  79. ],
  80. resources: t(
  81. '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.'
  82. ),
  83. },
  84. {
  85. module: 'android.app.Instrumentation',
  86. functions: ['callApplicationOnCreate'],
  87. offendingThreadStates: [
  88. ThreadStates.WAITING,
  89. ThreadStates.TIMED_WAITING,
  90. ThreadStates.RUNNABLE,
  91. ],
  92. resources: tct(
  93. '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.',
  94. {
  95. link: (
  96. <ExternalLink href="https://developer.android.com/topic/performance/vitals/launch-time#heavy-app" />
  97. ),
  98. }
  99. ),
  100. },
  101. {
  102. module: 'android.content.res.AssetManager',
  103. functions: [
  104. 'nativeOpenAsset',
  105. 'nativeOpenAssetFd',
  106. 'nativeOpenNonAsset',
  107. 'nativeOpenNonAssetFd',
  108. ],
  109. offendingThreadStates: [
  110. ThreadStates.WAITING,
  111. ThreadStates.TIMED_WAITING,
  112. ThreadStates.RUNNABLE,
  113. ],
  114. resources: t(
  115. '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.'
  116. ),
  117. },
  118. {
  119. module: 'android.content.res.AssetManager',
  120. functions: [/^nativeGetResource[a-zA-Z]+/],
  121. offendingThreadStates: [
  122. ThreadStates.WAITING,
  123. ThreadStates.TIMED_WAITING,
  124. ThreadStates.RUNNABLE,
  125. ],
  126. resources: t(
  127. "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."
  128. ),
  129. },
  130. {
  131. module: 'android.view.LayoutInflater',
  132. functions: ['inflate'],
  133. offendingThreadStates: [
  134. ThreadStates.WAITING,
  135. ThreadStates.TIMED_WAITING,
  136. ThreadStates.RUNNABLE,
  137. ],
  138. resources: tct(
  139. '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.',
  140. {
  141. link: (
  142. <ExternalLink href="https://developer.android.com/develop/ui/views/layout/improving-layouts" />
  143. ),
  144. }
  145. ),
  146. },
  147. ];
  148. function satisfiesModuleCondition(frame: Frame, suspect: SuspectFrame) {
  149. if (suspect.module === null || suspect.module === undefined) {
  150. return true;
  151. }
  152. const matchFuction = suspect.module;
  153. return typeof matchFuction === 'string'
  154. ? frame.module?.startsWith(matchFuction)
  155. : frame.module && matchFuction.test(frame.module);
  156. }
  157. function satisfiesFunctionCondition(frame: Frame, suspect: SuspectFrame) {
  158. if (
  159. suspect.functions === undefined ||
  160. suspect.functions === null ||
  161. suspect.functions.length === 0
  162. ) {
  163. return true;
  164. }
  165. if (frame.function === null || frame.function === undefined) {
  166. return false;
  167. }
  168. for (let index = 0; index < suspect.functions.length; index++) {
  169. const matchFuction = suspect.functions[index];
  170. const match =
  171. typeof matchFuction === 'string'
  172. ? frame.function === matchFuction
  173. : matchFuction.test(frame.function);
  174. if (match) {
  175. return true;
  176. }
  177. }
  178. return false;
  179. }
  180. function satisfiesOffendingThreadCondition(
  181. threadState: string | undefined | null,
  182. offendingThreadStates?: ThreadStates[]
  183. ) {
  184. if (offendingThreadStates === undefined || offendingThreadStates.length === 0) {
  185. return true;
  186. }
  187. const mappedState = getMappedThreadState(threadState);
  188. if (mappedState === undefined) {
  189. return false;
  190. }
  191. return offendingThreadStates.includes(mappedState);
  192. }
  193. export function analyzeFramesForRootCause(event: Event): {
  194. culprit: string | Lock;
  195. resources: React.ReactNode;
  196. } | null {
  197. const exception = event.entries.find(entry => entry.type === EntryType.EXCEPTION) as
  198. | EntryException
  199. | undefined;
  200. if (exception === undefined) {
  201. return null;
  202. }
  203. const exceptionFrames = exception.data.values?.[0]?.stacktrace?.frames;
  204. if (exceptionFrames === undefined) {
  205. return null;
  206. }
  207. const currentThread = getCurrentThread(event);
  208. // iterating the frames in reverse order, because the topmost frames most like the root cause
  209. for (let index = exceptionFrames.length - 1; index >= 0; index--) {
  210. const frame = exceptionFrames[index];
  211. const rootCause = analyzeFrameForRootCause(frame, currentThread);
  212. if (defined(rootCause)) {
  213. return rootCause;
  214. }
  215. }
  216. return null;
  217. }
  218. function lockRootCauseCulprit(lock: Lock): {
  219. culprit: string | Lock;
  220. resources: React.ReactNode;
  221. } {
  222. const address = lock.address;
  223. const obj = `${lock.package_name}.${lock.class_name}`;
  224. const tid = lock.thread_id;
  225. return {
  226. culprit: lock,
  227. resources: tct(
  228. 'The main thread is blocked/waiting, trying to acquire lock [address] ([obj]) [heldByThread]',
  229. {
  230. address: <Bold>{address}</Bold>,
  231. obj: <Bold>{obj}</Bold>,
  232. heldByThread: tid ? 'held by the suspect frame of this thread.' : '.',
  233. }
  234. ),
  235. };
  236. }
  237. export function analyzeFrameForRootCause(
  238. frame: Frame,
  239. currentThread?: Thread,
  240. lockAddress?: string
  241. ): {
  242. culprit: string | Lock;
  243. resources: React.ReactNode;
  244. } | null {
  245. if (defined(lockAddress) && frame.lock?.address === lockAddress) {
  246. // if we are provided with a lockAddress, we just have to analyze if the frame's lock
  247. // address is equal to the one provided to mark the frame as suspect
  248. return lockRootCauseCulprit(frame.lock);
  249. }
  250. if (
  251. defined(frame.lock) &&
  252. currentThread?.current &&
  253. satisfiesOffendingThreadCondition(currentThread?.state, [
  254. ThreadStates.WAITING,
  255. ThreadStates.TIMED_WAITING,
  256. ThreadStates.BLOCKED,
  257. ])
  258. ) {
  259. // if the current (main) thread contains a lock and not in a RUNNABLE state, we return early
  260. // with the lock being the culprit
  261. return lockRootCauseCulprit(frame.lock);
  262. }
  263. // otherwise, we analyze for common patterns
  264. for (const possibleCulprit of CULPRIT_FRAMES) {
  265. if (
  266. satisfiesModuleCondition(frame, possibleCulprit) &&
  267. satisfiesFunctionCondition(frame, possibleCulprit) &&
  268. satisfiesOffendingThreadCondition(
  269. currentThread?.state,
  270. possibleCulprit.offendingThreadStates
  271. )
  272. ) {
  273. return {
  274. culprit:
  275. typeof possibleCulprit.module === 'string'
  276. ? possibleCulprit.module
  277. : possibleCulprit.module.toString(),
  278. resources: possibleCulprit.resources,
  279. };
  280. }
  281. }
  282. return null;
  283. }
  284. const Bold = styled('span')`
  285. font-weight: bold;
  286. `;