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 .protocol .SentryId ;
2524import io .sentry .transport .RateLimiter ;
2625import io .sentry .util .AutoClosableReentrantLock ;
2726import io .sentry .util .LazyEvaluator ;
2827import io .sentry .util .SentryRandom ;
29- import java .util .ArrayList ;
30- import java .util .List ;
3128import java .util .concurrent .Future ;
3229import java .util .concurrent .RejectedExecutionException ;
3330import java .util .concurrent .atomic .AtomicBoolean ;
4138 * Perfetto stack-sampling traces.
4239 *
4340 * <p>This class is intentionally separate from {@link AndroidContinuousProfiler} to keep the two
44- * profiling backends independent. All ProfilingManager API usage is confined to this file and
45- * {@link PerfettoProfiler}.
41+ * profiling backends independent. All ProfilingManager API usage is confined to this file and {@link
42+ * PerfettoProfiler}.
43+ *
44+ * <p>Unlike the legacy profiler, this class is not used for app-start profiling. It is created
45+ * during {@code Sentry.init()}, so scopes are always available when {@link #startProfiler} is
46+ * called.
4647 */
4748@ ApiStatus .Internal
4849@ RequiresApi (api = Build .VERSION_CODES .VANILLA_ICE_CREAM )
@@ -59,20 +60,19 @@ public class PerfettoContinuousProfiler
5960 private @ Nullable PerfettoProfiler perfettoProfiler = null ;
6061 private boolean isRunning = false ;
6162 private @ Nullable IScopes scopes ;
62- private @ Nullable Future <?> stopFuture ;
6363 private @ Nullable CompositePerformanceCollector performanceCollector ;
64- private final @ NotNull List < ProfileChunk . Builder > payloadBuilders = new ArrayList <>() ;
64+ private @ Nullable Future <?> stopFuture ;
6565 private @ NotNull SentryId profilerId = SentryId .EMPTY_ID ;
6666 private @ NotNull SentryId chunkId = SentryId .EMPTY_ID ;
6767 private final @ NotNull AtomicBoolean isClosed = new AtomicBoolean (false );
68- private @ NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate ();
68+ private @ NotNull SentryDate startProfileChunkTimestamp =
69+ new io .sentry .SentryNanotimeDate ();
6970 private volatile boolean shouldSample = true ;
7071 private boolean shouldStop = false ;
7172 private boolean isSampled = false ;
7273 private int activeTraceCount = 0 ;
7374
7475 private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
75- private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock ();
7676
7777 public PerfettoContinuousProfiler (
7878 final @ NotNull BuildInfoProvider buildInfoProvider ,
@@ -186,39 +186,58 @@ public boolean isRunning() {
186186 return isRunning ;
187187 }
188188
189+ /**
190+ * Resolves scopes on first call. Since PerfettoContinuousProfiler is created during
191+ * Sentry.init() and never used for app-start profiling, scopes is guaranteed to be available by
192+ * the time startProfiler is called.
193+ */
194+ private @ NotNull IScopes resolveScopes () {
195+ if (scopes != null && scopes != NoOpScopes .getInstance ()) {
196+ return scopes ;
197+ }
198+ final @ NotNull IScopes currentScopes = Sentry .getCurrentScopes ();
199+ if (currentScopes == NoOpScopes .getInstance ()) {
200+ logger .log (
201+ SentryLevel .ERROR ,
202+ "PerfettoContinuousProfiler: scopes not available. This is unexpected." );
203+ return currentScopes ;
204+ }
205+ this .scopes = currentScopes ;
206+ this .performanceCollector = currentScopes .getOptions ().getCompositePerformanceCollector ();
207+ final @ Nullable RateLimiter rateLimiter = currentScopes .getRateLimiter ();
208+ if (rateLimiter != null ) {
209+ rateLimiter .addRateLimitObserver (this );
210+ }
211+ return scopes ;
212+ }
213+
189214 /** Caller must hold {@link #lock}. */
190215 private void startInternal () {
191- tryResolveScopes ();
216+ final @ NotNull IScopes scopes = resolveScopes ();
192217 ensureProfiler ();
193218
194219 if (perfettoProfiler == null ) {
195220 return ;
196221 }
197222
198- if (scopes != null ) {
199- final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
200- if (rateLimiter != null
201- && (rateLimiter .isActiveForCategory (All )
202- || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
203- logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
204- // Let's stop and reset profiler id, as the profile is now broken anyway
205- stopInternal (false );
206- return ;
207- }
223+ final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
224+ if (rateLimiter != null
225+ && (rateLimiter .isActiveForCategory (All )
226+ || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
227+ logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
228+ stopInternal (false );
229+ return ;
230+ }
208231
209- // If device is offline, we don't start the profiler, to avoid flooding the cache
210- // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
211- if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
212- logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
213- // Let's stop and reset profiler id, as the profile is now broken anyway
214- stopInternal (false );
215- return ;
216- }
217- startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
218- } else {
219- startProfileChunkTimestamp = new SentryNanotimeDate ();
232+ // If device is offline, we don't start the profiler, to avoid flooding the cache
233+ if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
234+ logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
235+ stopInternal (false );
236+ return ;
220237 }
221238
239+ startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
240+
222241 final AndroidProfiler .ProfileStartData startData =
223242 perfettoProfiler .start (MAX_CHUNK_DURATION_MILLIS );
224243 if (startData == null ) {
@@ -264,19 +283,23 @@ private void startInternal() {
264283
265284 /** Caller must hold {@link #lock}. */
266285 private void stopInternal (final boolean restartProfiler ) {
267- tryResolveScopes ();
268286 if (stopFuture != null ) {
269287 stopFuture .cancel (false );
270288 }
271289 // check if profiler was created and it's running
272290 if (perfettoProfiler == null || !isRunning ) {
273- // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
274- // ids
275291 profilerId = SentryId .EMPTY_ID ;
276292 chunkId = SentryId .EMPTY_ID ;
277293 return ;
278294 }
279295
296+ final @ NotNull IScopes scopes = resolveScopes ();
297+ final @ NotNull SentryOptions options = scopes .getOptions ();
298+
299+ if (performanceCollector != null ) {
300+ performanceCollector .stop (chunkId .toString ());
301+ }
302+
280303 final AndroidProfiler .ProfileEndData endData = perfettoProfiler .endAndCollect ();
281304
282305 // check if profiler ended successfully
@@ -285,31 +308,22 @@ private void stopInternal(final boolean restartProfiler) {
285308 SentryLevel .ERROR ,
286309 "An error occurred while collecting a profile chunk, and it won't be sent." );
287310 } 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- final ProfileChunk .Builder builder =
293- new ProfileChunk .Builder (
294- profilerId ,
295- chunkId ,
296- endData .measurementsMap ,
297- endData .traceFile ,
298- startProfileChunkTimestamp ,
299- ProfileChunk .PLATFORM_ANDROID );
300- builder .setContentType ("perfetto" );
301- payloadBuilders .add (builder );
302- }
311+ final ProfileChunk .Builder builder =
312+ new ProfileChunk .Builder (
313+ profilerId ,
314+ chunkId ,
315+ endData .measurementsMap ,
316+ endData .traceFile ,
317+ startProfileChunkTimestamp ,
318+ ProfileChunk .PLATFORM_ANDROID );
319+ builder .setContentType ("perfetto" );
320+ sendChunk (builder , scopes , options );
303321 }
304322
305323 isRunning = false ;
306324 // A chunk is finished. Next chunk will have a different id.
307325 chunkId = SentryId .EMPTY_ID ;
308326
309- if (scopes != null ) {
310- sendChunks (scopes , scopes .getOptions ());
311- }
312-
313327 if (restartProfiler && !shouldStop ) {
314328 logger .log (SentryLevel .DEBUG , "Profile chunk finished. Starting a new one." );
315329 startInternal ();
@@ -320,22 +334,6 @@ private void stopInternal(final boolean restartProfiler) {
320334 }
321335 }
322336
323- private void tryResolveScopes () {
324- if ((scopes == null || scopes == NoOpScopes .getInstance ())
325- && Sentry .getCurrentScopes () != NoOpScopes .getInstance ()) {
326- onScopesAvailable (Sentry .getCurrentScopes ());
327- }
328- }
329-
330- private void onScopesAvailable (final @ NotNull IScopes resolvedScopes ) {
331- this .scopes = resolvedScopes ;
332- this .performanceCollector = resolvedScopes .getOptions ().getCompositePerformanceCollector ();
333- final @ Nullable RateLimiter rateLimiter = resolvedScopes .getRateLimiter ();
334- if (rateLimiter != null ) {
335- rateLimiter .addRateLimitObserver (this );
336- }
337- }
338-
339337 private void ensureProfiler () {
340338 if (perfettoProfiler == null ) {
341339 logger .log (
@@ -350,29 +348,22 @@ public void reevaluateSampling() {
350348 shouldSample = true ;
351349 }
352350
353- private void sendChunks (final @ NotNull IScopes scopes , final @ NotNull SentryOptions options ) {
351+ private void sendChunk (
352+ final @ NotNull ProfileChunk .Builder builder ,
353+ final @ NotNull IScopes scopes ,
354+ final @ NotNull SentryOptions options ) {
354355 try {
355356 options
356357 .getExecutorService ()
357358 .submit (
358359 () -> {
359- // SDK is closed, we don't send the chunks
360360 if (isClosed .get ()) {
361361 return ;
362362 }
363- final ArrayList <ProfileChunk > payloads = new ArrayList <>(payloadBuilders .size ());
364- try (final @ NotNull ISentryLifecycleToken ignored = payloadLock .acquire ()) {
365- for (ProfileChunk .Builder builder : payloadBuilders ) {
366- payloads .add (builder .build (options ));
367- }
368- payloadBuilders .clear ();
369- }
370- for (ProfileChunk payload : payloads ) {
371- scopes .captureProfileChunk (payload );
372- }
363+ scopes .captureProfileChunk (builder .build (options ));
373364 });
374365 } catch (Throwable e ) {
375- options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunks ." , e );
366+ options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunk ." , e );
376367 }
377368 }
378369
0 commit comments