Skip to content

Commit

Permalink
Handle App Start Continuous Profiling v8 (p4) (#3730)
Browse files Browse the repository at this point in the history
* create app start continuous profiler instead of transaction profiler, based on config
* updated SentryAppStartProfilingOptions with isContinuousProfilingEnabled flag
* updated SentryOptions with isContinuousProfilingEnabled() method
* cut profiler setup out in a specific function to improve readability of AndroidOptionsInitializer

Add new APIs for Continuous Profiling v8 (p5) (#3844)
* AndroidContinuousProfiler now retrieve the scopes on start()
* removed profilesSampleRate from sample app to enable continuous profiling
* added Sentry.startProfiler and Sentry.stopProfiler APIs
  • Loading branch information
stefanosiano authored Nov 14, 2024
1 parent 9233f8a commit c0a7fd1
Show file tree
Hide file tree
Showing 31 changed files with 618 additions and 91 deletions.
3 changes: 2 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti
public fun close ()V
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
public fun isRunning ()Z
public fun setScopes (Lio/sentry/IScopes;)V
public fun start ()V
public fun stop ()V
}
Expand Down Expand Up @@ -448,6 +447,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
public fun clear ()V
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
Expand All @@ -466,6 +466,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
public fun setAppLaunchedInForeground (Z)V
public fun setAppStartContinuousProfiler (Lio/sentry/IContinuousProfiler;)V
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import io.sentry.ILogger;
import io.sentry.IScopes;
import io.sentry.ISentryExecutorService;
import io.sentry.NoOpScopes;
import io.sentry.PerformanceCollectionData;
import io.sentry.ProfileChunk;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
Expand Down Expand Up @@ -88,12 +90,14 @@ private void init() {
logger);
}

public synchronized void setScopes(final @NotNull IScopes scopes) {
this.scopes = scopes;
this.performanceCollector = scopes.getOptions().getCompositePerformanceCollector();
}

public synchronized void start() {
if ((scopes == null || scopes != NoOpScopes.getInstance())
&& Sentry.getCurrentScopes() != NoOpScopes.getInstance()) {
this.scopes = Sentry.getCurrentScopes();
this.performanceCollector =
Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector();
}

// Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
// causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import android.content.pm.PackageInfo;
import io.sentry.DeduplicateMultithreadedEventProcessor;
import io.sentry.DefaultCompositePerformanceCollector;
import io.sentry.IContinuousProfiler;
import io.sentry.ILogger;
import io.sentry.ISentryLifecycleToken;
import io.sentry.ITransactionProfiler;
Expand Down Expand Up @@ -158,43 +159,26 @@ static void initializeIntegrationsAndProcessors(
options.addEventProcessor(new AnrV2EventProcessor(context, options, buildInfoProvider));
options.setTransportGate(new AndroidTransportGate(options));

if (options.isProfilingEnabled()) {
options.setContinuousProfiler(NoOpContinuousProfiler.getInstance());
// Check if the profiler was already instantiated in the app start.
// We use the Android profiler, that uses a global start/stop api, so we need to preserve the
// state of the profiler, and it's only possible retaining the instance.
try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) {
final @Nullable ITransactionProfiler appStartProfiler =
AppStartMetrics.getInstance().getAppStartProfiler();
if (appStartProfiler != null) {
options.setTransactionProfiler(appStartProfiler);
AppStartMetrics.getInstance().setAppStartProfiler(null);
} else {
options.setTransactionProfiler(
new AndroidTransactionProfiler(
context,
options,
buildInfoProvider,
Objects.requireNonNull(
options.getFrameMetricsCollector(),
"options.getFrameMetricsCollector is required")));
}
}
} else {
options.setTransactionProfiler(NoOpTransactionProfiler.getInstance());
// todo handle app start continuous profiler
options.setContinuousProfiler(
new AndroidContinuousProfiler(
buildInfoProvider,
Objects.requireNonNull(
options.getFrameMetricsCollector(),
"options.getFrameMetricsCollector is required"),
options.getLogger(),
options.getProfilingTracesDirPath(),
options.getProfilingTracesHz(),
options.getExecutorService()));
// Check if the profiler was already instantiated in the app start.
// We use the Android profiler, that uses a global start/stop api, so we need to preserve the
// state of the profiler, and it's only possible retaining the instance.
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
final @Nullable ITransactionProfiler appStartTransactionProfiler;
final @Nullable IContinuousProfiler appStartContinuousProfiler;
try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) {
appStartTransactionProfiler = appStartMetrics.getAppStartProfiler();
appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler();
appStartMetrics.setAppStartProfiler(null);
appStartMetrics.setAppStartContinuousProfiler(null);
}

setupProfiler(
options,
context,
buildInfoProvider,
appStartTransactionProfiler,
appStartContinuousProfiler);

options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger()));

Expand Down Expand Up @@ -252,6 +236,56 @@ static void initializeIntegrationsAndProcessors(
}
}

