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 ,
@@ -185,39 +185,58 @@ public boolean isRunning() {
185185 return isRunning ;
186186 }
187187
188+ /**
189+ * Resolves scopes on first call. Since PerfettoContinuousProfiler is created during
190+ * Sentry.init() and never used for app-start profiling, scopes is guaranteed to be available by
191+ * the time startProfiler is called.
192+ */
193+ private @ NotNull IScopes resolveScopes () {
194+ if (scopes != null && scopes != NoOpScopes .getInstance ()) {
195+ return scopes ;
196+ }
197+ final @ NotNull IScopes currentScopes = Sentry .getCurrentScopes ();
198+ if (currentScopes == NoOpScopes .getInstance ()) {
199+ logger .log (
200+ SentryLevel .ERROR ,
201+ "PerfettoContinuousProfiler: scopes not available. This is unexpected." );
202+ return currentScopes ;
203+ }
204+ this .scopes = currentScopes ;
205+ this .performanceCollector = currentScopes .getOptions ().getCompositePerformanceCollector ();
206+ final @ Nullable RateLimiter rateLimiter = currentScopes .getRateLimiter ();
207+ if (rateLimiter != null ) {
208+ rateLimiter .addRateLimitObserver (this );
209+ }
210+ return scopes ;
211+ }
212+
188213 /** Caller must hold {@link #lock}. */
189214 private void startInternal () {
190- tryResolveScopes ();
215+ final @ NotNull IScopes scopes = resolveScopes ();
191216 ensureProfiler ();
192217
193218 if (perfettoProfiler == null ) {
194219 return ;
195220 }
196221
197- if (scopes != null ) {
198- final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
199- if (rateLimiter != null
200- && (rateLimiter .isActiveForCategory (All )
201- || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
202- logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
203- // Let's stop and reset profiler id, as the profile is now broken anyway
204- stopInternal (false );
205- return ;
206- }
222+ final @ Nullable RateLimiter rateLimiter = scopes .getRateLimiter ();
223+ if (rateLimiter != null
224+ && (rateLimiter .isActiveForCategory (All )
225+ || rateLimiter .isActiveForCategory (DataCategory .ProfileChunkUi ))) {
226+ logger .log (SentryLevel .WARNING , "SDK is rate limited. Stopping profiler." );
227+ stopInternal (false );
228+ return ;
229+ }
207230
208- // If device is offline, we don't start the profiler, to avoid flooding the cache
209- // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
210- if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
211- logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
212- // Let's stop and reset profiler id, as the profile is now broken anyway
213- stopInternal (false );
214- return ;
215- }
216- startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
217- } else {
218- startProfileChunkTimestamp = new SentryNanotimeDate ();
231+ // If device is offline, we don't start the profiler, to avoid flooding the cache
232+ if (scopes .getOptions ().getConnectionStatusProvider ().getConnectionStatus () == DISCONNECTED ) {
233+ logger .log (SentryLevel .WARNING , "Device is offline. Stopping profiler." );
234+ stopInternal (false );
235+ return ;
219236 }
220237
238+ startProfileChunkTimestamp = scopes .getOptions ().getDateProvider ().now ();
239+
221240 final AndroidProfiler .ProfileStartData startData =
222241 perfettoProfiler .start (MAX_CHUNK_DURATION_MILLIS );
223242 if (startData == null ) {
@@ -263,19 +282,23 @@ private void startInternal() {
263282
264283 /** Caller must hold {@link #lock}. */
265284 private void stopInternal (final boolean restartProfiler ) {
266- tryResolveScopes ();
267285 if (stopFuture != null ) {
268286 stopFuture .cancel (false );
269287 }
270288 // check if profiler was created and it's running
271289 if (perfettoProfiler == null || !isRunning ) {
272- // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
273- // ids
274290 profilerId = SentryId .EMPTY_ID ;
275291 chunkId = SentryId .EMPTY_ID ;
276292 return ;
277293 }
278294
295+ final @ NotNull IScopes scopes = resolveScopes ();
296+ final @ NotNull SentryOptions options = scopes .getOptions ();
297+
298+ if (performanceCollector != null ) {
299+ performanceCollector .stop (chunkId .toString ());
300+ }
301+
279302 final AndroidProfiler .ProfileEndData endData = perfettoProfiler .endAndCollect ();
280303
281304 // check if profiler ended successfully
@@ -284,31 +307,22 @@ private void stopInternal(final boolean restartProfiler) {
284307 SentryLevel .ERROR ,
285308 "An error occurred while collecting a profile chunk, and it won't be sent." );
286309 } else {
287- // The scopes can be null if the profiler is started before the SDK is initialized (app
288- // start profiling), meaning there's no scopes to send the chunks. In that case, we store
289- // the data in a list and send it when the next chunk is finished.
290- try (final @ NotNull ISentryLifecycleToken ignored2 = payloadLock .acquire ()) {
291- final ProfileChunk .Builder builder =
292- new ProfileChunk .Builder (
293- profilerId ,
294- chunkId ,
295- endData .measurementsMap ,
296- endData .traceFile ,
297- startProfileChunkTimestamp ,
298- ProfileChunk .PLATFORM_ANDROID );
299- builder .setContentType ("perfetto" );
300- payloadBuilders .add (builder );
301- }
310+ final ProfileChunk .Builder builder =
311+ new ProfileChunk .Builder (
312+ profilerId ,
313+ chunkId ,
314+ endData .measurementsMap ,
315+ endData .traceFile ,
316+ startProfileChunkTimestamp ,
317+ ProfileChunk .PLATFORM_ANDROID );
318+ builder .setContentType ("perfetto" );
319+ sendChunk (builder , scopes , options );
302320 }
303321
304322 isRunning = false ;
305323 // A chunk is finished. Next chunk will have a different id.
306324 chunkId = SentryId .EMPTY_ID ;
307325
308- if (scopes != null ) {
309- sendChunks (scopes , scopes .getOptions ());
310- }
311-
312326 if (restartProfiler && !shouldStop ) {
313327 logger .log (SentryLevel .DEBUG , "Profile chunk finished. Starting a new one." );
314328 startInternal ();
@@ -319,22 +333,6 @@ private void stopInternal(final boolean restartProfiler) {
319333 }
320334 }
321335
322- private void tryResolveScopes () {
323- if ((scopes == null || scopes == NoOpScopes .getInstance ())
324- && Sentry .getCurrentScopes () != NoOpScopes .getInstance ()) {
325- onScopesAvailable (Sentry .getCurrentScopes ());
326- }
327- }
328-
329- private void onScopesAvailable (final @ NotNull IScopes resolvedScopes ) {
330- this .scopes = resolvedScopes ;
331- this .performanceCollector = resolvedScopes .getOptions ().getCompositePerformanceCollector ();
332- final @ Nullable RateLimiter rateLimiter = resolvedScopes .getRateLimiter ();
333- if (rateLimiter != null ) {
334- rateLimiter .addRateLimitObserver (this );
335- }
336- }
337-
338336 private void ensureProfiler () {
339337 if (perfettoProfiler == null ) {
340338 logger .log (
@@ -349,29 +347,22 @@ public void reevaluateSampling() {
349347 shouldSample = true ;
350348 }
351349
352- private void sendChunks (final @ NotNull IScopes scopes , final @ NotNull SentryOptions options ) {
350+ private void sendChunk (
351+ final @ NotNull ProfileChunk .Builder builder ,
352+ final @ NotNull IScopes scopes ,
353+ final @ NotNull SentryOptions options ) {
353354 try {
354355 options
355356 .getExecutorService ()
356357 .submit (
357358 () -> {
358- // SDK is closed, we don't send the chunks
359359 if (isClosed .get ()) {
360360 return ;
361361 }
362- final ArrayList <ProfileChunk > payloads = new ArrayList <>(payloadBuilders .size ());
363- try (final @ NotNull ISentryLifecycleToken ignored = payloadLock .acquire ()) {
364- for (ProfileChunk .Builder builder : payloadBuilders ) {
365- payloads .add (builder .build (options ));
366- }
367- payloadBuilders .clear ();
368- }
369- for (ProfileChunk payload : payloads ) {
370- scopes .captureProfileChunk (payload );
371- }
362+ scopes .captureProfileChunk (builder .build (options ));
372363 });
373364 } catch (Throwable e ) {
374- options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunks ." , e );
365+ options .getLogger ().log (SentryLevel .DEBUG , "Failed to send profile chunk ." , e );
375366 }
376367 }
377368
0 commit comments