Skip to content

Commit a18c69d

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 b9dc9a0 commit a18c69d

File tree

1 file changed

+73
-82
lines changed

1 file changed

+73
-82
lines changed

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,16 +18,13 @@
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.protocol.SentryId;
2524
import io.sentry.transport.RateLimiter;
2625
import io.sentry.util.AutoClosableReentrantLock;
2726
import io.sentry.util.LazyEvaluator;
2827
import io.sentry.util.SentryRandom;
29-
import java.util.ArrayList;
30-
import java.util.List;
3128
import java.util.concurrent.Future;
3229
import java.util.concurrent.RejectedExecutionException;
3330
import java.util.concurrent.atomic.AtomicBoolean;
@@ -41,8 +38,12 @@
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

Comments
 (0)