11
11
import io .sentry .ILogger ;
12
12
import io .sentry .IScopes ;
13
13
import io .sentry .ISentryExecutorService ;
14
+ import io .sentry .ISentryLifecycleToken ;
14
15
import io .sentry .NoOpScopes ;
15
16
import io .sentry .PerformanceCollectionData ;
16
17
import io .sentry .ProfileChunk ;
24
25
import io .sentry .android .core .internal .util .SentryFrameMetricsCollector ;
25
26
import io .sentry .protocol .SentryId ;
26
27
import io .sentry .transport .RateLimiter ;
28
+ import io .sentry .util .AutoClosableReentrantLock ;
27
29
import io .sentry .util .SentryRandom ;
28
30
import java .util .ArrayList ;
29
31
import java .util .List ;
@@ -57,10 +59,14 @@ public class AndroidContinuousProfiler
57
59
private @ NotNull SentryId chunkId = SentryId .EMPTY_ID ;
58
60
private final @ NotNull AtomicBoolean isClosed = new AtomicBoolean (false );
59
61
private @ NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate ();
60
- private boolean shouldSample = true ;
62
+ private volatile boolean shouldSample = true ;
63
+ private boolean shouldStop = false ;
61
64
private boolean isSampled = false ;
62
65
private int rootSpanCounter = 0 ;
63
66
67
+ private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
68
+ private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock ();
69
+
64
70
public AndroidContinuousProfiler (
65
71
final @ NotNull BuildInfoProvider buildInfoProvider ,
66
72
final @ NotNull SentryFrameMetricsCollector frameMetricsCollector ,
@@ -106,42 +112,46 @@ private void init() {
106
112
}
107
113
108
114
@ Override
109
- public synchronized void startProfiler (
115
+ public void startProfiler (
110
116
final @ NotNull ProfileLifecycle profileLifecycle ,
111
117
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
+ }
141
151
}
142
152
}
143
153
144
- private synchronized void start () {
154
+ private void start () {
145
155
if ((scopes == null || scopes == NoOpScopes .getInstance ())
146
156
&& Sentry .getCurrentScopes () != NoOpScopes .getInstance ()) {
147
157
this .scopes = Sentry .getCurrentScopes ();
@@ -213,103 +223,112 @@ private synchronized void start() {
213
223
SentryLevel .ERROR ,
214
224
"Failed to schedule profiling chunk finish. Did you call Sentry.close()?" ,
215
225
e );
226
+ shouldStop = true ;
216
227
}
217
228
}
218
229
219
230
@ 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
+ }
238
251
}
239
252
}
240
253
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
+ }
258
267
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
+ }
263
273
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
+ }
266
278
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
+ }
284
300
}
285
- }
286
301
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 ;
290
305
291
- if (scopes != null ) {
292
- sendChunks (scopes , scopes .getOptions ());
293
- }
306
+ if (scopes != null ) {
307
+ sendChunks (scopes , scopes .getOptions ());
308
+ }
294
309
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
+ }
302
318
}
303
319
}
304
320
305
- public synchronized void reevaluateSampling () {
321
+ public void reevaluateSampling () {
306
322
shouldSample = true ;
307
323
}
308
324
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
+ }
313
332
}
314
333
315
334
@ Override
@@ -328,7 +347,7 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti
328
347
return ;
329
348
}
330
349
final ArrayList <ProfileChunk > payloads = new ArrayList <>(payloadBuilders .size ());
331
- synchronized ( payloadBuilders ) {
350
+ try ( final @ NotNull ISentryLifecycleToken ignored = payloadLock . acquire () ) {
332
351
for (ProfileChunk .Builder builder : payloadBuilders ) {
333
352
payloads .add (builder .build (options ));
334
353
}
0 commit comments