/** Setup the correct profiler (transaction or continuous) based on the options. */
private static void setupProfiler(
final @NotNull SentryAndroidOptions options,
final @NotNull Context context,
final @NotNull BuildInfoProvider buildInfoProvider,
final @Nullable ITransactionProfiler appStartTransactionProfiler,
final @Nullable IContinuousProfiler appStartContinuousProfiler) {
if (options.isProfilingEnabled() || options.getProfilesSampleRate() != null) {
options.setContinuousProfiler(NoOpContinuousProfiler.getInstance());
// This is a safeguard, but it should never happen, as the app start profiler should be the
// continuous one.
if (appStartContinuousProfiler != null) {
appStartContinuousProfiler.close();
}
if (appStartTransactionProfiler != null) {
options.setTransactionProfiler(appStartTransactionProfiler);
} else {
options.setTransactionProfiler(
new AndroidTransactionProfiler(
context,
options,
buildInfoProvider,
Objects.requireNonNull(
options.getFrameMetricsCollector(),
"options.getFrameMetricsCollector is required")));
}
} else {
options.setTransactionProfiler(NoOpTransactionProfiler.getInstance());
// This is a safeguard, but it should never happen, as the app start profiler should be the
// transaction one.
if (appStartTransactionProfiler != null) {
appStartTransactionProfiler.close();
}
if (appStartContinuousProfiler != null) {
options.setContinuousProfiler(appStartContinuousProfiler);
} else {
options.setContinuousProfiler(
new AndroidContinuousProfiler(
buildInfoProvider,
Objects.requireNonNull(
options.getFrameMetricsCollector(),
"options.getFrameMetricsCollector is required"),
options.getLogger(),
options.getProfilingTracesDirPath(),
options.getProfilingTracesHz(),
options.getExecutorService()));
}
}
}

static void installDefaultIntegrations(
final @NotNull Context context,
final @NotNull SentryAndroidOptions options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import android.os.Process;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import io.sentry.IContinuousProfiler;
import io.sentry.ILogger;
import io.sentry.ISentryLifecycleToken;
import io.sentry.ITransactionProfiler;
Expand Down Expand Up @@ -100,6 +101,11 @@ public void shutdown() {
if (appStartProfiler != null) {
appStartProfiler.close();
}
final @Nullable IContinuousProfiler appStartContinuousProfiler =
AppStartMetrics.getInstance().getAppStartContinuousProfiler();
if (appStartContinuousProfiler != null) {
appStartContinuousProfiler.close();
}
}
}

Expand Down Expand Up @@ -132,40 +138,18 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri
return;
}

if (profilingOptions.isContinuousProfilingEnabled()) {
createAndStartContinuousProfiler(context, profilingOptions, appStartMetrics);
return;
}

if (!profilingOptions.isProfilingEnabled()) {
logger.log(
SentryLevel.INFO, "Profiling is not enabled. App start profiling will not start.");
return;
}

final @NotNull TracesSamplingDecision appStartSamplingDecision =
new TracesSamplingDecision(
profilingOptions.isTraceSampled(),
profilingOptions.getTraceSampleRate(),
profilingOptions.isProfileSampled(),
profilingOptions.getProfileSampleRate());
// We store any sampling decision, so we can respect it when the first transaction starts
appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision);

if (!(appStartSamplingDecision.getProfileSampled()
&& appStartSamplingDecision.getSampled())) {
logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start.");
return;
}
logger.log(SentryLevel.DEBUG, "App start profiling started.");

final @NotNull ITransactionProfiler appStartProfiler =
new AndroidTransactionProfiler(
context,
buildInfoProvider,
new SentryFrameMetricsCollector(context, logger, buildInfoProvider),
logger,
profilingOptions.getProfilingTracesDirPath(),
profilingOptions.isProfilingEnabled(),
profilingOptions.getProfilingTracesHz(),
new SentryExecutorService());
appStartMetrics.setAppStartProfiler(appStartProfiler);
appStartProfiler.start();
createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics);

} catch (FileNotFoundException e) {
logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e);
Expand All @@ -174,6 +158,59 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri
}
}

private void createAndStartContinuousProfiler(
final @NotNull Context context,
final @NotNull SentryAppStartProfilingOptions profilingOptions,
final @NotNull AppStartMetrics appStartMetrics) {
final @NotNull IContinuousProfiler appStartContinuousProfiler =
new AndroidContinuousProfiler(
buildInfoProvider,
new SentryFrameMetricsCollector(
context.getApplicationContext(), logger, buildInfoProvider),
logger,
profilingOptions.getProfilingTracesDirPath(),
profilingOptions.getProfilingTracesHz(),
new SentryExecutorService());
appStartMetrics.setAppStartProfiler(null);
appStartMetrics.setAppStartContinuousProfiler(appStartContinuousProfiler);
logger.log(SentryLevel.DEBUG, "App start continuous profiling started.");
appStartContinuousProfiler.start();
}

