Skip to content

Commit 022a6b2

Browse files
43jayclaude
andcommitted
feat(profiling): Add PerfettoProfiler and wire into AndroidContinuousProfiler
Introduces PerfettoProfiler, which uses Android's ProfilingManager system service (API 35+) for Perfetto-based stack sampling. When useProfilingManager is enabled, AndroidContinuousProfiler selects PerfettoProfiler at init time via createWithProfilingManager(); on older devices no profiling data is collected and the legacy Debug-based profiler is not used as a fallback. Key changes: - PerfettoProfiler: calls requestProfiling(STACK_SAMPLING), waits for ProfilingResult via CountDownLatch, reads .pftrace via getResultFilePath() - AndroidContinuousProfiler: factory methods createLegacy() / createWithProfilingManager() replace the public constructor; init() split into initLegacy() / initProfilingManager() for clarity; stopFuture uses cancel(false) to avoid interrupting the Perfetto result wait - AndroidOptionsInitializer: branches on isUseProfilingManager() to select the correct factory method - SentryEnvelopeItem: fromPerfettoProfileChunk() builds a single envelope item with meta_length header separating JSON metadata from binary .pftrace - SentryEnvelopeItemHeader: adds metaLength field for the binary format - ProfileChunk: adds contentType and version fields; Builder.setContentType() - SentryClient: routes Perfetto chunks to fromPerfettoProfileChunk() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 788d91a commit 022a6b2

File tree

11 files changed

+701
-27
lines changed

11 files changed

+701
-27
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
4141
}
4242

4343
public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
44-
public fun <init> (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/util/LazyEvaluator$Evaluator;)V
4544
public fun close (Z)V
45+
public static fun createLegacy (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/util/LazyEvaluator$Evaluator;)Lio/sentry/android/core/AndroidContinuousProfiler;
46+
public static fun createWithProfilingManager (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Lio/sentry/util/LazyEvaluator$Evaluator;)Lio/sentry/android/core/AndroidContinuousProfiler;
4647
public fun getChunkId ()Lio/sentry/protocol/SentryId;
4748
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
4849
public fun getRootSpanCounter ()I
@@ -338,6 +339,12 @@ public final class io/sentry/android/core/NetworkBreadcrumbsIntegration : io/sen
338339
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
339340
}
340341

342+
public class io/sentry/android/core/PerfettoProfiler {
343+
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;)V
344+
public fun endAndCollect ()Lio/sentry/android/core/AndroidProfiler$ProfileEndData;
345+
public fun start (J)Lio/sentry/android/core/AndroidProfiler$ProfileStartData;
346+
}
347+
341348
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor {
342349
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;Z)V
343350
public fun getOrder ()Ljava/lang/Long;

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

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED;
55
import static java.util.concurrent.TimeUnit.SECONDS;
66

7+
import android.annotation.SuppressLint;
8+
import android.content.Context;
79
import android.os.Build;
810
import io.sentry.CompositePerformanceCollector;
911
import io.sentry.DataCategory;
@@ -51,6 +53,10 @@ public class AndroidContinuousProfiler
5153
private boolean isInitialized = false;
5254
private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
5355
private @Nullable AndroidProfiler profiler = null;
56+
private @Nullable PerfettoProfiler perfettoProfiler = null;
57+
private final boolean useProfilingManager;
58+
private final @Nullable Context context;
59+
private boolean isPerfettoActive = false;
5460
private boolean isRunning = false;
5561
private @Nullable IScopes scopes;
5662
private @Nullable Future<?> stopFuture;
@@ -68,27 +74,91 @@ public class AndroidContinuousProfiler
6874
private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
6975
private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock();
7076

