Skip to content

Commit 331622b

Browse files
committed
ref(profiling): Move frame metrics collection from PerfettoProfiler to PerfettoContinuousProfiler
This separation keeps PerfettoProfiler focused on the ProfilingManager API and puts all measurement collection (frame metrics + performanceCollector) at the PerfettoContinuousProfiler layer, making both classes easier to reason about independently.
1 parent dcee449 commit 331622b

File tree

5 files changed

+134
-124
lines changed

5 files changed

+134
-124
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public final class io/sentry/android/core/NetworkBreadcrumbsIntegration : io/sen
339339
}
340340

341341
public class io/sentry/android/core/PerfettoContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
342-
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Lio/sentry/util/LazyEvaluator$Evaluator;)V
342+
public fun <init> (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/ILogger;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/util/LazyEvaluator$Evaluator;Lio/sentry/util/LazyEvaluator$Evaluator;)V
343343
public fun close (Z)V
344344
public fun getActiveTraceCount ()I
345345
public fun getChunkId ()Lio/sentry/protocol/SentryId;
@@ -352,9 +352,9 @@ public class io/sentry/android/core/PerfettoContinuousProfiler : io/sentry/ICont
352352
}
353353

354354
public class io/sentry/android/core/PerfettoProfiler {
355-
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;)V
356-
public fun endAndCollect ()Lio/sentry/android/core/AndroidProfiler$ProfileEndData;
357-
public fun start (J)Lio/sentry/android/core/AndroidProfiler$ProfileStartData;
355+
public fun <init> (Landroid/content/Context;Lio/sentry/ILogger;)V
356+
public fun endAndCollect ()Ljava/io/File;
357+
public fun start (J)Z
358358
}
359359

