Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `globalHubMode` is enabled by default on Android, where tags, extras, contexts and level set inside the callback were silently dropped
- Scopes that are explicitly made current, e.g. via `Sentry.setCurrentScopes` or the `SentryContext` coroutine integration, are now also honoured when `globalHubMode` is enabled
- `Sentry.pushScope`, `Sentry.pushIsolationScope` and `Sentry.popScope` remain no-ops when `globalHubMode` is enabled
- Skip Android profiling when the installed system profiling package delivers empty traces ([#5975](https://github.com/getsentry/sentry-java/pull/5975))

### Internal

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Long> 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();
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Loading