Skip to content

Commit 27d91aa

Browse files
authored
Add continuous profiling ProfileLifecycle (#4202)
* Added ProfileLifecycle * Sentry.startProfileSession() will work only in MANUAL mode * Tracer start will start profiler only in TRACE mode * Tracer and spans now attach profilerId only when sampled
1 parent 1440de9 commit 27d91aa

21 files changed

+509
-94
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti
4040
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
4141
public fun close ()V
4242
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
43+
public fun getRootSpanCounter ()I
4344
public fun isRunning ()Z
4445
public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
4546
public fun reevaluateSampling ()V
46-
public fun start (Lio/sentry/TracesSampler;)V
47-
public fun stop ()V
47+
public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V
48+
public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V
4849
}
4950

5051
public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector {

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

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED;
55
import static java.util.concurrent.TimeUnit.SECONDS;
66

7-
import android.annotation.SuppressLint;
87
import android.os.Build;
98
import io.sentry.CompositePerformanceCollector;
109
import io.sentry.DataCategory;
@@ -15,6 +14,7 @@
1514
import io.sentry.NoOpScopes;
1615
import io.sentry.PerformanceCollectionData;
1716
import io.sentry.ProfileChunk;
17+
import io.sentry.ProfileLifecycle;
1818
import io.sentry.Sentry;
1919
import io.sentry.SentryDate;
2020
import io.sentry.SentryLevel;
@@ -58,6 +58,7 @@ public class AndroidContinuousProfiler
5858
private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate();
5959
private boolean shouldSample = true;
6060
private boolean isSampled = false;
61+
private int rootSpanCounter = 0;
6162

6263
public AndroidContinuousProfiler(
6364
final @NotNull BuildInfoProvider buildInfoProvider,
@@ -103,7 +104,10 @@ private void init() {
103104
logger);
104105
}
105106

106-
public synchronized void start(final @NotNull TracesSampler tracesSampler) {
107+
@Override
108+
public synchronized void startProfileSession(
109+
final @NotNull ProfileLifecycle profileLifecycle,
110+
final @NotNull TracesSampler tracesSampler) {
107111
if (shouldSample) {
108112
isSampled = tracesSampler.sampleSessionProfile();
109113
shouldSample = false;
@@ -112,15 +116,31 @@ public synchronized void start(final @NotNull TracesSampler tracesSampler) {
112116
logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision.");
113117
return;
114118
}
115-
if (isRunning()) {
116-
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
117-
return;
119+
switch (profileLifecycle) {
120+
case TRACE:
121+
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
122+
// the profiler is running or close() is called. This is just a safety check.
123+
if (rootSpanCounter < 0) {
124+
rootSpanCounter = 0;
125+
}
126+
rootSpanCounter++;
127+
break;
128+
case MANUAL:
129+
// We check if the profiler is already running and log a message only in manual mode, since
130+
// in trace mode we can have multiple concurrent traces
131+
if (isRunning()) {
132+
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
133+
return;
134+
}
135+
break;
136+
}
137+
if (!isRunning()) {
138+
logger.log(SentryLevel.DEBUG, "Started Profiler.");
139+
start();
118140
}
119-
logger.log(SentryLevel.DEBUG, "Started Profiler.");
120-
startProfile();
121141
}
122142

123-
private synchronized void startProfile() {
143+
private synchronized void start() {
124144
if ((scopes == null || scopes == NoOpScopes.getInstance())
125145
&& Sentry.getCurrentScopes() != NoOpScopes.getInstance()) {
126146
this.scopes = Sentry.getCurrentScopes();
@@ -150,15 +170,15 @@ private synchronized void startProfile() {
150170
|| rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) {
151171
logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
152172
// Let's stop and reset profiler id, as the profile is now broken anyway
153-
stop();
173+
stop(false);
154174
return;
155175
}
156176

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

198-
public synchronized void stop() {
199-
stop(false);
218+
@Override
219+
public synchronized void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) {
220+
switch (profileLifecycle) {
221+
case TRACE:
222+
rootSpanCounter--;
223+
// If there are active spans, and profile lifecycle is trace, we don't stop the profiler
224+
if (rootSpanCounter > 0) {
225+
return;
226+
}
227+
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
228+
// the profiler is running or close() is called. This is just a safety check.
229+
if (rootSpanCounter < 0) {
230+
rootSpanCounter = 0;
231+
}
232+
stop(false);
233+
break;
234+
case MANUAL:
235+
stop(false);
236+
break;
237+
}
200238
}
201239

202-
@SuppressLint("NewApi")
203240
private synchronized void stop(final boolean restartProfiler) {
204241
if (stopFuture != null) {
205242
stopFuture.cancel(true);
@@ -256,7 +293,7 @@ private synchronized void stop(final boolean restartProfiler) {
256293

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

271308
public synchronized void close() {
272-
stop();
309+
rootSpanCounter = 0;
310+
stop(false);
273311
isClosed.set(true);
274312
}
275313

@@ -315,13 +353,18 @@ Future<?> getStopFuture() {
315353
return stopFuture;
316354
}
317355

356+
@VisibleForTesting
357+
public int getRootSpanCounter() {
358+
return rootSpanCounter;
359+
}
360+
318361
@Override
319362
public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) {
320363
// We stop the profiler as soon as we are rate limited, to avoid the performance overhead
321364
if (rateLimiter.isActiveForCategory(All)
322365
|| rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) {
323366
logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
324-
stop();
367+
stop(false);
325368
}
326369
// If we are not rate limited anymore, we don't do anything: the profile is broken, so it's
327370
// useless to restart it automatically

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import android.os.Bundle;
77
import io.sentry.ILogger;
88
import io.sentry.InitPriority;
9+
import io.sentry.ProfileLifecycle;
910
import io.sentry.SentryIntegrationPackageStorage;
1011
import io.sentry.SentryLevel;
1112
import io.sentry.protocol.SdkVersion;
@@ -67,6 +68,8 @@ final class ManifestMetadataReader {
6768
static final String PROFILE_SESSION_SAMPLE_RATE =
6869
"io.sentry.traces.profiling.session-sample-rate";
6970

71+
static final String PROFILE_LIFECYCLE = "io.sentry.traces.profiling.lifecycle";
72+
7073
@ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling";
7174
static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets";
7275

@@ -326,6 +329,19 @@ static void applyMetadata(
326329
}
327330
}
328331

332+
final String profileLifecycle =
333+
readString(
334+
metadata,
335+
logger,
336+
PROFILE_LIFECYCLE,
337+
options.getProfileLifecycle().name().toLowerCase(Locale.ROOT));
338+
if (profileLifecycle != null) {
339+
options
340+
.getExperimental()
341+
.setProfileLifecycle(
342+
ProfileLifecycle.valueOf(profileLifecycle.toUpperCase(Locale.ROOT)));
343+
}
344+
329345
options.setEnableUserInteractionTracing(
330346
readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing()));
331347

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ private void createAndStartContinuousProfiler(
186186
sentryOptions
187187
.getExperimental()
188188
.setProfileSessionSampleRate(profilingOptions.isContinuousProfileSampled() ? 1.0 : 0.0);
189-
appStartContinuousProfiler.start(new TracesSampler(sentryOptions));
189+
appStartContinuousProfiler.startProfileSession(
190+
profilingOptions.getProfileLifecycle(), new TracesSampler(sentryOptions));
190191
}
191192

192193
private void createAndStartTransactionProfiler(

0 commit comments

Comments
 (0)