71-
public AndroidContinuousProfiler(
77+
/**
78+
* Creates a profiler using the legacy Debug.startMethodTracingSampling engine. This is the
79+
* default path used for app-start profiling and devices below API 35.
80+
*/
81+
public static AndroidContinuousProfiler createLegacy(
7282
final @NotNull BuildInfoProvider buildInfoProvider,
7383
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
7484
final @NotNull ILogger logger,
7585
final @Nullable String profilingTracesDirPath,
7686
final int profilingTracesHz,
7787
final @NotNull LazyEvaluator.Evaluator<ISentryExecutorService> executorServiceSupplier) {
88+
return new AndroidContinuousProfiler(
89+
buildInfoProvider,
90+
frameMetricsCollector,
91+
executorServiceSupplier,
92+
logger,
93+
null,
94+
false,
95+
profilingTracesHz,
96+
profilingTracesDirPath);
97+
}
98+
99+
/**
100+
* Creates a profiler using Android's ProfilingManager (Perfetto) on API 35+. On older devices, no
101+
* profiling data is collected — the legacy Debug-based profiler is not used as a fallback.
102+
*/
103+
public static AndroidContinuousProfiler createWithProfilingManager(
104+
final @NotNull Context context,
105+
final @NotNull BuildInfoProvider buildInfoProvider,
106+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
107+
final @NotNull ILogger logger,
108+
final @NotNull LazyEvaluator.Evaluator<ISentryExecutorService> executorServiceSupplier) {
109+
return new AndroidContinuousProfiler(
110+
buildInfoProvider,
111+
frameMetricsCollector,
112+
executorServiceSupplier,
113+
logger,
114+
context,
115+
true,
116+
0,
117+
null);
118+
}
119+
120+
private AndroidContinuousProfiler(
121+
final @NotNull BuildInfoProvider buildInfoProvider,
122+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
123+
final @NotNull LazyEvaluator.Evaluator<ISentryExecutorService> executorServiceSupplier,
124+
final @NotNull ILogger logger,
125+
final @Nullable Context context,
126+
final boolean useProfilingManager,
127+
final int profilingTracesHz,
128+
final @Nullable String profilingTracesDirPath) {
78129
this.logger = logger;
79130
this.frameMetricsCollector = frameMetricsCollector;
80131
this.buildInfoProvider = buildInfoProvider;
81132
this.profilingTracesDirPath = profilingTracesDirPath;
82133
this.profilingTracesHz = profilingTracesHz;
83134
this.executorServiceSupplier = executorServiceSupplier;
135+
this.useProfilingManager = useProfilingManager;
136+
this.context = context;
84137
}
85138

139+
@SuppressLint("NewApi")
86140
private void init() {
141+
logger.log(
142+
SentryLevel.DEBUG,
143+
"AndroidContinuousProfiler.init() isInitialized=%s, useProfilingManager=%s, apiLevel=%d",
144+
isInitialized,
145+
useProfilingManager,
146+
buildInfoProvider.getSdkInfoVersion());
147+
87148
// We initialize it only once
88149
if (isInitialized) {
89150
return;
90151
}
91152
isInitialized = true;
153+
154+
if (useProfilingManager) {
155+
initProfilingManager();
156+
} else {
157+
initLegacy();
158+
}
159+
}
160+
161+
private void initLegacy() {
92162
if (profilingTracesDirPath == null) {
93163
logger.log(
94164
SentryLevel.WARNING,
@@ -112,6 +182,20 @@ private void init() {
112182
logger);
113183
}
114184

185+
@SuppressLint("NewApi")
186+
private void initProfilingManager() {
187+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.VANILLA_ICE_CREAM
188+
&& context != null) {
189+
perfettoProfiler = new PerfettoProfiler(context, frameMetricsCollector, logger);
190+
logger.log(SentryLevel.DEBUG, "Using Perfetto profiler (ProfilingManager).");
191+
} else {
192+
logger.log(
193+
SentryLevel.WARNING,
194+
"useProfilingManager requested but not available (requires API 35+). "
195+
+ "No profiling data will be collected.");
196+
}
197+
}
198+
115199
@Override
116200
public void startProfiler(
117201
final @NotNull ProfileLifecycle profileLifecycle,
@@ -175,7 +259,7 @@ private void start() {
175259
// Let's initialize trace folder and profiling interval
176260
init();
177261
// init() didn't create profiler, should never happen
178-
if (profiler == null) {
262+
if (profiler == null && perfettoProfiler == null) {
179263
return;
180264
}
181265

@@ -202,7 +286,14 @@ private void start() {
202286
} else {
203287
startProfileChunkTimestamp = new SentryNanotimeDate();
204288
}
205-
final AndroidProfiler.ProfileStartData startData = profiler.start();
289+
final AndroidProfiler.ProfileStartData startData;
290+
if (perfettoProfiler != null) {
291+
startData = perfettoProfiler.start(MAX_CHUNK_DURATION_MILLIS);
292+
isPerfettoActive = startData != null;
293+
} else {
294+
startData = profiler.start();
295+
isPerfettoActive = false;
296+
}
206297
// check if profiling started
207298
if (startData == null) {
208299
return;
@@ -262,10 +353,10 @@ private void stop(final boolean restartProfiler) {
262353
initScopes();
263354
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
264355
if (stopFuture != null) {
265-
stopFuture.cancel(true);
356+
stopFuture.cancel(false);
266357
}
267358
// check if profiler was created and it's running
268-
if (profiler == null || !isRunning) {
359+
if ((profiler == null && perfettoProfiler == null) || !isRunning) {
269360
// When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
270361
// ids
271362
profilerId = SentryId.EMPTY_ID;
@@ -284,8 +375,18 @@ private void stop(final boolean restartProfiler) {
284375
performanceCollectionData = performanceCollector.stop(chunkId.toString());
285376
}
286377

287-
final AndroidProfiler.ProfileEndData endData =
288-
profiler.endAndCollect(false, performanceCollectionData);
378+
final AndroidProfiler.ProfileEndData endData;
379+
final String platform;
380+
if (isPerfettoActive && perfettoProfiler != null) {
381+
endData = perfettoProfiler.endAndCollect();
382+
platform = ProfileChunk.PLATFORM_ANDROID;
383+
} else if (profiler != null) {
384+
endData = profiler.endAndCollect(false, performanceCollectionData);
385+
platform = ProfileChunk.PLATFORM_ANDROID;
386+
} else {
387+
endData = null;
388+
platform = ProfileChunk.PLATFORM_ANDROID;
389+
}
289390

290391
// check if profiler end successfully
291392
if (endData == null) {
@@ -297,14 +398,18 @@ private void stop(final boolean restartProfiler) {
297398
// start profiling), meaning there's no scopes to send the chunks. In that case, we store
298399
// the data in a list and send it when the next chunk is finished.
299400
try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) {
300-
payloadBuilders.add(
401+
final ProfileChunk.Builder builder =
301402
new ProfileChunk.Builder(
302403
profilerId,
303404
chunkId,
304405
endData.measurementsMap,
305406
endData.traceFile,
306407
startProfileChunkTimestamp,
307-
ProfileChunk.PLATFORM_ANDROID));
408+
platform);
409+
if (isPerfettoActive) {
410+
builder.setContentType("perfetto");
411+
}
412+
payloadBuilders.add(builder);
308413
}
309414
}
310415

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -335,16 +335,24 @@ private static void setupProfiler(
335335
performanceCollector.start(chunkId.toString());
336336
}
337337
} else {
338+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector =
339+
Objects.requireNonNull(
340+
options.getFrameMetricsCollector(), "options.getFrameMetricsCollector is required");
338341
options.setContinuousProfiler(
339-
new AndroidContinuousProfiler(
340-
buildInfoProvider,
341-
Objects.requireNonNull(
342-
options.getFrameMetricsCollector(),
343-
"options.getFrameMetricsCollector is required"),
344-
options.getLogger(),
345-
options.getProfilingTracesDirPath(),
346-
options.getProfilingTracesHz(),
347-
() -> options.getExecutorService()));
342+
options.isUseProfilingManager()
343+
? AndroidContinuousProfiler.createWithProfilingManager(
344+
context,
345+
buildInfoProvider,
346+
frameMetricsCollector,
347+
options.getLogger(),
348+
() -> options.getExecutorService())
349+
: AndroidContinuousProfiler.createLegacy(
350+
buildInfoProvider,
351+
frameMetricsCollector,
352+
options.getLogger(),
353+
options.getProfilingTracesDirPath(),
354+
options.getProfilingTracesHz(),
355+
() -> options.getExecutorService()));
348356
}
349357
}
350358
}

0 commit comments

Comments
 (0)