360360
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,11 @@ private static void setupProfiler(
345345
? new PerfettoContinuousProfiler(
346346
buildInfoProvider,
347347
options.getLogger(),
348+
frameMetricsCollector,
348349
() -> options.getExecutorService(),
349350
() ->
350351
new PerfettoProfiler(
351-
context.getApplicationContext(), frameMetricsCollector, options.getLogger()))
352+
context.getApplicationContext(), options.getLogger()))
352353
: AndroidContinuousProfiler.createLegacy(
353354
buildInfoProvider,
354355
frameMetricsCollector,

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

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,19 @@
2020
import io.sentry.SentryLevel;
2121
import io.sentry.SentryOptions;
2222
import io.sentry.TracesSampler;
23+
import io.sentry.SentryNanotimeDate;
24+
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
25+
import io.sentry.profilemeasurements.ProfileMeasurement;
26+
import io.sentry.profilemeasurements.ProfileMeasurementValue;
2327
import io.sentry.protocol.SentryId;
2428
import io.sentry.transport.RateLimiter;
2529
import io.sentry.util.AutoClosableReentrantLock;
2630
import io.sentry.util.LazyEvaluator;
2731
import io.sentry.util.SentryRandom;
32+
import java.io.File;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
import java.util.concurrent.ConcurrentLinkedDeque;
2836
import java.util.concurrent.Future;
2937
import java.util.concurrent.RejectedExecutionException;
3038
import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,6 +70,7 @@ public class PerfettoContinuousProfiler
6270
private final @NotNull LazyEvaluator.Evaluator<PerfettoProfiler> perfettoProfilerSupplier;
6371

6472
private @Nullable PerfettoProfiler perfettoProfiler = null;
73+
private final @NotNull FrameMetricsProfiler frameMetrics;
6574
private boolean isRunning = false;
6675
private @Nullable IScopes scopes;
6776
private @Nullable CompositePerformanceCollector performanceCollector;
@@ -80,10 +89,12 @@ public class PerfettoContinuousProfiler
8089
public PerfettoContinuousProfiler(
8190
final @NotNull BuildInfoProvider buildInfoProvider,
8291
final @NotNull ILogger logger,
92+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
8393
final @NotNull LazyEvaluator.Evaluator<ISentryExecutorService> executorServiceSupplier,
8494
final @NotNull LazyEvaluator.Evaluator<PerfettoProfiler> perfettoProfilerSupplier) {
8595
this.buildInfoProvider = buildInfoProvider;
8696
this.logger = logger;
97+
this.frameMetrics = new FrameMetricsProfiler(frameMetricsCollector);
8798
this.executorServiceSupplier = executorServiceSupplier;
8899
this.perfettoProfilerSupplier = perfettoProfilerSupplier;
89100
}
@@ -248,15 +259,15 @@ private void startInternal() {
248259

249260
startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now();
250261

251-
final AndroidProfiler.ProfileStartData startData =
252-
perfettoProfiler.start(MAX_CHUNK_DURATION_MILLIS);
253-
if (startData == null) {
262+
if (!perfettoProfiler.start(MAX_CHUNK_DURATION_MILLIS)) {
254263
logger.log(
255264
SentryLevel.ERROR,
256-
"Failed to start Perfetto profiling. PerfettoProfiler.start() returned null.");
265+
"Failed to start Perfetto profiling. PerfettoProfiler.start() returned false.");
257266
return;
258267
}
259268

269+
frameMetrics.startCollection();
270+
260271
isRunning = true;
261272

262273
if (profilerId.equals(SentryId.EMPTY_ID)) {
@@ -296,7 +307,8 @@ private void stopInternal(final boolean restartProfiler) {
296307
if (stopFuture != null) {
297308
stopFuture.cancel(false);
298309
}
299-
// check if profiler was created and it's running
310+
311+
// Make sure perfetto was running
300312
if (perfettoProfiler == null || !isRunning) {
301313
profilerId = SentryId.EMPTY_ID;
302314
chunkId = SentryId.EMPTY_ID;
@@ -310,10 +322,12 @@ private void stopInternal(final boolean restartProfiler) {
310322
performanceCollector.stop(chunkId.toString());
311323
}
312324

313-
final AndroidProfiler.ProfileEndData endData = perfettoProfiler.endAndCollect();
325+
final @NotNull Map<String, ProfileMeasurement> measurements =
326+
frameMetrics.stopCollection();
327+
328+
final @Nullable File traceFile = perfettoProfiler.endAndCollect();
314329

315-
// check if profiler ended successfully
316-
if (endData == null) {
330+
if (traceFile == null) {
317331
logger.log(
318332
SentryLevel.ERROR,
319333
"An error occurred while collecting a profile chunk, and it won't be sent.");
@@ -322,8 +336,8 @@ private void stopInternal(final boolean restartProfiler) {
322336
new ProfileChunk.Builder(
323337
profilerId,
324338
chunkId,
325-
endData.measurementsMap,
326-
endData.traceFile,
339+
measurements,
340+
traceFile,
327341
startProfileChunkTimestamp,
328342
ProfileChunk.PLATFORM_ANDROID);
329343
builder.setContentType("perfetto");
@@ -389,4 +403,89 @@ Future<?> getStopFuture() {
389403
public int getActiveTraceCount() {
390404
return activeTraceCount;
391405
}
406+
407+
/**
408+
* Utility wrapping {@link SentryFrameMetricsCollector} for frame metrics collection in a single
409+
* profiling chunk. Wraps with start/stop lifecycle and measurement snapshotting.
410+
*
411+
* <p>Frame metrics are delivered on the FrameMetrics HandlerThread via {@code
412+
* onFrameMetricCollected}. The deques use {@link ConcurrentLinkedDeque} because the HandlerThread
413+
* writes and the executor thread reads in {@code stopCollectionAndBuildMeasurements}.
414+
*/
415+
private static class FrameMetricsProfiler {
416+
private final @NotNull SentryFrameMetricsCollector collector;
417+
private @Nullable String listenerId = null;
418+
419+
private final @NotNull ConcurrentLinkedDeque<ProfileMeasurementValue>
420+
slowFrameRenderMeasurements = new ConcurrentLinkedDeque<>();
421+
private final @NotNull ConcurrentLinkedDeque<ProfileMeasurementValue>
422+
frozenFrameRenderMeasurements = new ConcurrentLinkedDeque<>();
423+
private final @NotNull ConcurrentLinkedDeque<ProfileMeasurementValue>
424+
screenFrameRateMeasurements = new ConcurrentLinkedDeque<>();
425+
426+
FrameMetricsProfiler(final @NotNull SentryFrameMetricsCollector collector) {
427+
this.collector = collector;
428+
}
429+
430+
void startCollection() {
431+
slowFrameRenderMeasurements.clear();
432+
frozenFrameRenderMeasurements.clear();
433+
screenFrameRateMeasurements.clear();
434+
listenerId =
435+
collector.startCollection(
436+
new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
437+
float lastRefreshRate = 0;
438+
439+
@Override
440+
public void onFrameMetricCollected(
441+
final long frameStartNanos,
442+
final long frameEndNanos,
443+
final long durationNanos,
444+
final long delayNanos,
445+
final boolean isSlow,
446+
final boolean isFrozen,
447+
final float refreshRate) {
448+
final long timestampNanos = new SentryNanotimeDate().nanoTimestamp();
449+
if (isFrozen) {
450+
frozenFrameRenderMeasurements.addLast(
451+
new ProfileMeasurementValue(frameEndNanos, durationNanos, timestampNanos));
452+
} else if (isSlow) {
453+
slowFrameRenderMeasurements.addLast(
454+
new ProfileMeasurementValue(frameEndNanos, durationNanos, timestampNanos));
455+
}
456+
if (refreshRate != lastRefreshRate) {
457+
lastRefreshRate = refreshRate;
458+
screenFrameRateMeasurements.addLast(
459+
new ProfileMeasurementValue(frameEndNanos, refreshRate, timestampNanos));
460+
}
461+
}
462+
});
463+
}
464+
465+
@NotNull
466+
Map<String, ProfileMeasurement> stopCollection() {
467+
collector.stopCollection(listenerId);
468+
listenerId = null;
469+
470+
final @NotNull Map<String, ProfileMeasurement> measurements = new HashMap<>();
471+
if (!slowFrameRenderMeasurements.isEmpty()) {
472+
measurements.put(
473+
ProfileMeasurement.ID_SLOW_FRAME_RENDERS,
474+
new ProfileMeasurement(
475+
ProfileMeasurement.UNIT_NANOSECONDS, slowFrameRenderMeasurements));
476+
}
477+
if (!frozenFrameRenderMeasurements.isEmpty()) {
478+
measurements.put(
479+
ProfileMeasurement.ID_FROZEN_FRAME_RENDERS,
480+
new ProfileMeasurement(
481+
ProfileMeasurement.UNIT_NANOSECONDS, frozenFrameRenderMeasurements));
482+
}
483+
if (!screenFrameRateMeasurements.isEmpty()) {
484+
measurements.put(
485+
ProfileMeasurement.ID_SCREEN_FRAME_RATES,
486+
new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements));
487+
}
488+
return measurements;
489+
}
490+
}
392491
}

0 commit comments

Comments
 (0)