1111import io .sentry .ILogger ;
1212import io .sentry .IScopes ;
1313import io .sentry .ISentryExecutorService ;
14+ import io .sentry .ISentryLifecycleToken ;
1415import io .sentry .NoOpScopes ;
1516import io .sentry .PerformanceCollectionData ;
1617import io .sentry .ProfileChunk ;
2425import io .sentry .android .core .internal .util .SentryFrameMetricsCollector ;
2526import io .sentry .protocol .SentryId ;
2627import io .sentry .transport .RateLimiter ;
28+ import io .sentry .util .AutoClosableReentrantLock ;
2729import io .sentry .util .SentryRandom ;
2830import java .util .ArrayList ;
2931import java .util .List ;
@@ -57,10 +59,14 @@ public class AndroidContinuousProfiler
5759 private @ NotNull SentryId chunkId = SentryId .EMPTY_ID ;
5860 private final @ NotNull AtomicBoolean isClosed = new AtomicBoolean (false );
5961 private @ NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate ();
60- private boolean shouldSample = true ;
62+ private volatile boolean shouldSample = true ;
63+ private boolean shouldStop = false ;
6164 private boolean isSampled = false ;
6265 private int rootSpanCounter = 0 ;
6366
67+ private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
68+ private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock ();
69+
6470 public AndroidContinuousProfiler (
6571 final @ NotNull BuildInfoProvider buildInfoProvider ,
6672 final @ NotNull SentryFrameMetricsCollector frameMetricsCollector ,
@@ -106,42 +112,46 @@ private void init() {
106112 }
107113
108114 @ Override
109- public synchronized void startProfiler (
115+ public void startProfiler (
110116 final @ NotNull ProfileLifecycle profileLifecycle ,
111117 final @ NotNull TracesSampler tracesSampler ) {
112- if (shouldSample ) {
113- isSampled = tracesSampler .sampleSessionProfile (SentryRandom .current ().nextDouble ());
114- shouldSample = false ;
115- }
116- if (!isSampled ) {
117- logger .log (SentryLevel .DEBUG , "Profiler was not started due to sampling decision." );
118- return ;
119- }
120- switch (profileLifecycle ) {
121- case TRACE :
122- // rootSpanCounter should never be negative, unless the user changed profile lifecycle while
123- // the profiler is running or close() is called. This is just a safety check.
124- if (rootSpanCounter < 0 ) {
125- rootSpanCounter = 0 ;
126- }
127- rootSpanCounter ++;
128- break ;
129- case MANUAL :
130- // We check if the profiler is already running and log a message only in manual mode, since
131- // in trace mode we can have multiple concurrent traces
132- if (isRunning ()) {
133- logger .log (SentryLevel .DEBUG , "Profiler is already running." );
134- return ;
135- }
136- break ;
137- }
138- if (!isRunning ()) {
139- logger .log (SentryLevel .DEBUG , "Started Profiler." );
140- start ();
118+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
119+ if (shouldSample ) {
120+ isSampled = tracesSampler .sampleSessionProfile (SentryRandom .current ().nextDouble ());
121+ shouldSample = false ;
122+ }
123+ if (!isSampled ) {
124+ logger .log (SentryLevel .DEBUG , "Profiler was not started due to sampling decision." );
125+ return ;
126+ }
127+ switch (profileLifecycle ) {
128+ case TRACE :
129+ // rootSpanCounter should never be negative, unless the user changed profile lifecycle
130+ // while
131+ // the profiler is running or close() is called. This is just a safety check.
132+ if (rootSpanCounter < 0 ) {
133+ rootSpanCounter = 0 ;
134+ }
135+ rootSpanCounter ++;
136+ break ;
137+ case MANUAL :
138+ // We check if the profiler is already running and log a message only in manual mode,
139+ // since
140+ // in trace mode we can have multiple concurrent traces
141+ if (isRunning ()) {
142+ logger .log (SentryLevel .DEBUG , "Profiler is already running." );
143+ return ;
144+ }
145+ break ;
146+ }
147+ if (!isRunning ()) {
148+ logger .log (SentryLevel .DEBUG , "Started Profiler." );
149+ start ();
150+ }
141151 }
142152 }
143153
144- private synchronized void start () {
154+ private void start () {
145155 if ((scopes == null || scopes == NoOpScopes .getInstance ())
146156 && Sentry .getCurrentScopes () != NoOpScopes .getInstance ()) {
147157 this .scopes = Sentry .getCurrentScopes ();
@@ -213,103 +223,112 @@ private synchronized void start() {
213223 SentryLevel .ERROR ,
214224 "Failed to schedule profiling chunk finish. Did you call Sentry.close()?" ,
215225 e );
226+ shouldStop = true ;
216227 }
217228 }
218229
219230 @ Override
220- public synchronized void stopProfiler (final @ NotNull ProfileLifecycle profileLifecycle ) {
221- switch (profileLifecycle ) {
222- case TRACE :
223- rootSpanCounter --;
224- // If there are active spans, and profile lifecycle is trace, we don't stop the profiler
225- if (rootSpanCounter > 0 ) {
226- return ;
227- }
228- // rootSpanCounter should never be negative, unless the user changed profile lifecycle while
229- // the profiler is running or close() is called. This is just a safety check.
230- if (rootSpanCounter < 0 ) {
231- rootSpanCounter = 0 ;
232- }
233- stop (false );
234- break ;
235- case MANUAL :
236- stop (false );
237- break ;
231+ public void stopProfiler (final @ NotNull ProfileLifecycle profileLifecycle ) {
232+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
233+ switch (profileLifecycle ) {
234+ case TRACE :
235+ rootSpanCounter --;
236+ // If there are active spans, and profile lifecycle is trace, we don't stop the profiler
237+ if (rootSpanCounter > 0 ) {
238+ return ;
239+ }
240+ // rootSpanCounter should never be negative, unless the user changed profile lifecycle
241+ // while the profiler is running or close() is called. This is just a safety check.
242+ if (rootSpanCounter < 0 ) {
243+ rootSpanCounter = 0 ;
244+ }
245+ shouldStop = true ;
246+ break ;
247+ case MANUAL :
248+ shouldStop = true ;
249+ break ;
250+ }
238251 }
239252 }
240253
241- private synchronized void stop (final boolean restartProfiler ) {
242- if (stopFuture != null ) {
243- stopFuture .cancel (true );
244- }
245- // check if profiler was created and it's running
246- if (profiler == null || !isRunning ) {
247- // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the ids
248- profilerId = SentryId .EMPTY_ID ;
249- chunkId = SentryId .EMPTY_ID ;
250- return ;
251- }
252-
253- // onTransactionStart() is only available since Lollipop_MR1
254- // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
255- if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) {
256- return ;
257- }
254+ private void stop (final boolean restartProfiler ) {
255+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
256+ if (stopFuture != null ) {
257+ stopFuture .cancel (true );
258+ }
259+ // check if profiler was created and it's running
260+ if (profiler == null || !isRunning ) {
261+ // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
262+ // ids
263+ profilerId = SentryId .EMPTY_ID ;
264+ chunkId = SentryId .EMPTY_ID ;
265+ return ;
266+ }
258267
259- List <PerformanceCollectionData > performanceCollectionData = null ;
260- if (performanceCollector != null ) {
261- performanceCollectionData = performanceCollector .stop (chunkId .toString ());
262- }
268+ // onTransactionStart() is only available since Lollipop_MR1
269+ // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
270+ if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) {
271+ return ;
272+ }
263273
264- final AndroidProfiler .ProfileEndData endData =
265- profiler .endAndCollect (false , performanceCollectionData );
274+ List <PerformanceCollectionData > performanceCollectionData = null ;
275+ if (performanceCollector != null ) {
276+ performanceCollectionData = performanceCollector .stop (chunkId .toString ());
277+ }
266278
267- // check if profiler end successfully
268- if (endData == null ) {
269- logger .log (
270- SentryLevel .ERROR ,
271- "An error occurred while collecting a profile chunk, and it won't be sent." );
272- } else {
273- // The scopes can be null if the profiler is started before the SDK is initialized (app start
274- // profiling), meaning there's no scopes to send the chunks. In that case, we store the data
275- // in a list and send it when the next chunk is finished.
276- synchronized (payloadBuilders ) {
277- payloadBuilders .add (
278- new ProfileChunk .Builder (
279- profilerId ,
280- chunkId ,
281- endData .measurementsMap ,
282- endData .traceFile ,
283- startProfileChunkTimestamp ));
279+ final AndroidProfiler .ProfileEndData endData =
280+ profiler .endAndCollect (false , performanceCollectionData );
281+
282+ // check if profiler end successfully
283+ if (endData == null ) {
284+ logger .log (
285+ SentryLevel .ERROR ,
286+ "An error occurred while collecting a profile chunk, and it won't be sent." );
287+ } else {
288+ // The scopes can be null if the profiler is started before the SDK is initialized (app
289+ // start profiling), meaning there's no scopes to send the chunks. In that case, we store
290+ // the data in a list and send it when the next chunk is finished.
291+ try (final @ NotNull ISentryLifecycleToken ignored2 = payloadLock .acquire ()) {
292+ payloadBuilders .add (
293+ new ProfileChunk .Builder (
294+ profilerId ,
295+ chunkId ,
296+ endData .measurementsMap ,
297+ endData .traceFile ,
298+ startProfileChunkTimestamp ));
299+ }
284300 }
285- }
286301
287- isRunning = false ;
288- // A chunk is finished. Next chunk will have a different id.
289- chunkId = SentryId .EMPTY_ID ;
302+ isRunning = false ;
303+ // A chunk is finished. Next chunk will have a different id.
304+ chunkId = SentryId .EMPTY_ID ;
290305
291- if (scopes != null ) {
292- sendChunks (scopes , scopes .getOptions ());
293- }
306+ if (scopes != null ) {
307+ sendChunks (scopes , scopes .getOptions ());
308+ }
294309
295- if (restartProfiler ) {
296- logger .log (SentryLevel .DEBUG , "Profile chunk finished. Starting a new one." );
297- start ();
298- } else {
299- // When the profiler is stopped manually, we have to reset its id
300- profilerId = SentryId .EMPTY_ID ;
301- logger .log (SentryLevel .DEBUG , "Profile chunk finished." );
310+ if (restartProfiler && !shouldStop ) {
311+ logger .log (SentryLevel .DEBUG , "Profile chunk finished. Starting a new one." );
312+ start ();
313+ } else {
314+ // When the profiler is stopped manually, we have to reset its id
315+ profilerId = SentryId .EMPTY_ID ;
316+ logger .log (SentryLevel .DEBUG , "Profile chunk finished." );
317+ }
302318 }
303319 }
304320
305- public synchronized void reevaluateSampling () {
321+ public void reevaluateSampling () {
306322 shouldSample = true ;
307323 }
308324
309- public synchronized void close () {
310- rootSpanCounter = 0 ;
311- stop (false );
312- isClosed .set (true );
325+ public void close () {
326+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
327+ rootSpanCounter = 0 ;
328+ shouldStop = true ;
329+ stop (false );
330+ isClosed .set (true );
331+ }
313332 }
314333
315334 @ Override
@@ -328,7 +347,7 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti
328347 return ;
329348 }
330349 final ArrayList <ProfileChunk > payloads = new ArrayList <>(payloadBuilders .size ());
331- synchronized ( payloadBuilders ) {
350+ try ( final @ NotNull ISentryLifecycleToken ignored = payloadLock . acquire () ) {
332351 for (ProfileChunk .Builder builder : payloadBuilders ) {
333352 payloads .add (builder .build (options ));
334353 }
0 commit comments