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

Startup Profiling 2 - Add options and sampling logic #3121

Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -6,6 +6,7 @@

- Handle `monitor`/`check_in` in client reports and rate limiter ([#3096](https://github.com/getsentry/sentry-java/pull/3096))
- Startup profiling 1 - Decouple Profiler from Transaction ([#3101](https://github.com/getsentry/sentry-java/pull/3101))
- Startup profiling 2 - Add options and sampling logic ([#3121](https://github.com/getsentry/sentry-java/pull/3121))
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned in the other PR, maybe let's squash these changelogs into one?


### Fixes

Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2174,6 +2174,7 @@ public class io/sentry/SentryOptions {
public fun isEnableExternalConfiguration ()Z
public fun isEnablePrettySerializationOutput ()Z
public fun isEnableShutdownHook ()Z
public fun isEnableStartupProfiling ()Z
public fun isEnableTimeToFullDisplayTracing ()Z
public fun isEnableUncaughtExceptionHandler ()Z
public fun isEnableUserInteractionBreadcrumbs ()Z
Expand Down Expand Up @@ -2211,6 +2212,7 @@ public class io/sentry/SentryOptions {
public fun setEnableExternalConfiguration (Z)V
public fun setEnablePrettySerializationOutput (Z)V
public fun setEnableShutdownHook (Z)V
public fun setEnableStartupProfiling (Z)V
public fun setEnableTimeToFullDisplayTracing (Z)V
public fun setEnableTracing (Ljava/lang/Boolean;)V
public fun setEnableUncaughtExceptionHandler (Z)V
Expand Down Expand Up @@ -2716,6 +2718,8 @@ public final class io/sentry/TransactionContext : io/sentry/SpanContext {
public fun getParentSampled ()Ljava/lang/Boolean;
public fun getParentSamplingDecision ()Lio/sentry/TracesSamplingDecision;
public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource;
public fun isForNextStartup ()Z
public fun setForNextStartup (Z)V
public fun setInstrumenter (Lio/sentry/Instrumenter;)V
public fun setName (Ljava/lang/String;)V
public fun setParentSampled (Ljava/lang/Boolean;)V
Expand Down
2 changes: 2 additions & 0 deletions sentry/src/main/java/io/sentry/JsonSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public JsonSerializer(@NotNull SentryOptions options) {
deserializersByClass.put(SentrySpan.class, new SentrySpan.Deserializer());
deserializersByClass.put(SentryStackFrame.class, new SentryStackFrame.Deserializer());
deserializersByClass.put(SentryStackTrace.class, new SentryStackTrace.Deserializer());
deserializersByClass.put(
SentryStartupProfilingOptions.class, new SentryStartupProfilingOptions.Deserializer());
deserializersByClass.put(SentryThread.class, new SentryThread.Deserializer());
deserializersByClass.put(SentryTransaction.class, new SentryTransaction.Deserializer());
deserializersByClass.put(Session.class, new Session.Deserializer());
Expand Down
62 changes: 59 additions & 3 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.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.config.PropertiesProviderFactory;
import io.sentry.instrumentation.file.SentryFileWriter;
import io.sentry.internal.debugmeta.NoOpDebugMetaLoader;
import io.sentry.internal.debugmeta.ResourcesDebugMetaLoader;
import io.sentry.internal.modules.CompositeModulesLoader;
Expand All @@ -21,6 +22,8 @@
import io.sentry.util.thread.MainThreadChecker;
import io.sentry.util.thread.NoOpMainThreadChecker;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
Expand All @@ -47,6 +50,9 @@ private Sentry() {}
/** whether to use a single (global) Hub as opposed to one per thread. */
private static volatile boolean globalHubMode = GLOBAL_HUB_DEFAULT_MODE;

private static final @NotNull String STARTUP_PROFILING_CONFIG_FILE_NAME =
"startup_profiling_config";

/**
* Returns the current (threads) hub, if none, clones the mainHub and returns it.
*
Expand Down Expand Up @@ -225,9 +231,7 @@ private static synchronized void init(

// If the executorService passed in the init is the same that was previously closed, we have to
// set a new one
final ISentryExecutorService sentryExecutorService = options.getExecutorService();
// If the passed executor service was previously called we set a new one
if (sentryExecutorService.isClosed()) {
if (options.getExecutorService().isClosed()) {
options.setExecutorService(new SentryExecutorService());
}

Expand All @@ -242,6 +246,58 @@ private static synchronized void init(
notifyOptionsObservers(options);

finalizePreviousSession(options, HubAdapter.getInstance());

handleStartupProfilingConfig(options, options.getExecutorService());
}

@SuppressWarnings("FutureReturnValueIgnored")
private static void handleStartupProfilingConfig(
final @NotNull SentryOptions options,
final @NotNull ISentryExecutorService sentryExecutorService) {
sentryExecutorService.submit(
romtsn marked this conversation as resolved.
Show resolved Hide resolved
() -> {
final String cacheDirPath = options.getCacheDirPathWithoutDsn();
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
if (cacheDirPath != null) {
final @NotNull File startupProfilingConfigFile =
new File(cacheDirPath, STARTUP_PROFILING_CONFIG_FILE_NAME);
// We always delete the config file for startup profiling
FileUtils.deleteRecursively(startupProfilingConfigFile);
if (!options.isEnableStartupProfiling()) {
return;
}
if (!options.isTracingEnabled()) {
options
.getLogger()
.log(
SentryLevel.INFO,
"Tracing is disabled and startup profiling will not start.");
return;
}
try {
if (startupProfilingConfigFile.createNewFile()) {
final @NotNull TracesSamplingDecision startupSamplingDecision =
sampleStartupProfiling(options);
final @NotNull SentryStartupProfilingOptions startupProfilingOptions =
new SentryStartupProfilingOptions(options, startupSamplingDecision);
try (Writer fileWriter = new SentryFileWriter(startupProfilingConfigFile)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
try (Writer fileWriter = new SentryFileWriter(startupProfilingConfigFile)) {
try (Writer fileWriter = new FileWriter(startupProfilingConfigFile)) {

SentryFileWriter is specific for performance instrumentation, I guess we don't want to create spans for our own file operations :)

Copy link
Member

Choose a reason for hiding this comment

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

Also, let's use FileOutputStream maybe, to align with all other places where we write to a file?

options.getSerializer().serialize(startupProfilingOptions, fileWriter);
}
}
} catch (IOException e) {
options
.getLogger()
.log(SentryLevel.ERROR, "Unable to create startup profiling config file. ", e);
}
}
});
}

private static @NotNull TracesSamplingDecision sampleStartupProfiling(
final @NotNull SentryOptions options) {
TransactionContext startupTransactionContext = new TransactionContext("ui.load", "");
romtsn marked this conversation as resolved.
Show resolved Hide resolved
startupTransactionContext.setForNextStartup(true);
SamplingContext startupSamplingContext = new SamplingContext(startupTransactionContext, null);
return new TracesSampler(options).sample(startupSamplingContext);
}

@SuppressWarnings("FutureReturnValueIgnored")
Expand Down
36 changes: 36 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,9 @@ public class SentryOptions {

@ApiStatus.Experimental private boolean enableBackpressureHandling = false;

/** Whether to enable startup profiling, depending on profilesSampler or profilesSampleRate. */
private boolean enableStartupProfiling = false;

/**
* Adds an event processor
*
Expand Down Expand Up @@ -730,6 +733,20 @@ public void setBeforeBreadcrumb(@Nullable BeforeBreadcrumbCallback beforeBreadcr
return dsnHash != null ? new File(cacheDirPath, dsnHash).getAbsolutePath() : cacheDirPath;
}

/**
* Returns the cache dir path if set, without the appended dsn hash.
*
* @return the cache dir path, without the appended dsn hash, or null if not set.
*/
@Nullable
String getCacheDirPathWithoutDsn() {
if (cacheDirPath == null || cacheDirPath.isEmpty()) {
return null;
}

return cacheDirPath;
}

/**
* Returns the outbox path if cacheDirPath is set
*
Expand Down Expand Up @@ -2122,6 +2139,25 @@ public void setEnablePrettySerializationOutput(boolean enablePrettySerialization
this.enablePrettySerializationOutput = enablePrettySerializationOutput;
}

/**
* Whether to enable startup profiling, depending on profilesSampler or profilesSampleRate.
* Depends on {@link SentryOptions#isProfilingEnabled()}
*
* @return true if startup profiling should be started.
*/
public boolean isEnableStartupProfiling() {
return isProfilingEnabled() && enableStartupProfiling;
}

/**
* Whether to enable startup profiling, depending on profilesSampler or profilesSampleRate.
*
* @param enableStartupProfiling true if startup profiling should be started.
*/
public void setEnableStartupProfiling(boolean enableStartupProfiling) {
this.enableStartupProfiling = enableStartupProfiling;
}

/**
* Whether to send modules containing information about versions.
*
Expand Down
146 changes: 146 additions & 0 deletions sentry/src/main/java/io/sentry/SentryStartupProfilingOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package io.sentry;

import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

final class SentryStartupProfilingOptions implements JsonUnknown, JsonSerializable {

boolean profileSampled;
Copy link
Member

Choose a reason for hiding this comment

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

should these be final?

Copy link
Member Author

Choose a reason for hiding this comment

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

nope, as these are read and deserialized from json (L100)

@Nullable Double profileSampleRate;
boolean traceSampled;
@Nullable Double traceSampleRate;
@Nullable String profilingTracesDirPath;
boolean isProfilingEnabled;

private @Nullable Map<String, Object> unknown;

SentryStartupProfilingOptions() {
traceSampled = false;
traceSampleRate = null;
profileSampled = false;
profileSampleRate = null;
profilingTracesDirPath = null;
isProfilingEnabled = false;
}

SentryStartupProfilingOptions(
final @NotNull SentryOptions options,
final @NotNull TracesSamplingDecision samplingDecision) {
traceSampled = samplingDecision.getSampled();
traceSampleRate = samplingDecision.getSampleRate();
profileSampled = samplingDecision.getProfileSampled();
profileSampleRate = samplingDecision.getProfileSampleRate();
profilingTracesDirPath = options.getProfilingTracesDirPath();
isProfilingEnabled = options.isProfilingEnabled();
}

// JsonSerializable

public static final class JsonKeys {
public static final String PROFILE_SAMPLED = "profile_sampled";
public static final String PROFILE_SAMPLE_RATE = "profile_sample_rate";
public static final String TRACE_SAMPLED = "trace_sampled";
public static final String TRACE_SAMPLE_RATE = "trace_sample_rate";
public static final String PROFILING_TRACES_DIR_PATH = "profiling_traces_dir_path";
public static final String IS_PROFILING_ENABLED = "is_profiling_enabled";
}

@Override
public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
throws IOException {
writer.beginObject();
writer.name(JsonKeys.PROFILE_SAMPLED).value(logger, profileSampled);
writer.name(JsonKeys.PROFILE_SAMPLE_RATE).value(logger, profileSampleRate);
writer.name(JsonKeys.TRACE_SAMPLED).value(logger, traceSampled);
writer.name(JsonKeys.TRACE_SAMPLE_RATE).value(logger, traceSampleRate);
writer.name(JsonKeys.PROFILING_TRACES_DIR_PATH).value(logger, profilingTracesDirPath);
writer.name(JsonKeys.IS_PROFILING_ENABLED).value(logger, isProfilingEnabled);

if (unknown != null) {
for (String key : unknown.keySet()) {
Object value = unknown.get(key);
writer.name(key);
writer.value(logger, value);
}
}
writer.endObject();
}

@Nullable
@Override
public Map<String, Object> getUnknown() {
return unknown;
}

@Override
public void setUnknown(@Nullable Map<String, Object> unknown) {
this.unknown = unknown;
}

public static final class Deserializer
implements JsonDeserializer<SentryStartupProfilingOptions> {

@Override
public @NotNull SentryStartupProfilingOptions deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
reader.beginObject();
SentryStartupProfilingOptions options = new SentryStartupProfilingOptions();
Map<String, Object> unknown = null;

while (reader.peek() == JsonToken.NAME) {
final String nextName = reader.nextName();
switch (nextName) {
case JsonKeys.PROFILE_SAMPLED:
Boolean profileSampled = reader.nextBooleanOrNull();
if (profileSampled != null) {
options.profileSampled = profileSampled;
}
break;
case JsonKeys.PROFILE_SAMPLE_RATE:
Double profileSampleRate = reader.nextDoubleOrNull();
if (profileSampleRate != null) {
options.profileSampleRate = profileSampleRate;
}
break;
case JsonKeys.TRACE_SAMPLED:
Boolean traceSampled = reader.nextBooleanOrNull();
if (traceSampled != null) {
options.traceSampled = traceSampled;
}
break;
case JsonKeys.TRACE_SAMPLE_RATE:
Double traceSampleRate = reader.nextDoubleOrNull();
if (traceSampleRate != null) {
options.traceSampleRate = traceSampleRate;
}
break;
case JsonKeys.PROFILING_TRACES_DIR_PATH:
String profilingTracesDirPath = reader.nextStringOrNull();
if (profilingTracesDirPath != null) {
options.profilingTracesDirPath = profilingTracesDirPath;
}
break;
case JsonKeys.IS_PROFILING_ENABLED:
Boolean isProfilingEnabled = reader.nextBooleanOrNull();
if (isProfilingEnabled != null) {
options.isProfilingEnabled = isProfilingEnabled;
}
break;
default:
if (unknown == null) {
unknown = new ConcurrentHashMap<>();
}
reader.nextUnknown(logger, unknown, nextName);
break;
}
}
options.setUnknown(unknown);
reader.endObject();
return options;
}
}
}
17 changes: 17 additions & 0 deletions sentry/src/main/java/io/sentry/TransactionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public final class TransactionContext extends SpanContext {
private @Nullable TracesSamplingDecision parentSamplingDecision;
private @Nullable Baggage baggage;
private @NotNull Instrumenter instrumenter = Instrumenter.SENTRY;
private boolean isForNextStartup = false;

/**
* Creates {@link TransactionContext} from sentry-trace header.
Expand Down Expand Up @@ -200,4 +201,20 @@ public void setName(final @NotNull String name) {
public void setTransactionNameSource(final @NotNull TransactionNameSource transactionNameSource) {
this.transactionNameSource = transactionNameSource;
}

@ApiStatus.Internal
public void setForNextStartup(final boolean forNextStartup) {
isForNextStartup = forNextStartup;
}

/**
* Whether this {@link TransactionContext} evaluates for the next startup. If this is true, it
* gets called only once when the SDK initializes. This is set only if {@link
* SentryOptions#isEnableStartupProfiling()} is true.
*
* @return True if this {@link TransactionContext} will be used for the next startup.
*/
public boolean isForNextStartup() {
return isForNextStartup;
}
}
Loading