Skip to content

Commit 85e54fc

Browse files
committed
ref(profiling): Consolidate measurement collection into ChunkMeasurementCollector
Rename FrameMetricsProfiler to ChunkMeasurementCollector and expand it to own the measurement lifecycle for both types of profiling we do outside of perfetto: 1) frame metrics (slow/frozen frames, refresh rate) (previous) 2) performance data (CPU usage, memory footprint) from the CompositePerformanceCollector (new change). Also restores the performanceCollector impl that was accidentally removed in an earlier commit.
1 parent 331622b commit 85e54fc

File tree

3 files changed

+117
-42
lines changed

3 files changed

+117
-42
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 {

0 commit comments

Comments
 (0)