Skip to content

Commit 66ae530

Browse files
committed
improve error handling, fix start stop start flow
1 parent b7645f2 commit 66ae530

2 files changed

Lines changed: 113 additions & 40 deletions

File tree

sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.sentry.util.AutoClosableReentrantLock;
2525
import io.sentry.util.SentryRandom;
2626
import java.io.File;
27-
import java.io.IOException;
2827
import java.util.ArrayList;
2928
import java.util.HashMap;
3029
import java.util.List;
@@ -52,13 +51,12 @@ public final class JavaContinuousProfiler
5251
private @Nullable Future<?> stopFuture;
5352
private final @NotNull List<ProfileChunk.Builder> payloadBuilders = new ArrayList<>();
5453
private @NotNull SentryId profilerId = SentryId.EMPTY_ID;
55-
private @NotNull SentryId chunkId = SentryId.EMPTY_ID;
5654
private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false);
5755
private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate();
5856

5957
private @NotNull String filename = "";
6058

61-
private final @NotNull AsyncProfiler profiler;
59+
private @Nullable AsyncProfiler profiler;
6260
private volatile boolean shouldSample = true;
6361
private boolean shouldStop = false;
6462
private boolean isSampled = false;
@@ -76,21 +74,52 @@ public JavaContinuousProfiler(
7674
this.profilingTracesDirPath = profilingTracesDirPath;
7775
this.profilingTracesHz = profilingTracesHz;
7876
this.executorService = executorService;
79-
this.profiler = AsyncProfiler.getInstance();
77+
initializeProfiler();
78+
}
79+
80+
private void initializeProfiler() {
81+
try {
82+
this.profiler = AsyncProfiler.getInstance();
83+
// Check version to verify profiler is working
84+
String version = profiler.execute("version");
85+
logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version);
86+
} catch (Exception e) {
87+
logger.log(
88+
SentryLevel.WARNING,
89+
"Failed to initialize AsyncProfiler. Profiling will be disabled.",
90+
e);
91+
this.profiler = null;
92+
}
8093
}
8194

