Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
50505ef
Set continuousProfilesSampleRate and startProfiler() and stopProfiler…
stefanosiano Jan 13, 2025
def8f0c
Added chunk start timestamp to ProfileChunk
stefanosiano Jan 20, 2025
6c07e33
updated changelog
stefanosiano Jan 21, 2025
6eeed8e
Moved setContinuousProfilesSampleRate into ExperimentalOptions
stefanosiano Jan 24, 2025
c9fbfbb
increased continuous profiling chunk duration to 1 minute
stefanosiano Feb 11, 2025
018391a
Merge branch 'feat/continuous-profiling-part1' into feat/continuous-p…
stefanosiano Feb 18, 2025
dcee57a
replaced continuousProfilesSampleRate with profileSessionSampleRate (…
stefanosiano Feb 18, 2025
04e99f9
renamed Sentry.startProfiler with Sentry.startProfileSession and Sent…
stefanosiano Feb 19, 2025
c0279e2
renamed Sentry.startProfiler with Sentry.startProfileSession and Sent…
stefanosiano Feb 19, 2025
fbe53eb
Added ProfileLifecycle
stefanosiano Feb 24, 2025
5c2c846
Merge branch 'feat/continuous-profiling-part1' into feat/continuous-p…
stefanosiano Feb 27, 2025
79742ca
merged base branch
stefanosiano Feb 27, 2025
dd735c2
added isStartProfilerOnAppStart experimental option
stefanosiano Feb 28, 2025
d790035
added isStartProfilerOnAppStart logic and tests
stefanosiano Mar 4, 2025
d72b09f
added app start option checks in SentryPerformanceProvider
stefanosiano Mar 7, 2025
6b168b6
added @Nullable annotation to SentryAppStartProfilingOptions json dec…
stefanosiano Mar 12, 2025
1381d4a
added @Nullable annotation to ManifestMetadataReader json decoding
stefanosiano Mar 12, 2025
23ee0ae
Merge branch 'feat/continuous-profiling-part1' into feat/continuous-p…
stefanosiano Mar 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti
public fun <init> (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V
public fun close ()V
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
public fun getRootSpanCounter ()I
public fun isRunning ()Z
public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
public fun reevaluateSampling ()V
public fun start (Lio/sentry/TracesSampler;)V
public fun stop ()V
public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V
public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V
}

public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED;
import static java.util.concurrent.TimeUnit.SECONDS;

import android.annotation.SuppressLint;
import android.os.Build;
import io.sentry.CompositePerformanceCollector;
import io.sentry.DataCategory;
Expand All @@ -15,6 +14,7 @@
import io.sentry.NoOpScopes;
import io.sentry.PerformanceCollectionData;
import io.sentry.ProfileChunk;
import io.sentry.ProfileLifecycle;
import io.sentry.Sentry;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
Expand Down Expand Up @@ -58,6 +58,7 @@ public class AndroidContinuousProfiler
private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate();
private boolean shouldSample = true;
private boolean isSampled = false;
private int rootSpanCounter = 0;

public AndroidContinuousProfiler(
final @NotNull BuildInfoProvider buildInfoProvider,
Expand Down Expand Up @@ -103,7 +104,10 @@ private void init() {
logger);
}

public synchronized void start(final @NotNull TracesSampler tracesSampler) {
@Override
public synchronized void startProfileSession(
final @NotNull ProfileLifecycle profileLifecycle,
final @NotNull TracesSampler tracesSampler) {
if (shouldSample) {
isSampled = tracesSampler.sampleSessionProfile();
shouldSample = false;
Expand All @@ -112,15 +116,31 @@ public synchronized void start(final @NotNull TracesSampler tracesSampler) {
logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision.");
return;
}
if (isRunning()) {
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
return;
switch (profileLifecycle) {
case TRACE:
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
// the profiler is running or close() is called. This is just a safety check.
if (rootSpanCounter < 0) {
rootSpanCounter = 0;
}
rootSpanCounter++;
break;
case MANUAL:
// We check if the profiler is already running and log a message only in manual mode, since
// in trace mode we can have multiple concurrent traces
if (isRunning()) {
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
return;
}
break;
}
if (!isRunning()) {
logger.log(SentryLevel.DEBUG, "Started Profiler.");
start();
}
logger.log(SentryLevel.DEBUG, "Started Profiler.");
startProfile();
}

private synchronized void startProfile() {
private synchronized void start() {
if ((scopes == null || scopes == NoOpScopes.getInstance())
&& Sentry.getCurrentScopes() != NoOpScopes.getInstance()) {
this.scopes = Sentry.getCurrentScopes();
Expand Down Expand Up @@ -150,15 +170,15 @@ private synchronized void startProfile() {
|| rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) {
logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
// Let's stop and reset profiler id, as the profile is now broken anyway
stop();
stop(false);
return;
}

// If device is offline, we don't start the profiler, to avoid flooding the cache
if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) {
logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler.");
// Let's stop and reset profiler id, as the profile is now broken anyway
stop();
stop(false);
return;
}
startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now();
Expand Down Expand Up @@ -195,11 +215,28 @@ private synchronized void startProfile() {
}
}

public synchronized void stop() {
stop(false);
@Override
public synchronized void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) {
switch (profileLifecycle) {
case TRACE:
rootSpanCounter--;
// If there are active spans, and profile lifecycle is trace, we don't stop the profiler
if (rootSpanCounter > 0) {
return;
}
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
// the profiler is running or close() is called. This is just a safety check.
if (rootSpanCounter < 0) {
rootSpanCounter = 0;
}
stop(false);
break;
case MANUAL:
stop(false);
break;
}
}

@SuppressLint("NewApi")
private synchronized void stop(final boolean restartProfiler) {
if (stopFuture != null) {
stopFuture.cancel(true);
Expand Down Expand Up @@ -256,7 +293,7 @@ private synchronized void stop(final boolean restartProfiler) {

if (restartProfiler) {
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
startProfile();
start();
} else {
// When the profiler is stopped manually, we have to reset its id
profilerId = SentryId.EMPTY_ID;
Expand All @@ -269,7 +306,8 @@ public synchronized void reevaluateSampling() {
}

public synchronized void close() {
stop();
rootSpanCounter = 0;
stop(false);
isClosed.set(true);
}

Expand Down Expand Up @@ -315,13 +353,18 @@ Future<?> getStopFuture() {
return stopFuture;
}

@VisibleForTesting
public int getRootSpanCounter() {
return rootSpanCounter;
}

@Override
public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) {
// We stop the profiler as soon as we are rate limited, to avoid the performance overhead
if (rateLimiter.isActiveForCategory(All)
|| rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) {
logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
stop();
stop(false);
}
// If we are not rate limited anymore, we don't do anything: the profile is broken, so it's
// useless to restart it automatically
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.os.Bundle;
import io.sentry.ILogger;
import io.sentry.InitPriority;
import io.sentry.ProfileLifecycle;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.SentryLevel;
import io.sentry.protocol.SdkVersion;
Expand Down Expand Up @@ -67,6 +68,10 @@ final class ManifestMetadataReader {
static final String PROFILE_SESSION_SAMPLE_RATE =
"io.sentry.traces.profiling.session-sample-rate";

static final String PROFILE_LIFECYCLE = "io.sentry.traces.profiling.lifecycle";

static final String PROFILER_START_ON_APP_START = "io.sentry.traces.profiling.start-on-app-start";

@ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling";
static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets";

Expand Down Expand Up @@ -326,6 +331,28 @@ static void applyMetadata(
}
}

final String profileLifecycle =
readString(
metadata,
logger,
PROFILE_LIFECYCLE,
options.getProfileLifecycle().name().toLowerCase(Locale.ROOT));
if (profileLifecycle != null) {
options
.getExperimental()
.setProfileLifecycle(
ProfileLifecycle.valueOf(profileLifecycle.toUpperCase(Locale.ROOT)));
}

options
.getExperimental()
.setStartProfilerOnAppStart(
readBool(
metadata,
logger,
PROFILER_START_ON_APP_START,
options.isStartProfilerOnAppStart()));

options.setEnableUserInteractionTracing(
readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri
return;
}

if (profilingOptions.isContinuousProfilingEnabled()) {
if (profilingOptions.isContinuousProfilingEnabled()
&& profilingOptions.isStartProfilerOnAppStart()) {
createAndStartContinuousProfiler(context, profilingOptions, appStartMetrics);
return;
}
Expand All @@ -150,8 +151,9 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri
return;
}

createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics);

if (profilingOptions.isEnableAppStartProfiling()) {
createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics);
}
} catch (FileNotFoundException e) {
logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e);
} catch (Throwable e) {
Expand Down Expand Up @@ -186,7 +188,8 @@ private void createAndStartContinuousProfiler(
sentryOptions
.getExperimental()
.setProfileSessionSampleRate(profilingOptions.isContinuousProfileSampled() ? 1.0 : 0.0);
appStartContinuousProfiler.start(new TracesSampler(sentryOptions));
appStartContinuousProfiler.startProfileSession(
profilingOptions.getProfileLifecycle(), new TracesSampler(sentryOptions));
}

private void createAndStartTransactionProfiler(
Expand Down
Loading