Skip to content

Commit 5c3cd7a

Browse files
authored
Continuous Profiling - Add delayed stop (#4293)
* replaced synchronized blocks with AutoClosableReentrantLock in AndroidContinuousProfiler * Added "delayed" stop of profiler, which stops the profiler after the current chunk is finished * Added default span data (profiler id, thread name and thread id) to transaction root span
1 parent a17f72e commit 5c3cd7a

File tree

6 files changed

+200
-178
lines changed

6 files changed

+200
-178
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Continuous Profiling - Add delayed stop ([#4293](https://github.com/getsentry/sentry-java/pull/4293))
78
- Continuous Profiling - Out of Experimental ([#4310](https://github.com/getsentry/sentry-java/pull/4310))
89

910
## 8.6.0

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

+129-110
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.sentry.ILogger;
1212
import io.sentry.IScopes;
1313
import io.sentry.ISentryExecutorService;
14+
import io.sentry.ISentryLifecycleToken;
1415
import io.sentry.NoOpScopes;
1516
import io.sentry.PerformanceCollectionData;
1617
import io.sentry.ProfileChunk;
@@ -24,6 +25,7 @@
2425
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
2526
import io.sentry.protocol.SentryId;
2627
import io.sentry.transport.RateLimiter;
28+
import io.sentry.util.AutoClosableReentrantLock;
2729
import io.sentry.util.SentryRandom;
2830
import java.util.ArrayList;
2931
import java.util.List;
@@ -57,10 +59,14 @@ public class AndroidContinuousProfiler
5759
private @NotNull SentryId chunkId = SentryId.EMPTY_ID;
5860
private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false);
5961
private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate();
60-
private boolean shouldSample = true;
62+
private volatile boolean shouldSample = true;
63+
private boolean shouldStop = false;
6164
private boolean isSampled = false;
6265
private int rootSpanCounter = 0;
6366

67+
private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
68+
private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock();
69+
6470
public AndroidContinuousProfiler(
6571
final @NotNull BuildInfoProvider buildInfoProvider,
6672
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
@@ -106,42 +112,46 @@ private void init() {
106112
}
107113

108114
@Override
109-
public synchronized void startProfiler(
115+
public void startProfiler(
110116
final @NotNull ProfileLifecycle profileLifecycle,
111117
final @NotNull TracesSampler tracesSampler) {
112-
if (shouldSample) {
113-
isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble());
114-
shouldSample = false;
115-
}
116-
if (!isSampled) {
117-
logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision.");
118-
return;
119-
}
120-
switch (profileLifecycle) {
121-
case TRACE:
122-
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
123-
// the profiler is running or close() is called. This is just a safety check.
124-
if (rootSpanCounter < 0) {
125-
rootSpanCounter = 0;
126-
}
127-
rootSpanCounter++;
128-
break;
129-
case MANUAL:
130-
// We check if the profiler is already running and log a message only in manual mode, since
131-
// in trace mode we can have multiple concurrent traces
132-
if (isRunning()) {
133-
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
134-
return;
135-
}
136-
break;
137-
}
138-
if (!isRunning()) {
139-
logger.log(SentryLevel.DEBUG, "Started Profiler.");
140-
start();
118+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
119+
if (shouldSample) {
120+
isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble());
121+
shouldSample = false;
122+
}
123+
if (!isSampled) {
124+
logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision.");
125+
return;
126+
}
127+
switch (profileLifecycle) {
128+
case TRACE:
129+
// rootSpanCounter should never be negative, unless the user changed profile lifecycle
130+
// while
131+
// the profiler is running or close() is called. This is just a safety check.
132+
if (rootSpanCounter < 0) {
133+
rootSpanCounter = 0;
134+
}
135+
rootSpanCounter++;
136+
break;
137+
case MANUAL:
138+
// We check if the profiler is already running and log a message only in manual mode,
139+
// since
140+
// in trace mode we can have multiple concurrent traces
141+
if (isRunning()) {
142+
logger.log(SentryLevel.DEBUG, "Profiler is already running.");
143+
return;
144+
}
145+
break;
146+
}
147+
if (!isRunning()) {
148+
logger.log(SentryLevel.DEBUG, "Started Profiler.");
149+
start();
150+
}
141151
}
142152
}
143153

144-
private synchronized void start() {
154+
private void start() {
145155
if ((scopes == null || scopes == NoOpScopes.getInstance())
146156
&& Sentry.getCurrentScopes() != NoOpScopes.getInstance()) {
147157
this.scopes = Sentry.getCurrentScopes();
@@ -213,103 +223,112 @@ private synchronized void start() {
213223
SentryLevel.ERROR,
214224
"Failed to schedule profiling chunk finish. Did you call Sentry.close()?",
215225
e);
226+
shouldStop = true;
216227
}
217228
}
218229

219230
@Override
220-
public synchronized void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) {
221-
switch (profileLifecycle) {
222-
case TRACE:
223-
rootSpanCounter--;
224-
// If there are active spans, and profile lifecycle is trace, we don't stop the profiler
225-
if (rootSpanCounter > 0) {
226-
return;
227-
}
228-
// rootSpanCounter should never be negative, unless the user changed profile lifecycle while
229-
// the profiler is running or close() is called. This is just a safety check.
230-
if (rootSpanCounter < 0) {
231-
rootSpanCounter = 0;
232-
}
233-
stop(false);
234-
break;
235-
case MANUAL:
236-
stop(false);
237-
break;
231+
public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) {
232+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
233+
switch (profileLifecycle) {
234+
case TRACE:
235+
rootSpanCounter--;
236+
// If there are active spans, and profile lifecycle is trace, we don't stop the profiler
237+
if (rootSpanCounter > 0) {
238+
return;
239+
}
240+
// rootSpanCounter should never be negative, unless the user changed profile lifecycle
241+
// while the profiler is running or close() is called. This is just a safety check.
242+
if (rootSpanCounter < 0) {
243+
rootSpanCounter = 0;
244+
}
245+
shouldStop = true;
246+
break;
247+
case MANUAL:
248+
shouldStop = true;
249+
break;
250+
}
238251
}
239252
}
240253

241-
private synchronized void stop(final boolean restartProfiler) {
242-
if (stopFuture != null) {
243-
stopFuture.cancel(true);
244-
}
245-
// check if profiler was created and it's running
246-
if (profiler == null || !isRunning) {
247-
// When the profiler is stopped due to an error (e.g. offline or rate limited), reset the ids
248-
profilerId = SentryId.EMPTY_ID;
249-
chunkId = SentryId.EMPTY_ID;
250-
return;
251-
}
252-
253-
// onTransactionStart() is only available since Lollipop_MR1
254-
// and SystemClock.elapsedRealtimeNanos() since Jelly Bean
255-
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) {
256-
return;
257-
}
254+
private void stop(final boolean restartProfiler) {
255+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
256+
if (stopFuture != null) {
257+
stopFuture.cancel(true);
258+
}
259+
// check if profiler was created and it's running
260+
if (profiler == null || !isRunning) {
261+
// When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
262+
// ids
263+
profilerId = SentryId.EMPTY_ID;
264+
chunkId = SentryId.EMPTY_ID;
265+
return;
266+
}
258267

259-
List<PerformanceCollectionData> performanceCollectionData = null;
260-
if (performanceCollector != null) {
261-
performanceCollectionData = performanceCollector.stop(chunkId.toString());
262-
}
268+
// onTransactionStart() is only available since Lollipop_MR1
269+
// and SystemClock.elapsedRealtimeNanos() since Jelly Bean
270+
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) {
271+
return;
272+
}
263273

264-
final AndroidProfiler.ProfileEndData endData =
265-
profiler.endAndCollect(false, performanceCollectionData);
274+
List<PerformanceCollectionData> performanceCollectionData = null;
275+
if (performanceCollector != null) {
276+
performanceCollectionData = performanceCollector.stop(chunkId.toString());
277+
}
266278

267-
// check if profiler end successfully
268-
if (endData == null) {
269-
logger.log(
270-
SentryLevel.ERROR,
271-
"An error occurred while collecting a profile chunk, and it won't be sent.");
272-
} else {
273-
// The scopes can be null if the profiler is started before the SDK is initialized (app start
274-
// profiling), meaning there's no scopes to send the chunks. In that case, we store the data
275-
// in a list and send it when the next chunk is finished.
276-
synchronized (payloadBuilders) {
277-
payloadBuilders.add(
278-
new ProfileChunk.Builder(
279-
profilerId,
280-
chunkId,
281-
endData.measurementsMap,
282-
endData.traceFile,
283-
startProfileChunkTimestamp));
279+
final AndroidProfiler.ProfileEndData endData =
280+
profiler.endAndCollect(false, performanceCollectionData);
281+
282+
// check if profiler end successfully
283+
if (endData == null) {
284+
logger.log(
285+
SentryLevel.ERROR,
286+
"An error occurred while collecting a profile chunk, and it won't be sent.");
287+
} else {
288+
// The scopes can be null if the profiler is started before the SDK is initialized (app
289+
// start profiling), meaning there's no scopes to send the chunks. In that case, we store
290+
// the data in a list and send it when the next chunk is finished.
291+
try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) {
292+
payloadBuilders.add(
293+
new ProfileChunk.Builder(
294+
profilerId,
295+
chunkId,
296+
endData.measurementsMap,
297+
endData.traceFile,
298+
startProfileChunkTimestamp));
299+
}
284300
}
285-
}
286301

287-
isRunning = false;
288-
// A chunk is finished. Next chunk will have a different id.
289-
chunkId = SentryId.EMPTY_ID;
302+
isRunning = false;
303+
// A chunk is finished. Next chunk will have a different id.
304+
chunkId = SentryId.EMPTY_ID;
290305

291-
if (scopes != null) {
292-
sendChunks(scopes, scopes.getOptions());
293-
}
306+
if (scopes != null) {
307+
sendChunks(scopes, scopes.getOptions());
308+
}
294309

295-
if (restartProfiler) {
296-
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
297-
start();
298-
} else {
299-
// When the profiler is stopped manually, we have to reset its id
300-
profilerId = SentryId.EMPTY_ID;
301-
logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
310+
if (restartProfiler && !shouldStop) {
311+
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
312+
start();
313+
} else {
314+
// When the profiler is stopped manually, we have to reset its id
315+
profilerId = SentryId.EMPTY_ID;
316+
logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
317+
}
302318
}
303319
}
304320

305-
public synchronized void reevaluateSampling() {
321+
public void reevaluateSampling() {
306322
shouldSample = true;
307323
}
308324

309-
public synchronized void close() {
310-
rootSpanCounter = 0;
311-
stop(false);
312-
isClosed.set(true);
325+
public void close() {
326+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
327+
rootSpanCounter = 0;
328+
shouldStop = true;
329+
stop(false);
330+
isClosed.set(true);
331+
}
313332
}
314333

315334
@Override
@@ -328,7 +347,7 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti
328347
return;
329348
}
330349
final ArrayList<ProfileChunk> payloads = new ArrayList<>(payloadBuilders.size());
331-
synchronized (payloadBuilders) {
350+
try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) {
332351
for (ProfileChunk.Builder builder : payloadBuilders) {
333352
payloads.add(builder.build(options));
334353
}

0 commit comments

Comments
 (0)