1818import io .sentry .Sentry ;
1919import io .sentry .SentryDate ;
2020import io .sentry .SentryLevel ;
21- import io .sentry .SentryNanotimeDate ;
2221import io .sentry .SentryOptions ;
2322import io .sentry .TracesSampler ;
2423import io .sentry .android .core .internal .util .SentryFrameMetricsCollector ;
2726import io .sentry .util .AutoClosableReentrantLock ;
2827import io .sentry .util .LazyEvaluator ;
2928import io .sentry .util .SentryRandom ;
30- import java .util .ArrayList ;
31- import java .util .List ;
3229import java .util .concurrent .Future ;
3330import java .util .concurrent .RejectedExecutionException ;
3431import java .util .concurrent .atomic .AtomicBoolean ;
4239 * Perfetto stack-sampling traces.
4340 *
4441 * <p>This class is intentionally separate from {@link AndroidContinuousProfiler} to keep the two
45- * profiling backends independent. All ProfilingManager API usage is confined to this file and
46- * {@link PerfettoProfiler}.
42+ * profiling backends independent. All ProfilingManager API usage is confined to this file and {@link
43+ * PerfettoProfiler}.
44+ *
45+ * <p>Unlike the legacy profiler, this class is not used for app-start profiling. It is created
46+ * during {@code Sentry.init()}, so scopes are always available when {@link #startProfiler} is
47+ * called.
4748 */
4849@ ApiStatus .Internal
4950@ RequiresApi (api = Build .VERSION_CODES .VANILLA_ICE_CREAM )
@@ -62,20 +63,19 @@ public class PerfettoContinuousProfiler
6263 private @ Nullable PerfettoProfiler perfettoProfiler = null ;
6364 private boolean isRunning = false ;
6465 private @ Nullable IScopes scopes ;
65- private @ Nullable Future <?> stopFuture ;
6666 private @ Nullable CompositePerformanceCollector performanceCollector ;
67- private final @ NotNull List < ProfileChunk . Builder > payloadBuilders = new ArrayList <>() ;
67+ private @ Nullable Future <?> stopFuture ;
6868 private @ NotNull SentryId profilerId = SentryId .EMPTY_ID ;
6969 private @ NotNull SentryId chunkId = SentryId .EMPTY_ID ;
7070 private final @ NotNull AtomicBoolean isClosed = new AtomicBoolean (false );
71- private @ NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate ();
71+ private @ NotNull SentryDate startProfileChunkTimestamp =
72+ new io .sentry .SentryNanotimeDate ();
7273 private volatile boolean shouldSample = true ;
7374 private boolean shouldStop = false ;
7475 private boolean isSampled = false ;
7576 private int activeTraceCount = 0 ;
7677
7778 private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
78- private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock ();
7979
8080 public PerfettoContinuousProfiler (
8181 final @ NotNull android .content .Context context ,
@@ -190,39 +190,58 @@ public boolean isRunning() {
190190 return isRunning ;
191191 }
192192
193+ /**
194+ * Resolves scopes on first call. Since PerfettoContinuousProfiler is created during
195+ * Sentry.init() and never used for app-start profiling, scopes is guaranteed to be available by
196+ * the time startProfiler is called.
197+ */
198+ private @ NotNull IScopes resolveScopes () {
199+ if (scopes != null && scopes != NoOpScopes .getInstance ()) {
200+ return scopes ;
201+ }
202+ final @ NotNull IScopes currentScopes = Sentry .getCurrentScopes ();
203+ if (currentScopes == NoOpScopes .getInstance ()) {
204+ logger .log (
205+ SentryLevel .ERROR ,
206+ "PerfettoContinuousProfiler: scopes not available. This is unexpected." );
207+ return currentScopes ;
208+ }
209+ this .scopes = currentScopes ;
210+ this .performanceCollector = currentScopes .getOptions ().getCompositePerformanceCollector ();
211+ final @ Nullable RateLimiter rateLimiter = currentScopes .getRateLimiter ();
212+ if (rateLimiter != null ) {
213+ rateLimiter .addRateLimitObserver (this );
214+ }
215+ return scopes ;
216+ }
217+
193218 /** Caller must hold {@link #lock}. */
194219 private void startInternal () {
195- tryResolveScopes ();
220+ final @ NotNull IScopes scopes = resolveScopes ();
196221 ensureProfiler ();
197222
198223 if (perfettoProfiler == null ) {
199224 return ;
200225 }
201226
202- if (scopes != null ) {
203- final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
204- if (rateLimiter != null
205- && (rateLimiter .isActiveForCategory (All )
206- || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
207- logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
208- // Let's stop and reset profiler id, as the profile is now broken anyway
209- stopInternal (false );
210- return ;
211- }
227+ final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
228+ if (rateLimiter != null
229+ && (rateLimiter .isActiveForCategory (All )
230+ || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
231+ logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
232+ stopInternal (false );
233+ return ;
234+ }
212235
213- // If device is offline, we don't start the profiler, to avoid flooding the cache
214- // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
215- if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
216- logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
217- // Let's stop and reset profiler id, as the profile is now broken anyway
218- stopInternal (false );
219- return ;
220- }
221- startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
222- } else {
223- startProfileChunkTimestamp = new SentryNanotimeDate ();
236+ // If device is offline, we don't start the profiler, to avoid flooding the cache
237+ if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
238+ logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
239+ stopInternal (false );
240+ return ;
224241 }
225242
243+ startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
244+
226245 final AndroidProfiler .ProfileStartData startData =
227246 perfettoProfiler .start (MAX_CHUNK_DURATION_MILLIS );
228247 // check if profiling started
@@ -266,19 +285,23 @@ private void startInternal() {
266285
267286 /** Caller must hold {@link #lock}. */
268287 private void stopInternal (final boolean restartProfiler ) {
269- tryResolveScopes ();
270288 if (stopFuture != null ) {
271289 stopFuture .cancel (false );
272290 }
273291 // check if profiler was created and it's running
274292 if (perfettoProfiler == null || !isRunning ) {
275- // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
276- // ids
277293 profilerId = SentryId .EMPTY_ID ;
278294 chunkId = SentryId .EMPTY_ID ;
279295 return ;
280296 }
281297
298+ final @ NotNull IScopes scopes = resolveScopes ();
299+ final @ NotNull SentryOptions options = scopes .getOptions ();
300+
301+ if (performanceCollector != null ) {
302+ performanceCollector .stop (chunkId .toString ());
303+ }
304+
282305 final AndroidProfiler .ProfileEndData endData = perfettoProfiler .endAndCollect ();
283306
284307 // check if profiler ended successfully
@@ -287,31 +310,22 @@ private void stopInternal(final boolean restartProfiler) {
287310 SentryLevel .ERROR ,
288311 "An error occurred while collecting a profile chunk, and it won't be sent." );
289312 } else {
290- // The scopes can be null if the profiler is started before the SDK is initialized (app
291- // start profiling), meaning there's no scopes to send the chunks. In that case, we store
292- // the data in a list and send it when the next chunk is finished.
293- try (final @ NotNull ISentryLifecycleToken ignored2 = payloadLock .acquire ()) {
294- final ProfileChunk .Builder builder =
295- new ProfileChunk .Builder (
296- profilerId ,
297- chunkId ,
298- endData .measurementsMap ,
299- endData .traceFile ,
300- startProfileChunkTimestamp ,
301- ProfileChunk .PLATFORM_ANDROID );
302- builder .setContentType ("perfetto" );
303- payloadBuilders .add (builder );
304- }
313+ final ProfileChunk .Builder builder =
314+ new ProfileChunk .Builder (
315+ profilerId ,
316+ chunkId ,
317+ endData .measurementsMap ,
318+ endData .traceFile ,
319+ startProfileChunkTimestamp ,
320+ ProfileChunk .PLATFORM_ANDROID );
321+ builder .setContentType ("perfetto" );
322+ sendChunk (builder , scopes , options );
305323 }
306324
307325 isRunning = false ;
308326 // A chunk is finished. Next chunk will have a different id.
309327 chunkId = SentryId .EMPTY_ID ;
310328
311- if (scopes != null ) {
312- sendChunks (scopes , scopes .getOptions ());
313- }
314-
315329 if (restartProfiler && !shouldStop ) {
316330 logger .log (SentryLevel .DEBUG , "Profile chunk finished. Starting a new one." );
317331 startInternal ();
@@ -322,22 +336,6 @@ private void stopInternal(final boolean restartProfiler) {
322336 }
323337 }
324338
325- private void tryResolveScopes () {
326- if ((scopes == null || scopes == NoOpScopes .getInstance ())
327- && Sentry .getCurrentScopes () != NoOpScopes .getInstance ()) {
328- onScopesAvailable (Sentry .getCurrentScopes ());
329- }
330- }
331-
332- private void onScopesAvailable (final @ NotNull IScopes resolvedScopes ) {
333- this .scopes = resolvedScopes ;
334- this .performanceCollector = resolvedScopes .getOptions ().getCompositePerformanceCollector ();
335- final @ Nullable RateLimiter rateLimiter = resolvedScopes .getRateLimiter ();
336- if (rateLimiter != null ) {
337- rateLimiter .addRateLimitObserver (this );
338- }
339- }
340-
341339 private void ensureProfiler () {
342340 logger .log (
343341 SentryLevel .DEBUG ,
@@ -355,29 +353,22 @@ public void reevaluateSampling() {
355353 shouldSample = true ;
356354 }
357355
358- private void sendChunks (final @ NotNull IScopes scopes , final @ NotNull SentryOptions options ) {
356+ private void sendChunk (
357+ final @ NotNull ProfileChunk .Builder builder ,
358+ final @ NotNull IScopes scopes ,
359+ final @ NotNull SentryOptions options ) {
359360 try {
360361 options
361362 .getExecutorService ()
362363 .submit (
363364 () -> {
364- // SDK is closed, we don't send the chunks
365365 if (isClosed .get ()) {
366366 return ;
367367 }
368- final ArrayList <ProfileChunk > payloads = new ArrayList <>(payloadBuilders .size ());
369- try (final @ NotNull ISentryLifecycleToken ignored = payloadLock .acquire ()) {
370- for (ProfileChunk .Builder builder : payloadBuilders ) {
371- payloads .add (builder .build (options ));
372- }
373- payloadBuilders .clear ();
374- }
375- for (ProfileChunk payload : payloads ) {
376- scopes .captureProfileChunk (payload );
377- }
368+ scopes .captureProfileChunk (builder .build (options ));
378369 });
379370 } catch (Throwable e ) {
380- options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunks ." , e );
371+ options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunk ." , e );
381372 }
382373 }
383374
0 commit comments