diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a45ec3fe3..cc8e5e0969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Skip Android profiling when the installed system profiling package delivers empty traces ([#5975](https://github.com/getsentry/sentry-java/pull/5975)) + ### Dependencies - Bump Native SDK from v0.16.2 to v0.16.3 ([#5962](https://github.com/getsentry/sentry-java/pull/5962)) 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 d09c725269..3bcd6450f6 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; @@ -11,6 +13,7 @@ import io.sentry.ILogger; import io.sentry.ISentryExecutorService; import io.sentry.SentryLevel; +import io.sentry.android.core.util.AndroidLazyEvaluator; import java.io.File; import java.util.concurrent.RejectedExecutionException; import java.util.function.Consumer; @@ -41,9 +44,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(); @@ -60,16 +83,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; } public boolean start(final long durationMs) { @@ -84,6 +110,14 @@ public boolean start(final long durationMs) { return false; } + 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); @@ -216,6 +250,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 0746d36dff..e11f63e79f 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 @@ -23,6 +23,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 @@ -52,8 +54,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 createTraceFile(): File { @@ -94,6 +99,22 @@ class PerfettoProfilerTest { assertFalse(profiler.start(60000)) } + @Test + fun `start returns false and does not request profiling for unsupported package version`() { + val profiler = getSut(profilingPackageVersion = 370546200L) + + assertFalse(profiler.start(60000)) + verify(mockProfilingManager, never()).requestProfiling(any(), any(), any(), any(), any(), any()) + } + + @Test + fun `start requests profiling for other package versions`() { + val profiler = getSut(profilingPackageVersion = 370546201L) + + assertTrue(profiler.start(60000)) + verify(mockProfilingManager).requestProfiling(any(), any(), any(), any(), any(), any()) + } + @Test fun `endAndCollect calls listener with null when never started`() { val profiler = getSut()