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 ;
@@ -39,10 +37,10 @@ final class AndroidTransactionProfiler implements ITransactionProfiler {
3937 private final @ NotNull ISentryExecutorService executorService ;
4038 private final @ NotNull BuildInfoProvider buildInfoProvider ;
4139 private boolean isInitialized = false ;
42- private int transactionsCounter = 0 ;
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 ;
@@ -95,6 +93,7 @@ private void init() {
9593 return ;
9694 }
9795 isInitialized = true ;
96+
9897 if (!isProfilingEnabled ) {
9998 logger .log (SentryLevel .INFO , "Profiling is disabled in options." );
10099 return ;
@@ -124,22 +123,30 @@ private void init() {
124123
125124 @ Override
126125 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 ;
126+ // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
127+ // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
128+ if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return ;
131129
130+ // When the first transaction is starting, we can start profiling
131+ if (!isRunning .getAndSet (true )) {
132132 // Let's initialize trace folder and profiling interval
133133 init ();
134134
135- transactionsCounter ++;
136- // When the first transaction is starting, we can start profiling
137- if (transactionsCounter == 1 && onFirstStart ()) {
135+ if (onFirstStart ()) {
138136 logger .log (SentryLevel .DEBUG , "Profiler started." );
139137 } else {
140- transactionsCounter --;
141- logger .log (
142- SentryLevel .WARNING , "A profile is already running. This profile will be ignored." );
138+ // If profiler is not null and is running, it means that a profile is already running
139+ if (profiler != null && profiler .isRunning ()) {
140+ logger .log (
141+ SentryLevel .WARNING , "A profile is already running. This profile will be ignored." );
142+ } else {
143+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
144+ // Ensure we unbind any transaction data, just in case of concurrent starts
145+ currentProfilingTransactionData = null ;
146+ }
147+ // Otherwise we update the flag, because it means the profiler is not running
148+ isRunning .set (false );
149+ }
143150 }
144151 }
145152 }
@@ -164,11 +171,14 @@ private boolean onFirstStart() {
164171
165172 @ Override
166173 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 );
174+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
175+ if (isRunning .get () && currentProfilingTransactionData == null ) {
176+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
177+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
178+ if (isRunning .get () && currentProfilingTransactionData == null ) {
179+ currentProfilingTransactionData =
180+ new ProfilingTransactionData (transaction , profileStartNanos , profileStartCpuMillis );
181+ }
172182 }
173183 }
174184 }
@@ -178,15 +188,13 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
178188 final @ NotNull ITransaction transaction ,
179189 final @ Nullable List <PerformanceCollectionData > performanceCollectionData ,
180190 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- }
191+ return onTransactionFinish (
192+ transaction .getName (),
193+ transaction .getEventId ().toString (),
194+ transaction .getSpanContext ().getTraceId ().toString (),
195+ false ,
196+ performanceCollectionData ,
197+ options );
190198 }
191199
192200 @ SuppressLint ("NewApi" )
@@ -197,20 +205,23 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
197205 final boolean isTimeout ,
198206 final @ Nullable List <PerformanceCollectionData > performanceCollectionData ,
199207 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- }
205208
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 ;
209+ // onTransactionStart() is only available since Lollipop_MR1
210+ // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
211+ // and SUPPORTED_ABIS since KITKAT
212+ if (buildInfoProvider .getSdkInfoVersion () < Build .VERSION_CODES .LOLLIPOP_MR1 ) return null ;
213+
214+ // check if profiler was created
215+ if (profiler == null ) {
216+ return null ;
217+ }
218+
219+ final ProfilingTransactionData txData ;
220+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
221+ txData = currentProfilingTransactionData ;
210222
211223 // Transaction finished, but it's not in the current profile
212- if (currentProfilingTransactionData == null
213- || !currentProfilingTransactionData .getId ().equals (transactionId )) {
224+ if (txData == null || !txData .getId ().equals (transactionId )) {
214225 // A transaction is finishing, but it's not profiled. We can skip it
215226 logger .log (
216227 SentryLevel .INFO ,
@@ -219,118 +230,90 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
219230 traceId );
220231 return null ;
221232 }
233+ currentProfilingTransactionData = null ;
234+ }
222235
223- if (transactionsCounter > 0 ) {
224- transactionsCounter --;
225- }
226-
227- logger .log (SentryLevel .DEBUG , "Transaction %s (%s) finished." , transactionName , traceId );
236+ logger .log (SentryLevel .DEBUG , "Transaction %s (%s) finished." , transactionName , traceId );
228237
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- }
240-
241- final AndroidProfiler .ProfileEndData endData =
242- profiler .endAndCollect (false , performanceCollectionData );
243- // check if profiler end successfully
244- if (endData == null ) {
245- return null ;
246- }
238+ final AndroidProfiler .ProfileEndData endData =
239+ profiler .endAndCollect (false , performanceCollectionData );
247240
248- long transactionDurationNanos = endData . endNanos - profileStartNanos ;
241+ isRunning . set ( false ) ;
249242
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 ;
243+ // check if profiler end successfully
244+ if (endData == null ) {
245+ return null ;
246+ }
258247
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 ;
248+ long transactionDurationNanos = endData .endNanos - profileStartNanos ;
268249
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- }
250+ final @ NotNull List <ProfilingTransactionData > transactionList = new ArrayList <>(1 );
251+ transactionList .add (txData );
252+ txData .notifyFinish (
253+ endData .endNanos , profileStartNanos , endData .endCpuMillis , profileStartCpuMillis );
275254
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 );
255+ String totalMem = "0" ;
256+ final @ Nullable Long memory =
257+ (options instanceof SentryAndroidOptions )
258+ ? DeviceInfoUtil .getInstance (context , (SentryAndroidOptions ) options ).getTotalMemory ()
259+ : null ;
260+ if (memory != null ) {
261+ totalMem = Long .toString (memory );
301262 }
263+ final String [] abis = Build .SUPPORTED_ABIS ;
264+
265+ // cpu max frequencies are read with a lambda because reading files is involved, so it will be
266+ // done in the background when the trace file is read
267+ return new ProfilingTraceData (
268+ endData .traceFile ,
269+ profileStartTimestamp ,
270+ transactionList ,
271+ transactionName ,
272+ transactionId ,
273+ traceId ,
274+ Long .toString (transactionDurationNanos ),
275+ buildInfoProvider .getSdkInfoVersion (),
276+ abis != null && abis .length > 0 ? abis [0 ] : "" ,
277+ () -> CpuInfoUtils .getInstance ().readMaxFrequencies (),
278+ buildInfoProvider .getManufacturer (),
279+ buildInfoProvider .getModel (),
280+ buildInfoProvider .getVersionRelease (),
281+ buildInfoProvider .isEmulator (),
282+ totalMem ,
283+ options .getProguardUuid (),
284+ options .getRelease (),
285+ options .getEnvironment (),
286+ (endData .didTimeout || isTimeout )
287+ ? ProfilingTraceData .TRUNCATION_REASON_TIMEOUT
288+ : ProfilingTraceData .TRUNCATION_REASON_NORMAL ,
289+ endData .measurementsMap );
302290 }
303291
304292 @ Override
305293 public boolean isRunning () {
306- return transactionsCounter != 0 ;
294+ return isRunning . get () ;
307295 }
308296
309297 @ Override
310298 public void close () {
299+ final @ Nullable ProfilingTransactionData txData = currentProfilingTransactionData ;
311300 // we stop profiling
312- if (currentProfilingTransactionData != null ) {
301+ if (txData != null ) {
313302 onTransactionFinish (
314- currentProfilingTransactionData .getName (),
315- currentProfilingTransactionData .getId (),
316- currentProfilingTransactionData .getTraceId (),
303+ txData .getName (),
304+ txData .getId (),
305+ txData .getTraceId (),
317306 true ,
318307 null ,
319308 ScopesAdapter .getInstance ().getOptions ());
320- } else if (transactionsCounter != 0 ) {
321- // 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 --;
324309 }
310+ // in case the app start profiling is running, and it's not bound to a transaction, we still
311+ // stop profiling, but we also have to manually update the flag.
312+ isRunning .set (false );
325313
326314 // we have to first stop profiling otherwise we would lost the last profile
327315 if (profiler != null ) {
328316 profiler .close ();
329317 }
330318 }
331-
332- @ TestOnly
333- int getTransactionsCounter () {
334- return transactionsCounter ;
335- }
336319}
0 commit comments