Skip to content

Commit 873bad3

Browse files
43jayclaude
andcommitted
ref(profiling): Consolidate measurement collection into ChunkMeasurementCollector
Rename FrameMetricsProfiler to ChunkMeasurementCollector and expand it to own the full measurement lifecycle for a profiling chunk: both frame metrics (slow/frozen frames, refresh rate) (previous) AND performance data (CPU usage, memory footprint) from the CompositePerformanceCollector (new change). Also restores the performanceCollector impl that was accidentally removed in an earlier commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 77940b8 commit 873bad3

File tree

4 files changed

+121
-43
lines changed

4 files changed

+121
-43
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,7 @@ private static void setupProfiler(
348348
frameMetricsCollector,
349349
() -> options.getExecutorService(),
350350
() ->
351-
new PerfettoProfiler(
352-
context.getApplicationContext(), options.getLogger()))
351+
new PerfettoProfiler(context.getApplicationContext(), options.getLogger()))
353352
: AndroidContinuousProfiler.createLegacy(
354353
buildInfoProvider,
355354
frameMetricsCollector,

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

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
import io.sentry.ISentryExecutorService;
1414
import io.sentry.ISentryLifecycleToken;
1515
import io.sentry.NoOpScopes;
16+
import io.sentry.PerformanceCollectionData;
1617
import io.sentry.ProfileChunk;
1718
import io.sentry.ProfileLifecycle;
1819
import io.sentry.Sentry;
1920
import io.sentry.SentryDate;
2021
import io.sentry.SentryLevel;
22+
import io.sentry.SentryNanotimeDate;
2123
import io.sentry.SentryOptions;
2224
import io.sentry.TracesSampler;
23-
import io.sentry.SentryNanotimeDate;
2425
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
2526
import io.sentry.profilemeasurements.ProfileMeasurement;
2627
import io.sentry.profilemeasurements.ProfileMeasurementValue;
@@ -30,7 +31,9 @@
3031
import io.sentry.util.LazyEvaluator;
3132
import io.sentry.util.SentryRandom;
3233
import java.io.File;
34+
import java.util.ArrayDeque;
3335
import java.util.HashMap;
36+
import java.util.List;
3437
import java.util.Map;
3538
import java.util.concurrent.ConcurrentLinkedDeque;
3639
import java.util.concurrent.Future;
@@ -49,14 +52,14 @@
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
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
import org.jetbrains.annotations.NotNull;
1818
import org.jetbrains.annotations.Nullable;
1919

20-
/**
21-
* Wraps Android's {@link ProfilingManager} API for Perfetto stack sampling.
22-
*/
20+
/** Wraps Android's {@link ProfilingManager} API for Perfetto stack sampling. */
2321
@ApiStatus.Internal
2422
@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
2523
public class PerfettoProfiler {

sentry-android-core/src/test/java/io/sentry/android/core/PerfettoContinuousProfilerTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ class PerfettoContinuousProfilerTest {
127127
// shouldStop must have been reset to false by startProfiler, so the profiler
128128
// should restart for the next chunk.
129129
fixture.executor.runAll()
130-
assertTrue(profiler.isRunning, "Profiler should continue running after chunk restart — shouldStop must be reset on start")
130+
assertTrue(
131+
profiler.isRunning,
132+
"Profiler should continue running after chunk restart — shouldStop must be reset on start",
133+
)
131134
}
132135
}

0 commit comments

Comments
 (0)