Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Android profiling traces #1897

Merged
merged 15 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AndroidTraceTransactionListener : io/sentry/ITransactionListener {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public fun onTransactionEnd (Lio/sentry/ITransaction;)V
public fun onTransactionStart (Lio/sentry/ITransaction;)V
}

public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun close ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ static void init(
options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));

options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
options.setTransactionListener(new AndroidTraceTransactionListener());
}

private static void installDefaultIntegrations(
Expand Down Expand Up @@ -249,7 +250,9 @@ private static void readDefaultOptionValues(
private static void initializeCacheDirs(
final @NotNull Context context, final @NotNull SentryOptions options) {
final File cacheDir = new File(context.getCacheDir(), "sentry");
final File profilingTracesDir = new File(cacheDir, "profiling_traces");
options.setCacheDirPath(cacheDir.getAbsolutePath());
options.setProfilingTracesDirPath(profilingTracesDir.getAbsolutePath());
}

private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package io.sentry.android.core;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

import android.os.Build;
import android.os.Debug;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ITransaction;
import io.sentry.ITransactionListener;
import io.sentry.SentryEnvelope;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.protocol.SentryId;
import io.sentry.util.FileUtils;
import io.sentry.util.Objects;
import io.sentry.util.SentryExecutors;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class AndroidTraceTransactionListener implements ITransactionListener {

/**
* This appears to correspond to the buffer size of the data part of the file, excluding the key
* part. Once the buffer is full, new records are ignored, but the resulting trace file will be
* valid.
*
* <p>30 second traces can require a buffer of a few MB. 8MB is the default buffer size for
* [Debug.startMethodTracingSampling], but 3 should be enough for most cases. We can adjust this
* in the future if we notice that traces are being truncated in some applications.
*/
private static final int BUFFER_SIZE_BYTES = 3_000_000;

private final IHub hub;

private @Nullable File traceFile = null;
private @Nullable File traceFilesDir = null;
private boolean startedMethodTracing = false;
private @Nullable ITransaction activeTransaction = null;

private @NotNull final SentryOptions options;

public AndroidTraceTransactionListener() {
this(HubAdapter.getInstance());
}

public AndroidTraceTransactionListener(IHub hub) {
this.hub = Objects.requireNonNull(hub, "hub is required");
options = hub.getOptions();
final String tracesFilesDirPath = options.getProfilingTracesDirPath();
if (tracesFilesDirPath == null || tracesFilesDirPath.isEmpty()) {
options
.getLogger()
.log(SentryLevel.ERROR, "No profiling traces dir path is defined in options.");
return;
}
traceFilesDir = new File(tracesFilesDirPath);
}

@Override
@SuppressWarnings("FutureReturnValueIgnored")
public synchronized void onTransactionStart(ITransaction transaction) {

// Debug.startMethodTracingSampling() is only available since Lollipop
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;

// Let's be sure to end any running trace
if (activeTransaction != null) onTransactionEnd(activeTransaction);

traceFile = FileUtils.resolve(traceFilesDir, "sentry-" + UUID.randomUUID() + ".trace");

if (traceFile == null) {
options.getLogger().log(SentryLevel.DEBUG, "Could not create a trace file");
return;
}

if (traceFile.exists()) {
options
.getLogger()
.log(SentryLevel.DEBUG, "Trace file already exists: %s", traceFile.getPath());
return;
}

long intervalMs = options.getProfilingTracesIntervalMillis();
if (intervalMs <= 0) {
options
.getLogger()
.log(SentryLevel.DEBUG, "Profiling trace interval is set to %d milliseconds", intervalMs);
return;
}

// We stop the trace after 30 seconds, since such a long trace is very probably a trace
// that will never end due to an error
SentryExecutors.tracingExecutor.schedule(
() -> onTransactionEnd(transaction), 30_000, MILLISECONDS);

startedMethodTracing = true;
int intervalUs = (int) MILLISECONDS.toMicros(intervalMs);
activeTransaction = transaction;
Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a heavy/blocking method? I wonder because we're calling this synchronously from the startTransaction call so it'll happen on the UI thread

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only operation that happens in the main thread (as noted by the strict mode) is the File.exists() method, called directly and from the Debug.startMethodTracingSampling internally.
The actual writing is handled by the system automatically in the background

}

@Override
public synchronized void onTransactionEnd(ITransaction transaction) {
// In case a previous timeout tries to end a newer transaction we simply ignore it
if (transaction != activeTransaction) return;

if (startedMethodTracing) {
startedMethodTracing = false;
activeTransaction = null;

Debug.stopMethodTracing();

if (traceFile == null || !traceFile.exists()) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Trace file %s does not exists",
traceFile == null ? "null" : traceFile.getPath());
return;
}

// todo should I use transaction.getEventId() instead of new SentryId()?
// Or should I add the transaction id as an header to the envelope?
// Or should I simply ignore the transaction entirely (wouldn't make any sense)?
// And how to check if a trace is from a startup?
try {
SentryEnvelope envelope =
SentryEnvelope.from(
new SentryId(),
traceFile.getPath(),
traceFile.getName(),
options.getSdkVersion(),
options.getMaxTraceFileSize(),
true);
hub.captureEnvelope(envelope);
} catch (IOException e) {
options.getLogger().log(SentryLevel.ERROR, "Failed to capture the trace.", e);
// We don't return out of this function here, so we can go on and clean the variables
}
}

if (traceFile != null) traceFile.delete();
traceFile = null;
}
}
35 changes: 35 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ public abstract interface class io/sentry/ITransaction : io/sentry/ISpan {
public abstract fun setRequest (Lio/sentry/protocol/Request;)V
}