private void createAndStartTransactionProfiler(
final @NotNull Context context,
final @NotNull SentryAppStartProfilingOptions profilingOptions,
final @NotNull AppStartMetrics appStartMetrics) {
final @NotNull TracesSamplingDecision appStartSamplingDecision =
new TracesSamplingDecision(
profilingOptions.isTraceSampled(),
profilingOptions.getTraceSampleRate(),
profilingOptions.isProfileSampled(),
profilingOptions.getProfileSampleRate());
// We store any sampling decision, so we can respect it when the first transaction starts
appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision);

if (!(appStartSamplingDecision.getProfileSampled() && appStartSamplingDecision.getSampled())) {
logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start.");
return;
}

final @NotNull ITransactionProfiler appStartProfiler =
new AndroidTransactionProfiler(
context,
buildInfoProvider,
new SentryFrameMetricsCollector(context, logger, buildInfoProvider),
logger,
profilingOptions.getProfilingTracesDirPath(),
profilingOptions.isProfilingEnabled(),
profilingOptions.getProfilingTracesHz(),
new SentryExecutorService());
appStartMetrics.setAppStartContinuousProfiler(null);
appStartMetrics.setAppStartProfiler(appStartProfiler);
logger.log(SentryLevel.DEBUG, "App start profiling started.");
appStartProfiler.start();
}

@SuppressLint("NewApi")
private void onAppLaunched(
final @Nullable Context context, final @NotNull AppStartMetrics appStartMetrics) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.sentry.IContinuousProfiler;
import io.sentry.ISentryLifecycleToken;
import io.sentry.ITransactionProfiler;
import io.sentry.SentryDate;
Expand Down Expand Up @@ -57,6 +58,7 @@ public enum AppStartType {
private final @NotNull Map<ContentProvider, TimeSpan> contentProviderOnCreates;
private final @NotNull List<ActivityLifecycleTimeSpan> activityLifecycles;
private @Nullable ITransactionProfiler appStartProfiler = null;
private @Nullable IContinuousProfiler appStartContinuousProfiler = null;
private @Nullable TracesSamplingDecision appStartSamplingDecision = null;
private @Nullable SentryDate onCreateTime = null;
private boolean appLaunchTooLong = false;
Expand Down Expand Up @@ -186,6 +188,10 @@ public void clear() {
appStartProfiler.close();
}
appStartProfiler = null;
if (appStartContinuousProfiler != null) {
appStartContinuousProfiler.close();
}
appStartContinuousProfiler = null;
appStartSamplingDecision = null;
appLaunchTooLong = false;
appLaunchedInForeground = false;
Expand All @@ -201,6 +207,15 @@ public void setAppStartProfiler(final @Nullable ITransactionProfiler appStartPro
this.appStartProfiler = appStartProfiler;
}

public @Nullable IContinuousProfiler getAppStartContinuousProfiler() {
return appStartContinuousProfiler;
}

public void setAppStartContinuousProfiler(
final @Nullable IContinuousProfiler appStartContinuousProfiler) {
this.appStartContinuousProfiler = appStartContinuousProfiler;
}

public void setAppStartSamplingDecision(
final @Nullable TracesSamplingDecision appStartSamplingDecision) {
this.appStartSamplingDecision = appStartSamplingDecision;
Expand Down Expand Up @@ -259,11 +274,15 @@ private void checkCreateTimeOnMain(final @NotNull Application application) {
if (onCreateTime == null) {
appLaunchedInForeground = false;

// we stop the app start profiler, as it's useless and likely to timeout
// we stop the app start profilers, as they are useless and likely to timeout
if (appStartProfiler != null && appStartProfiler.isRunning()) {
appStartProfiler.close();
appStartProfiler = null;
}
if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) {
appStartContinuousProfiler.close();
appStartContinuousProfiler = null;
}
}
application.unregisterActivityLifecycleCallbacks(instance);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.sentry.IScopes
import io.sentry.ISentryExecutorService
import io.sentry.MemoryCollectionData
import io.sentry.PerformanceCollectionData
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.SentryNanotimeDate
import io.sentry.SentryTracer
Expand Down Expand Up @@ -80,7 +81,7 @@ class AndroidContinuousProfilerTest {
options.profilingTracesDirPath,
options.profilingTracesHz,
options.executorService
).also { it.setScopes(scopes) }
)
}
}

Expand Down Expand Up @@ -118,6 +119,8 @@ class AndroidContinuousProfilerTest {
// Profiler doesn't start if the folder doesn't exists.
// Usually it's generated when calling Sentry.init, but for tests we can create it manually.
File(fixture.options.profilingTracesDirPath!!).mkdirs()

Sentry.setCurrentScopes(fixture.scopes)
}

@AfterTest
Expand Down
Loading

0 comments on commit c0a7fd1

Please sign in to comment.