2020import java .io .File ;
2121import java .io .IOException ;
2222import java .util .concurrent .atomic .AtomicBoolean ;
23+ import java .util .concurrent .atomic .AtomicInteger ;
2324import org .jetbrains .annotations .ApiStatus ;
2425import org .jetbrains .annotations .NotNull ;
2526import org .jetbrains .annotations .Nullable ;
@@ -31,6 +32,7 @@ public class AnrProfilingIntegration
3132 public static final long POLLING_INTERVAL_MS = 66 ;
3233 private static final long THRESHOLD_SUSPICION_MS = 1000 ;
3334 public static final long THRESHOLD_ANR_MS = 4000 ;
35+ static final int MAX_NUM_STACKS = (int ) (10_000 / POLLING_INTERVAL_MS );
3436
3537 private final AtomicBoolean enabled = new AtomicBoolean (true );
3638 private final Runnable updater = () -> lastMainThreadExecutionTime = SystemClock .uptimeMillis ();
@@ -39,6 +41,7 @@ public class AnrProfilingIntegration
3941 new AutoClosableReentrantLock ();
4042
4143 private volatile long lastMainThreadExecutionTime = SystemClock .uptimeMillis ();
44+ final AtomicInteger numCollectedStacks = new AtomicInteger ();
4245 private volatile MainThreadState mainThreadState = MainThreadState .IDLE ;
4346 private volatile @ Nullable AnrProfileManager profileManager ;
4447 private volatile @ NotNull ILogger logger = NoOpLogger .getInstance ();
@@ -54,6 +57,11 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
5457 "SentryAndroidOptions is required" );
5558 this .logger = options .getLogger ();
5659
60+ if (options .getCacheDirPath () == null ) {
61+ logger .log (SentryLevel .WARNING , "ANR Profiling is enabled but cacheDirPath is not set" );
62+ return ;
63+ }
64+
5765 if (((SentryAndroidOptions ) options ).isEnableAnrProfiling ()) {
5866 addIntegrationToSdkVersion ("AnrProfiling" );
5967 AppState .getInstance ().addAppStateListener (this );
@@ -71,6 +79,7 @@ public void close() throws IOException {
7179 if (p != null ) {
7280 p .close ();
7381 }
82+ profileManager = null ;
7483 }
7584 }
7685
@@ -138,7 +147,7 @@ public void run() {
138147 }
139148 }
140149 } catch (Throwable t ) {
141- logger .log (SentryLevel .WARNING , "Failed execute AnrStacktraceIntegration" , t );
150+ logger .log (SentryLevel .WARNING , "Failed to execute AnrStacktraceIntegration" , t );
142151 }
143152 }
144153
@@ -152,29 +161,40 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
152161 }
153162
154163 if (mainThreadState == MainThreadState .IDLE && diff > THRESHOLD_SUSPICION_MS ) {
155- logger .log (SentryLevel .DEBUG , "ANR: main thread is suspicious" );
164+ if (logger .isEnabled (SentryLevel .DEBUG )) {
165+ logger .log (SentryLevel .DEBUG , "ANR: main thread is suspicious" );
166+ }
156167 mainThreadState = MainThreadState .SUSPICIOUS ;
157168 clearStacks ();
158169 }
159170
160171 // if we are suspicious, we need to collect stack traces
161172 if (mainThreadState == MainThreadState .SUSPICIOUS
162173 || mainThreadState == MainThreadState .ANR_DETECTED ) {
163- final long start = SystemClock .uptimeMillis ();
164- final @ NotNull AnrStackTrace trace =
165- new AnrStackTrace (System .currentTimeMillis (), mainThread .getStackTrace ());
166- final long duration = SystemClock .uptimeMillis () - start ;
167- logger .log (
168- SentryLevel .DEBUG ,
169- "AnrWatchdog: capturing main thread stacktrace took " + duration + "ms" );
170-
171- addStackTrace (trace );
174+ if (numCollectedStacks .get () < MAX_NUM_STACKS ) {
175+ final long start = SystemClock .uptimeMillis ();
176+ final @ NotNull AnrStackTrace trace =
177+ new AnrStackTrace (System .currentTimeMillis (), mainThread .getStackTrace ());
178+ final long duration = SystemClock .uptimeMillis () - start ;
179+ if (logger .isEnabled (SentryLevel .DEBUG )) {
180+ logger .log (
181+ SentryLevel .DEBUG ,
182+ "AnrWatchdog: capturing main thread stacktrace took " + duration + "ms" );
183+ }
184+ addStackTrace (trace );
185+ } else {
186+ if (logger .isEnabled (SentryLevel .DEBUG )) {
187+ logger .log (
188+ SentryLevel .DEBUG ,
189+ "ANR: reached maximum number of collected stack traces, skipping further collection" );
190+ }
191+ }
172192 }
173193
174- // TODO is this still required,
175- // maybe add stop condition
176194 if (mainThreadState == MainThreadState .SUSPICIOUS && diff > THRESHOLD_ANR_MS ) {
177- logger .log (SentryLevel .DEBUG , "ANR: main thread ANR threshold reached" );
195+ if (logger .isEnabled (SentryLevel .DEBUG )) {
196+ logger .log (SentryLevel .DEBUG , "ANR: main thread ANR threshold reached" );
197+ }
178198 mainThreadState = MainThreadState .ANR_DETECTED ;
179199 }
180200 }
@@ -192,8 +212,12 @@ protected AnrProfileManager getProfileManager() {
192212 if (profileManager == null ) {
193213 final @ NotNull SentryOptions opts =
194214 Objects .requireNonNull (options , "Options can't be null" );
215+ final @ Nullable String cacheDirPath = opts .getCacheDirPath ();
216+ if (cacheDirPath == null ) {
217+ throw new IllegalStateException ("cacheDirPath is required for ANR profiling" );
218+ }
195219 final @ NotNull File currentFile =
196- AnrProfileRotationHelper .getFileForRecording (new File (opts . getCacheDirPath () ));
220+ AnrProfileRotationHelper .getFileForRecording (new File (cacheDirPath ));
197221 profileManager = new AnrProfileManager (opts , currentFile );
198222 }
199223
@@ -202,10 +226,12 @@ protected AnrProfileManager getProfileManager() {
202226 }
203227
204228 private void clearStacks () throws IOException {
229+ numCollectedStacks .set (0 );
205230 getProfileManager ().clear ();
206231 }
207232
208233 private void addStackTrace (@ NotNull final AnrStackTrace trace ) throws IOException {
234+ numCollectedStacks .incrementAndGet ();
209235 getProfileManager ().add (trace );
210236 }
211237
0 commit comments