8295
private boolean init() {
83-
// We initialize it only once
8496
if (isInitialized) {
8597
return true;
8698
}
8799
isInitialized = true;
100+
101+
if (profiler == null) {
102+
logger.log(SentryLevel.ERROR, "Disabling profiling because AsyncProfiler is not available.");
103+
return false;
104+
}
105+
88106
if (profilingTracesDirPath == null) {
89107
logger.log(
90108
SentryLevel.WARNING,
91109
"Disabling profiling because no profiling traces dir path is defined in options.");
92110
return false;
93111
}
112+
113+
File profileDir = new File(profilingTracesDirPath);
114+
115+
if (!profileDir.canWrite() || !profileDir.exists()) {
116+
logger.log(
117+
SentryLevel.WARNING,
118+
"Disabling profiling because traces directory is not writable or does not exist: %s",
119+
profilingTracesDirPath);
120+
return false;
121+
}
122+
94123
if (profilingTracesHz <= 0) {
95124
logger.log(
96125
SentryLevel.WARNING,
@@ -101,7 +130,6 @@ private boolean init() {
101130
return true;
102131
}
103132

104-
@SuppressWarnings("ReferenceEquality")
105133
@Override
106134
public void startProfiler(
107135
final @NotNull ProfileLifecycle profileLifecycle,
@@ -138,6 +166,7 @@ public void startProfiler(
138166
}
139167

140168
if (!isRunning()) {
169+
shouldStop = false;
141170
logger.log(SentryLevel.DEBUG, "Started Profiler.");
142171
start();
143172
}
@@ -159,7 +188,6 @@ private void initScopes() {
159188
private void start() {
160189
initScopes();
161190

162-
// Let's initialize trace folder and profiling interval
163191
if (!init()) {
164192
return;
165193
}
@@ -179,20 +207,31 @@ private void start() {
179207
} else {
180208
startProfileChunkTimestamp = new SentryNanotimeDate();
181209
}
210+
211+
if (profiler == null) {
212+
logger.log(SentryLevel.ERROR, "Cannot start profiling: AsyncProfiler is not available");
213+
return;
214+
}
215+
182216
filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr";
183-
String startData = null;
217+
218+
File jfrFile = new File(filename);
219+
184220
try {
185221
final String profilingIntervalMicros =
186222
String.format("%dus", (int) SECONDS.toMicros(1) / profilingTracesHz);
187223
final String command =
188224
String.format("start,jfr,wall=%s,file=%s", profilingIntervalMicros, filename);
189-
System.out.println(command);
190-
startData = profiler.execute(command);
225+
226+
profiler.execute(command);
227+
191228
} catch (Exception e) {
192229
logger.log(SentryLevel.ERROR, "Failed to start profiling: ", e);
193-
}
194-
// check if profiling started
195-
if (startData == null) {
230+
filename = "";
231+
// Try to clean up the file if it was created
232+
if (jfrFile.exists()) {
233+
jfrFile.delete();
234+
}
196235
return;
197236
}
198237

@@ -202,10 +241,6 @@ private void start() {
202241
profilerId = new SentryId();
203242
}
204243

205-
if (chunkId == SentryId.EMPTY_ID) {
206-
chunkId = new SentryId();
207-
}
208-
209244
try {
210245
stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS);
211246
} catch (RejectedExecutionException e) {
@@ -249,45 +284,57 @@ private void stop(final boolean restartProfiler) {
249284
// check if profiler was created and it's running
250285
if (!isRunning) {
251286
// When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
252-
// ids
287+
// id
253288
profilerId = SentryId.EMPTY_ID;
254-
chunkId = SentryId.EMPTY_ID;
255289
return;
256290
}
257291

258-
String endData = null;
292+
File jfrFile = new File(filename);
293+
294+
if (profiler == null) {
295+
logger.log(SentryLevel.WARNING, "Profiler is null when trying to stop");
296+
return;
297+
}
298+
259299
try {
260-
endData = profiler.execute("stop,jfr");
261-
} catch (IOException e) {
262-
throw new RuntimeException(e);
300+
profiler.execute("stop,jfr");
301+
} catch (Exception e) {
302+
logger.log(SentryLevel.ERROR, "Error stopping profiler, attempting cleanup: ", e);
303+
// Clean up file if it exists
304+
if (jfrFile.exists()) {
305+
jfrFile.delete();
306+
}
263307
}
264308

265-
// check if profiler end successfully
266-
if (endData == null) {
267-
logger.log(
268-
SentryLevel.ERROR,
269-
"An error occurred while collecting a profile chunk, and it won't be sent.");
270-
} else {
271-
// The scopes can be null if the profiler is started before the SDK is initialized (app
272-
// start profiling), meaning there's no scopes to send the chunks. In that case, we store
273-
// the data in a list and send it when the next chunk is finished.
309+
// The scopes can be null if the profiler is started before the SDK is initialized (app
310+
// start profiling), meaning there's no scopes to send the chunks. In that case, we store
311+
// the data in a list and send it when the next chunk is finished.
312+
if (jfrFile.exists() && jfrFile.canRead() && jfrFile.length() > 0) {
274313
try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) {
275-
File jfrFile = new File(filename);
276314
jfrFile.deleteOnExit();
277315
payloadBuilders.add(
278316
new ProfileChunk.Builder(
279317
profilerId,
280-
chunkId,
318+
new SentryId(),
281319
new HashMap<>(),
282320
jfrFile,
283321
startProfileChunkTimestamp,
284-
ProfileChunk.PLATFORM_ANDROID));
322+
ProfileChunk.PLATFORM_JAVA));
323+
}
324+
} else {
325+
logger.log(
326+
SentryLevel.WARNING,
327+
"JFR file is invalid or empty: exists=%b, readable=%b, size=%d",
328+
jfrFile.exists(),
329+
jfrFile.canRead(),
330+
jfrFile.length());
331+
if (jfrFile.exists()) {
332+
jfrFile.delete();
285333
}
286334
}
287335

336+
// Always clean up state, even if stop failed
288337
isRunning = false;
289-
// A chunk is finished. Next chunk will have a different id.
290-
chunkId = SentryId.EMPTY_ID;
291338
filename = "";
292339

293340
if (scopes != null) {
@@ -356,7 +403,9 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti
356403

357404
@Override
358405
public boolean isRunning() {
359-
return isRunning;
406+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
407+
return isRunning && profiler != null && !filename.isEmpty();
408+
}
360409
}
361410

362411
@VisibleForTesting

sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import kotlin.test.Test
2020
import kotlin.test.assertEquals
2121
import kotlin.test.assertFalse
2222
import kotlin.test.assertTrue
23+
import org.mockito.ArgumentMatchers.startsWith
2324
import org.mockito.Mockito
2425
import org.mockito.kotlin.any
2526
import org.mockito.kotlin.eq
@@ -274,7 +275,13 @@ class JavaContinuousProfilerTest {
274275
fixture.executor.runAll()
275276
// We assert that no trace files are written
276277
assertTrue(File(fixture.options.profilingTracesDirPath!!).list()!!.isEmpty())
277-
verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to start profiling: "), any())
278+
val expectedPath = fixture.options.profilingTracesDirPath
279+
verify(fixture.mockLogger)
280+
.log(
281+
eq(SentryLevel.WARNING),
282+
eq("Disabling profiling because traces directory is not writable or does not exist: %s"),
283+
eq(expectedPath),
284+
)
278285
}
279286

280287
@Test
@@ -324,7 +331,6 @@ class JavaContinuousProfilerTest {
324331
@Test
325332
fun `close without terminating stops all profiles after chunk is finished`() {
326333
val profiler = fixture.getSut()
327-
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
328334
profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler)
329335
assertTrue(profiler.isRunning)
330336
// We are scheduling the profiler to stop at the end of the chunk, so it should still be running
@@ -338,6 +344,24 @@ class JavaContinuousProfilerTest {
338344
assertFalse(profiler.isRunning)
339345
}
340346

347+
@Test
348+
fun `profiler can be stopped and restarted`() {
349+
val profiler = fixture.getSut()
350+
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
351+
assertTrue(profiler.isRunning)
352+
353+
profiler.stopProfiler(ProfileLifecycle.MANUAL)
354+
fixture.executor.runAll()
355+
assertFalse(profiler.isRunning)
356+
357+
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
358+
fixture.executor.runAll()
359+
360+
assertTrue(profiler.isRunning)
361+
verify(fixture.mockLogger, never())
362+
.log(eq(SentryLevel.WARNING), startsWith("JFR file is invalid or empty"), any(), any(), any())
363+
}
364+
341365
@Test
342366
fun `profiler does not send chunks after close`() {
343367
val profiler = fixture.getSut()

0 commit comments

Comments
 (0)