public abstract interface class io/sentry/ITransactionListener {
public abstract fun onTransactionEnd (Lio/sentry/ITransaction;)V
public abstract fun onTransactionStart (Lio/sentry/ITransaction;)V
}

public abstract interface class io/sentry/ITransportFactory {
public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport;
}
Expand Down Expand Up @@ -479,6 +484,12 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction {
public fun traceState ()Lio/sentry/TraceState;
}

public final class io/sentry/NoOpTransactionListener : io/sentry/ITransactionListener {
public static fun getInstance ()Lio/sentry/NoOpTransactionListener;
public fun onTransactionEnd (Lio/sentry/ITransaction;)V
public fun onTransactionStart (Lio/sentry/ITransaction;)V
}

public final class io/sentry/NoOpTransportFactory : io/sentry/ITransportFactory {
public fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport;
public static fun getInstance ()Lio/sentry/NoOpTransportFactory;
Expand Down Expand Up @@ -711,6 +722,7 @@ public final class io/sentry/SentryEnvelope {
public fun <init> (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SdkVersion;Ljava/lang/Iterable;)V
public static fun from (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;Lio/sentry/protocol/SdkVersion;)Lio/sentry/SentryEnvelope;
public static fun from (Lio/sentry/ISerializer;Lio/sentry/Session;Lio/sentry/protocol/SdkVersion;)Lio/sentry/SentryEnvelope;
public static fun from (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/SdkVersion;JZ)Lio/sentry/SentryEnvelope;
public fun getHeader ()Lio/sentry/SentryEnvelopeHeader;
public fun getItems ()Ljava/lang/Iterable;
}
Expand All @@ -737,6 +749,7 @@ public final class io/sentry/SentryEnvelopeItem {
public static fun fromAttachment (Lio/sentry/Attachment;J)Lio/sentry/SentryEnvelopeItem;
public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem;
public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem;
public static fun fromTraceFile (Ljava/lang/String;Ljava/lang/String;JZ)Lio/sentry/SentryEnvelopeItem;
public static fun fromUserFeedback (Lio/sentry/ISerializer;Lio/sentry/UserFeedback;)Lio/sentry/SentryEnvelopeItem;
public fun getData ()[B
public fun getEvent (Lio/sentry/ISerializer;)Lio/sentry/SentryEvent;
Expand Down Expand Up @@ -794,7 +807,9 @@ public final class io/sentry/SentryEvent : io/sentry/SentryBaseEvent, io/sentry/
public final class io/sentry/SentryItemType : java/lang/Enum {
public static final field Attachment Lio/sentry/SentryItemType;
public static final field Event Lio/sentry/SentryItemType;
public static final field InteractionTrace Lio/sentry/SentryItemType;
public static final field Session Lio/sentry/SentryItemType;
public static final field SessionTrace Lio/sentry/SentryItemType;
public static final field Transaction Lio/sentry/SentryItemType;
public static final field Unknown Lio/sentry/SentryItemType;
public static final field UserFeedback Lio/sentry/SentryItemType;
Expand Down Expand Up @@ -851,7 +866,10 @@ public class io/sentry/SentryOptions {
public fun getMaxQueueSize ()I
public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize;
public fun getMaxSpans ()I
public fun getMaxTraceFileSize ()J
public fun getOutboxPath ()Ljava/lang/String;
public fun getProfilingTracesDirPath ()Ljava/lang/String;
public fun getProfilingTracesIntervalMillis ()I
public fun getProguardUuid ()Ljava/lang/String;
public fun getProxy ()Lio/sentry/SentryOptions$Proxy;
public fun getReadTimeoutMillis ()I
Expand All @@ -868,6 +886,7 @@ public class io/sentry/SentryOptions {
public fun getTracesSampleRate ()Ljava/lang/Double;
public fun getTracesSampler ()Lio/sentry/SentryOptions$TracesSamplerCallback;
public fun getTracingOrigins ()Ljava/util/List;
public fun getTransactionListener ()Lio/sentry/ITransactionListener;
public fun getTransportFactory ()Lio/sentry/ITransportFactory;
public fun getTransportGate ()Lio/sentry/transport/ITransportGate;
public fun isAttachServerName ()Z
Expand All @@ -882,6 +901,7 @@ public class io/sentry/SentryOptions {
public fun isEnableSessionTracking ()Z
public fun isEnableShutdownHook ()Z
public fun isEnableUncaughtExceptionHandler ()Z
public fun isProfilingEnabled ()Z
public fun isSendDefaultPii ()Z
public fun isTraceSampling ()Z
public fun isTracingEnabled ()Z
Expand Down Expand Up @@ -918,6 +938,10 @@ public class io/sentry/SentryOptions {
public fun setMaxQueueSize (I)V
public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V
public fun setMaxSpans (I)V
public fun setMaxTraceFileSize (J)V
public fun setProfilingEnabled (Z)V
public fun setProfilingTracesDirPath (Ljava/lang/String;)V
public fun setProfilingTracesIntervalMillis (I)V
public fun setProguardUuid (Ljava/lang/String;)V
public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V
public fun setReadTimeoutMillis (I)V
Expand All @@ -935,6 +959,7 @@ public class io/sentry/SentryOptions {
public fun setTraceSampling (Z)V
public fun setTracesSampleRate (Ljava/lang/Double;)V
public fun setTracesSampler (Lio/sentry/SentryOptions$TracesSamplerCallback;)V
public fun setTransactionListener (Lio/sentry/ITransactionListener;)V
public fun setTransportFactory (Lio/sentry/ITransportFactory;)V
public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V
}
Expand Down Expand Up @@ -2041,6 +2066,12 @@ public final class io/sentry/util/ExceptionUtils {
public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable;
}

public final class io/sentry/util/FileUtils {
public fun <init> ()V
public static fun deleteRecursively (Ljava/io/File;)Z
public static fun resolve (Ljava/io/File;Ljava/lang/String;)Ljava/io/File;
}

public final class io/sentry/util/LogUtils {
public fun <init> ()V
public static fun logIfNotFlushable (Lio/sentry/ILogger;Ljava/lang/Object;)V
Expand All @@ -2063,6 +2094,10 @@ public final class io/sentry/util/Platform {
public static fun isJvm ()Z
}

public final class io/sentry/util/SentryExecutors {
public static final field tracingExecutor Ljava/util/concurrent/ScheduledExecutorService;
}

public final class io/sentry/util/StringUtils {
public static fun byteCountToString (J)Ljava/lang/String;
public static fun capitalize (Ljava/lang/String;)Ljava/lang/String;
Expand Down
10 changes: 9 additions & 1 deletion sentry/src/main/java/io/sentry/Hub.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private static void validateOptions(final @NotNull SentryOptions options) {
Objects.requireNonNull(options, "SentryOptions is required.");
if (options.getDsn() == null || options.getDsn().isEmpty()) {
throw new IllegalArgumentException(
"Hub requires a DSN to be instantiated. Considering using the NoOpHub is no DSN is available.");
"Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available.");
}
}

Expand Down Expand Up @@ -658,6 +658,14 @@ public void flush(long timeoutMillis) {
startTimestamp,
waitForChildren,
transactionFinishedCallback);

// todo Should the listener be called only when a transaction is sampled?
// The listener is called only if the transaction exists, as the transaction will needs to
// stop it
if (options.isProfilingEnabled()) {
final ITransactionListener transactionListener = options.getTransactionListener();
transactionListener.onTransactionStart(transaction);
}
}
if (bindToScope) {
configureScope(scope -> scope.setTransaction(transaction));
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/ITransactionListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.sentry;

import org.jetbrains.annotations.ApiStatus;

/** Used for performing operations when a transaction is started or ended. */
@ApiStatus.Internal
public interface ITransactionListener {
void onTransactionStart(ITransaction transaction);

void onTransactionEnd(ITransaction transaction);
}
18 changes: 18 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpTransactionListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry;

public final class NoOpTransactionListener implements ITransactionListener {

private static final NoOpTransactionListener instance = new NoOpTransactionListener();

private NoOpTransactionListener() {}

public static NoOpTransactionListener getInstance() {
return instance;
}

@Override
public void onTransactionStart(ITransaction transaction) {}

@Override
public void onTransactionEnd(ITransaction transaction) {}
}
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.sentry.config.PropertiesProviderFactory;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.util.FileUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
Expand Down Expand Up @@ -230,6 +231,16 @@ private static boolean initConfigurations(final @NotNull SentryOptions options)
options.setEnvelopeDiskCache(EnvelopeCache.create(options));
}

if (options.isProfilingEnabled()
&& options.getProfilingTracesDirPath() != null
&& !options.getProfilingTracesDirPath().isEmpty()) {
// Method trace files are normally deleted at the end of traces, but if that fails for some
// reason we try to clear any old files here.
final File traceFilesDir = new File(options.getProfilingTracesDirPath());
FileUtils.deleteRecursively(traceFilesDir);
traceFilesDir.mkdirs();
}

return true;
}

Expand Down
Loading