Skip to content

Commit 7bbe257

Browse files
43jayclaude
andcommitted
ref(profiling): Remove app-start profiling logic from PerfettoContinuousProfiler
Currently PerfettoContinuousProfiler is not doing app-start profiling. Because of this, scopes are always available. Remove the legacy patterns that were carried over from AndroidContinuousProfiler: - Replace tryResolveScopes/onScopesAvailable with resolveScopes() that returns @NotNull IScopes and logs an error if scopes is unexpectedly unavailable - Remove payloadBuilders list, payloadLock, and sendChunks() buffering; replace with sendChunk() that sends a single chunk immediately - Remove scopes != null guards and SentryNanotimeDate fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3b1d8fd commit 7bbe257

1 file changed

Lines changed: 73 additions & 82 deletions

File tree

sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java

Lines changed: 73 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import io.sentry.Sentry;
1919
import io.sentry.SentryDate;
2020
import io.sentry.SentryLevel;
21-
import io.sentry.SentryNanotimeDate;
2221
import io.sentry.SentryOptions;
2322
import io.sentry.TracesSampler;
2423
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
@@ -27,8 +26,6 @@
2726
import io.sentry.util.AutoClosableReentrantLock;
2827
import io.sentry.util.LazyEvaluator;
2928
import io.sentry.util.SentryRandom;
30-
import java.util.ArrayList;
31-
import java.util.List;
3229
import java.util.concurrent.Future;
3330
import java.util.concurrent.RejectedExecutionException;
3431
import java.util.concurrent.atomic.AtomicBoolean;
@@ -42,8 +39,12 @@
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

Comments
 (0)