-
-
Notifications
You must be signed in to change notification settings - Fork 458
Feature: Android profiling traces #1897
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
Changes from 1 commit
6dcdb4a
69e07de
41613c7
f9dc951
9671886
065221c
39288f6
e2f805d
2fc46f4
1515671
ffa2c21
7830a25
2a01756
1c53581
7d08aca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package io.sentry.android.core; | ||
|
|
||
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
|
|
||
| import android.os.Build; | ||
| import android.os.Debug; | ||
| import io.sentry.ITransaction; | ||
| import io.sentry.ITransactionListener; | ||
| import io.sentry.Sentry; | ||
| import io.sentry.SentryEnvelope; | ||
| import io.sentry.SentryLevel; | ||
| import io.sentry.SentryOptions; | ||
| import io.sentry.android.core.util.FileUtils; | ||
| import io.sentry.protocol.SentryId; | ||
| 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 @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(final @NotNull SentryOptions options) { | ||
| this.options = Objects.requireNonNull(options, "SentryOptions is required"); | ||
| final String tracesFilesDirPath = this.options.getProfilingTracesDirPath(); | ||
| if (tracesFilesDirPath == null || tracesFilesDirPath.isEmpty()) { | ||
| this.options | ||
| .getLogger() | ||
| .log(SentryLevel.ERROR, "No profiling traces dir path is defined in options."); | ||
| return; | ||
| } | ||
|
|
||
| traceFilesDir = new File(tracesFilesDirPath); | ||
| // 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. | ||
| FileUtils.deleteRecursively(traceFilesDir); | ||
| traceFilesDir.mkdirs(); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @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); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| traceFile = FileUtils.resolve(traceFilesDir, "sentry-" + UUID.randomUUID() + ".trace"); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (traceFile == null) { | ||
| options.getLogger().log(SentryLevel.DEBUG, "Could not create a trace file"); | ||
| return; | ||
| } | ||
|
|
||
| if (traceFile.exists()) { | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| options | ||
| .getLogger() | ||
| .log(SentryLevel.DEBUG, "Trace file already exists: %s", traceFile.getPath()); | ||
| return; | ||
| } | ||
|
|
||
| long intervalMs = options.getProfilingTracesIntervalMs(); | ||
| if (intervalMs <= 0) { | ||
| options | ||
| .getLogger() | ||
| .log(SentryLevel.DEBUG, "Profiling trace interval is set to %d milliseconds", intervalMs); | ||
| return; | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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( | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| () -> onTransactionEnd(transaction), 30_000, MILLISECONDS); | ||
|
|
||
| startedMethodTracing = true; | ||
| int intervalUs = (int) MILLISECONDS.toMicros(intervalMs); | ||
| activeTransaction = transaction; | ||
| Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); | ||
|
||
| } | ||
|
|
||
| @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; | ||
|
|
||
| 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); | ||
| Sentry.getCurrentHub().captureEnvelope(envelope); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } catch (IOException e) { | ||
| options.getLogger().log(SentryLevel.ERROR, "Failed to capture session.", e); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (traceFile != null) traceFile.delete(); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| activeTransaction = null; | ||
| traceFile = null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package io.sentry.android.core.util; | ||
|
|
||
| import java.io.File; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| @ApiStatus.Internal | ||
| public final class FileUtils { | ||
|
|
||
| /** | ||
| * Deletes the file or directory denoted by a path. If it is a directory, all files and directory | ||
| * inside it are deleted recursively. Note that if this operation fails then partial deletion may | ||
| * have taken place. | ||
| * | ||
| * @param file file or directory to delete | ||
| * @return true if the file/directory is successfully deleted, false otherwise | ||
| */ | ||
| public static boolean deleteRecursively(@Nullable File file) { | ||
| if (file == null || !file.exists()) { | ||
| return true; | ||
| } | ||
| if (file.isFile()) { | ||
| return file.delete(); | ||
| } | ||
| File[] children = file.listFiles(); | ||
| if (children == null) return true; | ||
| for (File f : children) { | ||
| if (!deleteRecursively(f)) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * If the relative path denotes an absolute path, it is returned back. Otherwise it is returned | ||
| * the file relative to f For instance, `File.resolve(new File("/foo/bar"), "gav")` is `new | ||
| * File("/foo/bar/gav")` While `File.resolve(new File("/foo/bar"), "/gav")` is `new File("/gav")`. | ||
| * | ||
| * @return concatenated this and [relative] paths, or just [relative] if it's absolute. | ||
| */ | ||
| public static @Nullable File resolve(@Nullable File f, @Nullable String relative) { | ||
| if (f == null || relative == null) return null; | ||
| File relativeFile = new File(relative); | ||
| // If relative path is absolute we return the relative file directly | ||
| if (relative.length() > 0 && relative.charAt(0) == File.separatorChar) return relativeFile; | ||
| // Otherwise we return the file relative to the parent f | ||
| return new File(f, relative); | ||
| } | ||
| } |
| 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); | ||
stefanosiano marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.