1313import io .sentry .ISentryExecutorService ;
1414import io .sentry .ISentryLifecycleToken ;
1515import io .sentry .NoOpScopes ;
16+ import io .sentry .PerformanceCollectionData ;
1617import io .sentry .ProfileChunk ;
1718import io .sentry .ProfileLifecycle ;
1819import io .sentry .Sentry ;
1920import io .sentry .SentryDate ;
2021import io .sentry .SentryLevel ;
22+ import io .sentry .SentryNanotimeDate ;
2123import io .sentry .SentryOptions ;
2224import io .sentry .TracesSampler ;
23- import io .sentry .SentryNanotimeDate ;
2425import io .sentry .android .core .internal .util .SentryFrameMetricsCollector ;
2526import io .sentry .profilemeasurements .ProfileMeasurement ;
2627import io .sentry .profilemeasurements .ProfileMeasurementValue ;
3031import io .sentry .util .LazyEvaluator ;
3132import io .sentry .util .SentryRandom ;
3233import java .io .File ;
34+ import java .util .ArrayDeque ;
3335import java .util .HashMap ;
36+ import java .util .List ;
3437import java .util .Map ;
3538import java .util .concurrent .ConcurrentLinkedDeque ;
3639import java .util .concurrent .Future ;
4952 * profiling backends independent. All ProfilingManager API usage is confined to this file and
5053 * {@link PerfettoProfiler}.
5154 *
52- * <p>Currently, this class doesn't do app-start profiling {@link SentryPerformanceProvider}.
53- * It is created during {@code Sentry.init()}.
55+ * <p>Currently, this class doesn't do app-start profiling {@link SentryPerformanceProvider}. It is
56+ * created during {@code Sentry.init()}.
5457 *
5558 * <p>Thread safety: all mutable state is guarded by a single {@link
5659 * io.sentry.util.AutoClosableReentrantLock}. Public entry points ({@link #startProfiler}, {@link
57- * #stopProfiler}, {@link #close}, {@link #onRateLimitChanged}, {@link #reevaluateSampling}, and
58- * the getters) acquire the lock themselves and are thread-safe.
59- * Private methods {@code startInternal} and {@code stopInternal} require the caller to hold the lock.
60+ * #stopProfiler}, {@link #close}, {@link #onRateLimitChanged}, {@link #reevaluateSampling}, and the
61+ * getters) acquire the lock themselves and are thread-safe. Private methods {@code startInternal}
62+ * and {@code stopInternal} require the caller to hold the lock.
6063 */
6164@ ApiStatus .Internal
6265@ RequiresApi (api = Build .VERSION_CODES .VANILLA_ICE_CREAM )
@@ -70,7 +73,7 @@ public class PerfettoContinuousProfiler
7073 private final @ NotNull LazyEvaluator .Evaluator <PerfettoProfiler > perfettoProfilerSupplier ;
7174
7275 private @ Nullable PerfettoProfiler perfettoProfiler = null ;
73- private final @ NotNull FrameMetricsProfiler frameMetrics ;
76+ private final @ NotNull ChunkMeasurementCollector chunkMeasurements ;
7477 private boolean isRunning = false ;
7578 private @ Nullable IScopes scopes ;
7679 private @ Nullable CompositePerformanceCollector performanceCollector ;
@@ -94,7 +97,7 @@ public PerfettoContinuousProfiler(
9497 final @ NotNull LazyEvaluator .Evaluator <PerfettoProfiler > perfettoProfilerSupplier ) {
9598 this .buildInfoProvider = buildInfoProvider ;
9699 this .logger = logger ;
97- this .frameMetrics = new FrameMetricsProfiler (frameMetricsCollector );
100+ this .chunkMeasurements = new ChunkMeasurementCollector (frameMetricsCollector );
98101 this .executorServiceSupplier = executorServiceSupplier ;
99102 this .perfettoProfilerSupplier = perfettoProfilerSupplier ;
100103 }
@@ -266,8 +269,6 @@ private void startInternal() {
266269 return ;
267270 }
268271
269- frameMetrics .startCollection ();
270-
271272 isRunning = true ;
272273
273274 if (profilerId .equals (SentryId .EMPTY_ID )) {
@@ -278,9 +279,7 @@ private void startInternal() {
278279 chunkId = new SentryId ();
279280 }
280281
281- if (performanceCollector != null ) {
282- performanceCollector .start (chunkId .toString ());
283- }
282+ chunkMeasurements .start (performanceCollector , chunkId .toString ());
284283
285284 try {
286285 stopFuture =
@@ -318,12 +317,7 @@ private void stopInternal(final boolean restartProfiler) {
318317 final @ NotNull IScopes scopes = resolveScopes ();
319318 final @ NotNull SentryOptions options = scopes .getOptions ();
320319
321- if (performanceCollector != null ) {
322- performanceCollector .stop (chunkId .toString ());
323- }
324-
325- final @ NotNull Map <String , ProfileMeasurement > measurements =
326- frameMetrics .stopCollection ();
320+ final @ NotNull Map <String , ProfileMeasurement > measurements = chunkMeasurements .stop ();
327321
328322 final @ Nullable File traceFile = perfettoProfiler .endAndCollect ();
329323
@@ -405,16 +399,20 @@ public int getActiveTraceCount() {
405399 }
406400
407401 /**
408- * Utility wrapping {@link SentryFrameMetricsCollector} for frame metrics collection in a single
409- * profiling chunk. Wraps with start/stop lifecycle and measurement snapshotting .
402+ * Collects measurements for a single profiling chunk: frame metrics (slow/frozen frames, refresh
403+ * rate) and performance data (CPU usage, memory footprint) .
410404 *
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}.
405+ * <p>Frame metrics are delivered on the FrameMetrics HandlerThread. The deques use {@link
406+ * ConcurrentLinkedDeque} because the HandlerThread writes and the executor thread reads.
407+ *
408+ * <p>Performance data is collected by the {@link CompositePerformanceCollector}'s Timer thread
409+ * every 100ms and returned as a list on {@code stop()}.
414410 */
415- private static class FrameMetricsProfiler {
416- private final @ NotNull SentryFrameMetricsCollector collector ;
417- private @ Nullable String listenerId = null ;
411+ private static class ChunkMeasurementCollector {
412+ private final @ NotNull SentryFrameMetricsCollector frameMetricsCollector ;
413+ private @ Nullable String frameMetricsListenerId = null ;
414+ private @ Nullable CompositePerformanceCollector performanceCollector = null ;
415+ private @ Nullable String chunkId = null ;
418416
419417 private final @ NotNull ConcurrentLinkedDeque <ProfileMeasurementValue >
420418 slowFrameRenderMeasurements = new ConcurrentLinkedDeque <>();
@@ -423,16 +421,22 @@ private static class FrameMetricsProfiler {
423421 private final @ NotNull ConcurrentLinkedDeque <ProfileMeasurementValue >
424422 screenFrameRateMeasurements = new ConcurrentLinkedDeque <>();
425423
426- FrameMetricsProfiler (final @ NotNull SentryFrameMetricsCollector collector ) {
427- this .collector = collector ;
424+ ChunkMeasurementCollector (final @ NotNull SentryFrameMetricsCollector frameMetricsCollector ) {
425+ this .frameMetricsCollector = frameMetricsCollector ;
428426 }
429427
430- void startCollection () {
428+ void start (
429+ final @ Nullable CompositePerformanceCollector performanceCollector ,
430+ final @ NotNull String chunkId ) {
431+ this .performanceCollector = performanceCollector ;
432+ this .chunkId = chunkId ;
433+
434+ // Start frame metrics collection (runs on the FrameMetrics HandlerThread)
431435 slowFrameRenderMeasurements .clear ();
432436 frozenFrameRenderMeasurements .clear ();
433437 screenFrameRateMeasurements .clear ();
434- listenerId =
435- collector .startCollection (
438+ frameMetricsListenerId =
439+ frameMetricsCollector .startCollection (
436440 new SentryFrameMetricsCollector .FrameMetricsCollectorListener () {
437441 float lastRefreshRate = 0 ;
438442
@@ -460,14 +464,39 @@ public void onFrameMetricCollected(
460464 }
461465 }
462466 });
467+
468+ // Start performance collection (runs on CompositePerformanceCollector's Timer thread)
469+ if (performanceCollector != null ) {
470+ performanceCollector .start (chunkId );
471+ }
463472 }
464473
474+ /**
475+ * Stops all collection, builds and returns the combined measurements map containing frame
476+ * metrics and performance data (CPU, memory).
477+ */
465478 @ NotNull
466- Map <String , ProfileMeasurement > stopCollection () {
467- collector .stopCollection (listenerId );
468- listenerId = null ;
469-
479+ Map <String , ProfileMeasurement > stop () {
470480 final @ NotNull Map <String , ProfileMeasurement > measurements = new HashMap <>();
481+ // Stop frame metrics
482+ frameMetricsCollector .stopCollection (frameMetricsListenerId );
483+ frameMetricsListenerId = null ;
484+ addFrameDataToMeasurements (measurements );
485+
486+ // Stop performance collection
487+ @ Nullable List <PerformanceCollectionData > performanceData = null ;
488+ if (performanceCollector != null && chunkId != null ) {
489+ performanceData = performanceCollector .stop (chunkId );
490+ addPerformanceDataToMeasurements (performanceData , measurements );
491+ }
492+ performanceCollector = null ;
493+ chunkId = null ;
494+
495+ return measurements ;
496+ }
497+
498+ private void addFrameDataToMeasurements (
499+ final @ NotNull Map <String , ProfileMeasurement > measurements ) {
471500 if (!slowFrameRenderMeasurements .isEmpty ()) {
472501 measurements .put (
473502 ProfileMeasurement .ID_SLOW_FRAME_RENDERS ,
@@ -485,7 +514,56 @@ Map<String, ProfileMeasurement> stopCollection() {
485514 ProfileMeasurement .ID_SCREEN_FRAME_RATES ,
486515 new ProfileMeasurement (ProfileMeasurement .UNIT_HZ , screenFrameRateMeasurements ));
487516 }
488- return measurements ;
517+ }
518+
519+ private static void addPerformanceDataToMeasurements (
520+ final @ Nullable List <PerformanceCollectionData > performanceData ,
521+ final @ NotNull Map <String , ProfileMeasurement > measurements ) {
522+ if (performanceData == null || performanceData .isEmpty ()) {
523+ return ;
524+ }
525+ final @ NotNull ArrayDeque <ProfileMeasurementValue > cpuUsageMeasurements =
526+ new ArrayDeque <>(performanceData .size ());
527+ final @ NotNull ArrayDeque <ProfileMeasurementValue > memoryUsageMeasurements =
528+ new ArrayDeque <>(performanceData .size ());
529+ final @ NotNull ArrayDeque <ProfileMeasurementValue > nativeMemoryUsageMeasurements =
530+ new ArrayDeque <>(performanceData .size ());
531+
532+ for (final @ NotNull PerformanceCollectionData data : performanceData ) {
533+ final long nanoTimestamp = data .getNanoTimestamp ();
534+ final @ Nullable Double cpuUsagePercentage = data .getCpuUsagePercentage ();
535+ final @ Nullable Long usedHeapMemory = data .getUsedHeapMemory ();
536+ final @ Nullable Long usedNativeMemory = data .getUsedNativeMemory ();
537+
538+ if (cpuUsagePercentage != null ) {
539+ cpuUsageMeasurements .addLast (
540+ new ProfileMeasurementValue (nanoTimestamp , cpuUsagePercentage , nanoTimestamp ));
541+ }
542+ if (usedHeapMemory != null ) {
543+ memoryUsageMeasurements .addLast (
544+ new ProfileMeasurementValue (nanoTimestamp , usedHeapMemory , nanoTimestamp ));
545+ }
546+ if (usedNativeMemory != null ) {
547+ nativeMemoryUsageMeasurements .addLast (
548+ new ProfileMeasurementValue (nanoTimestamp , usedNativeMemory , nanoTimestamp ));
549+ }
550+ }
551+
552+ if (!cpuUsageMeasurements .isEmpty ()) {
553+ measurements .put (
554+ ProfileMeasurement .ID_CPU_USAGE ,
555+ new ProfileMeasurement (ProfileMeasurement .UNIT_PERCENT , cpuUsageMeasurements ));
556+ }
557+ if (!memoryUsageMeasurements .isEmpty ()) {
558+ measurements .put (
559+ ProfileMeasurement .ID_MEMORY_FOOTPRINT ,
560+ new ProfileMeasurement (ProfileMeasurement .UNIT_BYTES , memoryUsageMeasurements ));
561+ }
562+ if (!nativeMemoryUsageMeasurements .isEmpty ()) {
563+ measurements .put (
564+ ProfileMeasurement .ID_MEMORY_NATIVE_FOOTPRINT ,
565+ new ProfileMeasurement (ProfileMeasurement .UNIT_BYTES , nativeMemoryUsageMeasurements ));
566+ }
489567 }
490568 }
491569}
0 commit comments