diff --git a/CHANGELOG.md b/CHANGELOG.md index 139a235940..a3e0e159e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Keep UI profiling running when `startProfiler` is called before a requested stop takes effect at the end of the current chunk ([#6291](https://github.com/getsentry/sentry-java/pull/6291)) - Before, the start call was ignored and the profiler still stopped at the end of the chunk, even in `TRACE` mode while sampled spans were active. On API 34 and lower, every later profile also stopped after one chunk. +- Skip Android profiling when the installed system profiling package delivers empty traces ([#5975](https://github.com/getsentry/sentry-java/pull/5975)) ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoProfiler.java index 627a434d6f..0be2926c1e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoProfiler.java @@ -2,6 +2,8 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; @@ -12,6 +14,7 @@ import io.sentry.ISentryExecutorService; import io.sentry.SentryLevel; import io.sentry.android.core.internal.profiling.ChunkRecord; +import io.sentry.android.core.util.AndroidLazyEvaluator; import io.sentry.profiling.ProfileRecordingState; import java.io.File; import java.util.concurrent.RejectedExecutionException; @@ -43,9 +46,29 @@ public class PerfettoProfiler { private static final long RESULT_TIMEOUT_MS = 5000; + /** Name of the APEX that provides {@link ProfilingManager}. */ + private static final String PROFILING_PACKAGE_NAME = "com.google.android.profiling"; + + /** + * This version of the profiling package accepts profiling requests, but delivers empty traces. + */ + private static final long EMPTY_TRACE_PROFILING_PACKAGE_VERSION = 370546200L; + + /** Used when the profiling package is not installed, or its version cannot be read. */ + private static final long UNKNOWN_PROFILING_PACKAGE_VERSION = 0L; + + /** + * A new profiler is created for each profile chunk, but the profiling package cannot change while + * the process runs, as an update of it restarts the app. Thus, we read it only once, to avoid a + * binder call per chunk. + */ + private static final @NotNull AndroidLazyEvaluator profilingPackageVersionEvaluator = + new AndroidLazyEvaluator<>(PerfettoProfiler::resolveProfilingPackageVersion); + private final @NotNull ILogger logger; private final @NotNull ISentryExecutorService executorService; private final @Nullable ProfilingManager profilingManager; + private final long profilingPackageVersion; private final @NotNull CancellationSignal cancellationSignal = new CancellationSignal(); private final @NotNull Object profilingResultLock = new Object(); @@ -64,16 +87,19 @@ public PerfettoProfiler( this( logger, executorService, - (ProfilingManager) context.getSystemService(Context.PROFILING_SERVICE)); + (ProfilingManager) context.getSystemService(Context.PROFILING_SERVICE), + getProfilingPackageVersion(context)); } PerfettoProfiler( final @NotNull ILogger logger, final @NotNull ISentryExecutorService executorService, - final @Nullable ProfilingManager profilingManager) { + final @Nullable ProfilingManager profilingManager, + final long profilingPackageVersion) { this.logger = logger; this.executorService = executorService; this.profilingManager = profilingManager; + this.profilingPackageVersion = profilingPackageVersion; } /** @@ -96,6 +122,14 @@ boolean start(final @NotNull ChunkRecord chunkRecord, final long durationMs) { } this.chunkRecord = chunkRecord; + if (profilingPackageVersion == EMPTY_TRACE_PROFILING_PACKAGE_VERSION) { + logger.log( + SentryLevel.WARNING, + "Android profiling package version %d delivers empty traces. Profiling is disabled.", + profilingPackageVersion); + return false; + } + final Bundle params = new Bundle(); params.putInt(KEY_DURATION_MS, (int) durationMs); params.putInt(KEY_FREQUENCY_HZ, PROFILING_FREQUENCY_HZ); @@ -252,6 +286,26 @@ private void deleteTraceFile(final @Nullable File traceFile) { return traceFile; } + private static long getProfilingPackageVersion(final @NotNull Context context) { + final @Nullable Long version = profilingPackageVersionEvaluator.getValue(context); + return version != null ? version : UNKNOWN_PROFILING_PACKAGE_VERSION; + } + + private static @NotNull Long resolveProfilingPackageVersion(final @NotNull Context context) { + try { + final @NotNull PackageInfo packageInfo = + context + .getPackageManager() + .getPackageInfo( + PROFILING_PACKAGE_NAME, + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX)); + return packageInfo.getLongVersionCode(); + } catch (PackageManager.NameNotFoundException e) { + // The profiling package is not installed on this device + return UNKNOWN_PROFILING_PACKAGE_VERSION; + } + } + private static @NotNull String errorCodeToString(final int errorCode) { switch (errorCode) { case ProfilingResult.ERROR_FAILED_RATE_LIMIT_PROCESS: diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoProfilerTest.kt index a21ca435b6..eea79dceae 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoProfilerTest.kt @@ -28,6 +28,8 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.annotation.Config @@ -58,8 +60,11 @@ class PerfettoProfilerTest { context = ApplicationProvider.getApplicationContext() } - private fun getSut(profilingManager: ProfilingManager? = mockProfilingManager): PerfettoProfiler { - return PerfettoProfiler(mockLogger, executor, profilingManager) + private fun getSut( + profilingManager: ProfilingManager? = mockProfilingManager, + profilingPackageVersion: Long = 0L, + ): PerfettoProfiler { + return PerfettoProfiler(mockLogger, executor, profilingManager, profilingPackageVersion) } private fun PerfettoProfiler.startSession(): ChunkRecord? { @@ -105,6 +110,22 @@ class PerfettoProfilerTest { assertNull(profiler.startSession()) } + @Test + fun `start returns null and does not request profiling for unsupported package version`() { + val profiler = getSut(profilingPackageVersion = 370546200L) + + assertNull(profiler.startSession()) + verify(mockProfilingManager, never()).requestProfiling(any(), any(), any(), any(), any(), any()) + } + + @Test + fun `start requests profiling for other package versions`() { + val profiler = getSut(profilingPackageVersion = 370546201L) + + assertNotNull(profiler.startSession()) + verify(mockProfilingManager).requestProfiling(any(), any(), any(), any(), any(), any()) + } + @Test fun `endAndCollect calls listener with null when never started`() { val profiler = getSut()