55import android .annotation .SuppressLint ;
66import android .content .Context ;
77import android .os .Build ;
8- import android .os .Process ;
9- import android .os .SystemClock ;
108import io .sentry .DateUtils ;
119import io .sentry .ILogger ;
1210import io .sentry .ISentryExecutorService ;
2624import java .util .ArrayList ;
2725import java .util .Date ;
2826import java .util .List ;
27+ import java .util .concurrent .atomic .AtomicBoolean ;
2928import org .jetbrains .annotations .NotNull ;
3029import org .jetbrains .annotations .Nullable ;
31- import org .jetbrains .annotations .TestOnly ;
3230
3331final class AndroidTransactionProfiler implements ITransactionProfiler {
3432 private final @ NotNull Context context ;
@@ -38,11 +36,11 @@ final class AndroidTransactionProfiler implements ITransactionProfiler {
3836 private final int profilingTracesHz ;
3937 private final @ NotNull ISentryExecutorService executorService ;
4038 private final @ NotNull BuildInfoProvider buildInfoProvider ;
41- private boolean isInitialized = false ;
42- private int transactionsCounter = 0 ;
39+ private final @ NotNull AtomicBoolean isInitialized = new AtomicBoolean ( false ) ;
40+ private final @ NotNull AtomicBoolean isRunning = new AtomicBoolean ( false ) ;
4341 private final @ NotNull SentryFrameMetricsCollector frameMetricsCollector ;
4442 private @ Nullable ProfilingTransactionData currentProfilingTransactionData ;
45- private @ Nullable AndroidProfiler profiler = null ;
43+ private volatile @ Nullable AndroidProfiler profiler = null ;
4644 private long profileStartNanos ;
4745 private long profileStartCpuMillis ;
4846 private @ NotNull Date profileStartTimestamp ;
@@ -91,10 +89,9 @@ public AndroidTransactionProfiler(
9189
9290 private void init () {
9391 // We initialize it only once
94- if (isInitialized ) {
92+ if (isInitialized . getAndSet ( true ) ) {
9593 return ;
9694 }
97- isInitialized = true ;
9895 if (!isProfilingEnabled ) {
9996 logger .log (SentryLevel .INFO , "Profiling is disabled in options." );
10097 return ;
@@ -124,22 +121,26 @@ private void init() {
124121
125122 @ Override
126123 public void start () {
127- try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
128- // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
129- // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
130- if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return ;
124+ // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
125+ // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
126+ if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return ;
131127
132- // Let's initialize trace folder and profiling interval
133- init ();
128+ // Let's initialize trace folder and profiling interval
129+ init ();
134130
135- transactionsCounter ++;
136- // When the first transaction is starting, we can start profiling
137- if (transactionsCounter == 1 && onFirstStart ()) {
131+ // When the first transaction is starting, we can start profiling
132+ if (! isRunning . getAndSet ( true )) {
133+ if (onFirstStart ()) {
138134 logger .log (SentryLevel .DEBUG , "Profiler started." );
139135 } else {
140- transactionsCounter --;
141- logger .log (
142- SentryLevel .WARNING , "A profile is already running. This profile will be ignored." );
136+ // If profiler is not null and is running, it means that a profile is already running
137+ if (profiler != null && profiler .isRunning ()) {
138+ logger .log (
139+ SentryLevel .WARNING , "A profile is already running. This profile will be ignored." );
140+ } else {
141+ // Otherwise we update the flag, because it means the profiler is not running
142+ isRunning .set (false );
143+ }
143144 }
144145 }
145146 }
@@ -164,11 +165,14 @@ private boolean onFirstStart() {
164165
165166 @ Override
166167 public void bindTransaction (final @ NotNull ITransaction transaction ) {
167- try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
168- // If the profiler is running, but no profilingTransactionData is set, we bind it here
169- if (transactionsCounter > 0 && currentProfilingTransactionData == null ) {
170- currentProfilingTransactionData =
171- new ProfilingTransactionData (transaction , profileStartNanos , profileStartCpuMillis );
168+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
169+ if (isRunning .get () && currentProfilingTransactionData == null ) {
170+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
171+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
172+ if (isRunning .get () && currentProfilingTransactionData == null ) {
173+ currentProfilingTransactionData =
174+ new ProfilingTransactionData (transaction , profileStartNanos , profileStartCpuMillis );
175+ }
172176 }
173177 }
174178 }
@@ -178,15 +182,13 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
178182 final @ NotNull ITransaction transaction ,
179183 final @ Nullable List <PerformanceCollectionData > performanceCollectionData ,
180184 final @ NotNull SentryOptions options ) {
181- try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
182- return onTransactionFinish (
183- transaction .getName (),
184- transaction .getEventId ().toString (),
185- transaction .getSpanContext ().getTraceId ().toString (),
186- false ,
187- performanceCollectionData ,
188- options );
189- }
185+ return onTransactionFinish (
186+ transaction .getName (),
187+ transaction .getEventId ().toString (),
188+ transaction .getSpanContext ().getTraceId ().toString (),
189+ false ,
190+ performanceCollectionData ,
191+ options );
190192 }
191193
192194 @ SuppressLint ("NewApi" )
@@ -197,20 +199,23 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
197199 final boolean isTimeout ,
198200 final @ Nullable List <PerformanceCollectionData > performanceCollectionData ,
199201 final @ NotNull SentryOptions options ) {
200- try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
201- // check if profiler was created
202- if (profiler == null ) {
203- return null ;
204- }
205202
206- // onTransactionStart() is only available since Lollipop_MR1
207- // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
208- // and SUPPORTED_ABIS since KITKAT
209- if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return null ;
203+ // onTransactionStart() is only available since Lollipop_MR1
204+ // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
205+ // and SUPPORTED_ABIS since KITKAT
206+ if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return null ;
207+
208+ // check if profiler was created
209+ if (profiler == null ) {
210+ return null ;
211+ }
212+
213+ final ProfilingTransactionData txData ;
214+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
215+ txData = currentProfilingTransactionData ;
210216
211217 // Transaction finished, but it's not in the current profile
212- if (currentProfilingTransactionData == null
213- || !currentProfilingTransactionData .getId ().equals (transactionId )) {
218+ if (txData == null || !txData .getId ().equals (transactionId )) {
214219 // A transaction is finishing, but it's not profiled. We can skip it
215220 logger .log (
216221 SentryLevel .INFO ,
@@ -219,118 +224,91 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
219224 traceId );
220225 return null ;
221226 }
227+ currentProfilingTransactionData = null ;
228+ }
222229
223- if (transactionsCounter > 0 ) {
224- transactionsCounter --;
225- }
226-
227- logger .log (SentryLevel .DEBUG , "Transaction %s (%s) finished." , transactionName , traceId );
228-
229- if (transactionsCounter != 0 ) {
230- // We notify the data referring to this transaction that it finished
231- if (currentProfilingTransactionData != null ) {
232- currentProfilingTransactionData .notifyFinish (
233- SystemClock .elapsedRealtimeNanos (),
234- profileStartNanos ,
235- Process .getElapsedCpuTime (),
236- profileStartCpuMillis );
237- }
238- return null ;
239- }
230+ logger .log (SentryLevel .DEBUG , "Transaction %s (%s) finished." , transactionName , traceId );
240231
241- final AndroidProfiler .ProfileEndData endData =
242- profiler .endAndCollect (false , performanceCollectionData );
243- // check if profiler end successfully
244- if (endData == null ) {
245- return null ;
246- }
232+ final AndroidProfiler .ProfileEndData endData =
233+ profiler .endAndCollect (false , performanceCollectionData );
247234
248- long transactionDurationNanos = endData . endNanos - profileStartNanos ;
235+ isRunning . set ( false ) ;
249236
250- List <ProfilingTransactionData > transactionList = new ArrayList <>(1 );
251- final ProfilingTransactionData txData = currentProfilingTransactionData ;
252- if (txData != null ) {
253- transactionList .add (txData );
254- }
255- currentProfilingTransactionData = null ;
256- // We clear the counter in case of a timeout
257- transactionsCounter = 0 ;
237+ // check if profiler end successfully
238+ if (endData == null ) {
239+ return null ;
240+ }
258241
259- String totalMem = "0" ;
260- final @ Nullable Long memory =
261- (options instanceof SentryAndroidOptions )
262- ? DeviceInfoUtil .getInstance (context , (SentryAndroidOptions ) options ).getTotalMemory ()
263- : null ;
264- if (memory != null ) {
265- totalMem = Long .toString (memory );
266- }
267- String [] abis = Build .SUPPORTED_ABIS ;
242+ long transactionDurationNanos = endData .endNanos - profileStartNanos ;
268243
269- // We notify all transactions data that all transactions finished.
270- // Some may not have been really finished, in case of a timeout
271- for (ProfilingTransactionData t : transactionList ) {
272- t .notifyFinish (
273- endData .endNanos , profileStartNanos , endData .endCpuMillis , profileStartCpuMillis );
274- }
244+ final @ NotNull List <ProfilingTransactionData > transactionList = new ArrayList <>(1 );
245+ transactionList .add (txData );
246+ txData .notifyFinish (
247+ endData .endNanos , profileStartNanos , endData .endCpuMillis , profileStartCpuMillis );
275248
276- // cpu max frequencies are read with a lambda because reading files is involved, so it will be
277- // done in the background when the trace file is read
278- return new ProfilingTraceData (
279- endData .traceFile ,
280- profileStartTimestamp ,
281- transactionList ,
282- transactionName ,
283- transactionId ,
284- traceId ,
285- Long .toString (transactionDurationNanos ),
286- buildInfoProvider .getSdkInfoVersion (),
287- abis != null && abis .length > 0 ? abis [0 ] : "" ,
288- () -> CpuInfoUtils .getInstance ().readMaxFrequencies (),
289- buildInfoProvider .getManufacturer (),
290- buildInfoProvider .getModel (),
291- buildInfoProvider .getVersionRelease (),
292- buildInfoProvider .isEmulator (),
293- totalMem ,
294- options .getProguardUuid (),
295- options .getRelease (),
296- options .getEnvironment (),
297- (endData .didTimeout || isTimeout )
298- ? ProfilingTraceData .TRUNCATION_REASON_TIMEOUT
299- : ProfilingTraceData .TRUNCATION_REASON_NORMAL ,
300- endData .measurementsMap );
249+ String totalMem = "0" ;
250+ final @ Nullable Long memory =
251+ (options instanceof SentryAndroidOptions )
252+ ? DeviceInfoUtil .getInstance (context , (SentryAndroidOptions ) options ).getTotalMemory ()
253+ : null ;
254+ if (memory != null ) {
255+ totalMem = Long .toString (memory );
301256 }
257+ final String [] abis = Build .SUPPORTED_ABIS ;
258+
259+ // cpu max frequencies are read with a lambda because reading files is involved, so it will be
260+ // done in the background when the trace file is read
261+ return new ProfilingTraceData (
262+ endData .traceFile ,
263+ profileStartTimestamp ,
264+ transactionList ,
265+ transactionName ,
266+ transactionId ,
267+ traceId ,
268+ Long .toString (transactionDurationNanos ),
269+ buildInfoProvider .getSdkInfoVersion (),
270+ abis != null && abis .length > 0 ? abis [0 ] : "" ,
271+ () -> CpuInfoUtils .getInstance ().readMaxFrequencies (),
272+ buildInfoProvider .getManufacturer (),
273+ buildInfoProvider .getModel (),
274+ buildInfoProvider .getVersionRelease (),
275+ buildInfoProvider .isEmulator (),
276+ totalMem ,
277+ options .getProguardUuid (),
278+ options .getRelease (),
279+ options .getEnvironment (),
280+ (endData .didTimeout || isTimeout )
281+ ? ProfilingTraceData .TRUNCATION_REASON_TIMEOUT
282+ : ProfilingTraceData .TRUNCATION_REASON_NORMAL ,
283+ endData .measurementsMap );
302284 }
303285
304286 @ Override
305287 public boolean isRunning () {
306- return transactionsCounter != 0 ;
288+ return isRunning . get () ;
307289 }
308290
309291 @ Override
310292 public void close () {
293+ final @ Nullable ProfilingTransactionData txData = currentProfilingTransactionData ;
311294 // we stop profiling
312- if (currentProfilingTransactionData != null ) {
295+ if (txData != null ) {
313296 onTransactionFinish (
314- currentProfilingTransactionData .getName (),
315- currentProfilingTransactionData .getId (),
316- currentProfilingTransactionData .getTraceId (),
297+ txData .getName (),
298+ txData .getId (),
299+ txData .getTraceId (),
317300 true ,
318301 null ,
319302 ScopesAdapter .getInstance ().getOptions ());
320- } else if (transactionsCounter != 0 ) {
303+ } else if (isRunning . get () ) {
321304 // in case the app start profiling is running, and it's not bound to a transaction, we still
322- // stop profiling, but we also have to manually update the counter .
323- transactionsCounter -- ;
305+ // stop profiling, but we also have to manually update the flag .
306+ isRunning . set ( false ) ;
324307 }
325308
326309 // we have to first stop profiling otherwise we would lost the last profile
327310 if (profiler != null ) {
328311 profiler .close ();
329312 }
330313 }
331-
332- @ TestOnly
333- int getTransactionsCounter () {
334- return transactionsCounter ;
335- }
336314}
0 commit comments