Skip to content

Commit

Permalink
feat: add sampling scheduler (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyhii committed Apr 19, 2023
1 parent f81ae88 commit 3fc20fa
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ public Options build() {
if (exporter == null) {
exporter = new QueuedExporter(config, new PyroscopeExporter(config, logger), logger);
}
scheduler = new ContinuousProfilingScheduler(config, exporter);
if (config.samplingDuration == null) {
scheduler = new ContinuousProfilingScheduler(config, exporter);
} else {
scheduler = new SamplingProfilingScheduler(config, exporter);
}
}
return new Options(this);
}
Expand Down
71 changes: 68 additions & 3 deletions agent/src/main/java/io/pyroscope/javaagent/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public final class Config {
private static final String PYROSCOPE_GC_BEFORE_DUMP = "PYROSCOPE_GC_BEFORE_DUMP";
private static final String PYROSCOPE_HTTP_HEADERS = "PYROSCOPE_HTTP_HEADERS";

/**
* Experimental feature, may be removed in the future
*/
private static final String PYROSCOPE_SAMPLING_RATE = "PYROSCOPE_SAMPLING_RATE";
/**
* Experimental feature, may be removed in the future
*/
private static final String PYROSCOPE_SAMPLING_DURATION = "PYROSCOPE_SAMPLING_DURATION";

public static final String DEFAULT_SPY_NAME = "javaspy";
private static final Duration DEFAULT_PROFILING_INTERVAL = Duration.ofMillis(10);
private static final EventType DEFAULT_PROFILER_EVENT = EventType.ITIMER;
Expand All @@ -60,6 +69,7 @@ public final class Config {
private static final String DEFAULT_LABELS = "";
private static final boolean DEFAULT_ALLOC_LIVE = false;
private static final boolean DEFAULT_GC_BEFORE_DUMP = false;
private static final Duration DEFAULT_SAMPLING_DURATION = null;

public final String applicationName;
public final Duration profilingInterval;
Expand All @@ -85,6 +95,7 @@ public final class Config {
public final boolean gcBeforeDump;

public final Map<String, String> httpHeaders;
public final Duration samplingDuration;

Config(final String applicationName,
final Duration profilingInterval,
Expand All @@ -103,7 +114,9 @@ public final class Config {
int compressionLevelLabels,
boolean allocLive,
boolean gcBeforeDump,
Map<String, String> httpHeaders) {
Map<String, String> httpHeaders,
Duration samplingDuration
) {
this.applicationName = applicationName;
this.profilingInterval = profilingInterval;
this.profilingEvent = profilingEvent;
Expand All @@ -119,6 +132,7 @@ public final class Config {
this.allocLive = allocLive;
this.gcBeforeDump = gcBeforeDump;
this.httpHeaders = httpHeaders;
this.samplingDuration = samplingDuration;
this.timeseries = timeseriesName(AppName.parse(applicationName), profilingEvent, format);
this.timeseriesName = timeseries.toString();
this.format = format;
Expand Down Expand Up @@ -152,6 +166,7 @@ public String toString() {
", compressionLevelLabels=" + compressionLevelLabels +
", allocLive=" + allocLive +
", httpHeaders=" + httpHeaders +
", samplingDuration=" + samplingDuration +
'}';
}

Expand Down Expand Up @@ -194,7 +209,8 @@ public static Config build(ConfigurationProvider configurationProvider) {
compressionLevel(configurationProvider, PYROSCOPE_EXPORT_COMPRESSION_LEVEL_LABELS),
allocLive,
bool(configurationProvider, PYROSCOPE_GC_BEFORE_DUMP, DEFAULT_GC_BEFORE_DUMP),
httpHeaders(configurationProvider)
httpHeaders(configurationProvider),
samplingDuration(configurationProvider)
);
}

Expand Down Expand Up @@ -451,8 +467,47 @@ public static Map<String, String> httpHeaders(ConfigurationProvider cp) {
}
}

private static Duration samplingDuration(ConfigurationProvider configurationProvider) {
Duration uploadInterval = uploadInterval(configurationProvider);

final String samplingDurationStr = configurationProvider.get(PYROSCOPE_SAMPLING_DURATION);
if (samplingDurationStr != null && !samplingDurationStr.isEmpty()) {
try {
Duration samplingDuration = IntervalParser.parse(samplingDurationStr);
if (samplingDuration.compareTo(uploadInterval) > 0) {
DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.WARN, "Invalid %s value %s, ignore it",
PYROSCOPE_SAMPLING_DURATION, samplingDurationStr);
} else {
return samplingDuration;
}
} catch (final NumberFormatException e) {
DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.WARN, "Invalid %s value %s, ignore it",
PYROSCOPE_SAMPLING_DURATION, samplingDurationStr);
}
return DEFAULT_SAMPLING_DURATION;
}

final String samplingRateStr = configurationProvider.get(PYROSCOPE_SAMPLING_RATE);
if (samplingRateStr == null || samplingRateStr.isEmpty()) {
return DEFAULT_SAMPLING_DURATION;
}
try {
double samplingRate = Double.parseDouble(samplingRateStr);
if (samplingRate <= 0.0 || samplingRate >= 1.0) {
return DEFAULT_SAMPLING_DURATION;
}
long uploadIntervalMillis = uploadInterval.toMillis();
long samplingDurationMillis = Math.min(uploadIntervalMillis, Math.round(uploadIntervalMillis * samplingRate));
return Duration.ofMillis(samplingDurationMillis);
} catch (final NumberFormatException e) {
DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.WARN, "Invalid %s value %s, ignore it",
PYROSCOPE_SAMPLING_RATE, samplingRateStr);
return DEFAULT_SAMPLING_DURATION;
}
}

public static class Builder {

public String applicationName = null;
public Duration profilingInterval = DEFAULT_PROFILING_INTERVAL;
public EventType profilingEvent = DEFAULT_PROFILER_EVENT;
Expand All @@ -471,6 +526,8 @@ public static class Builder {
public boolean allocLive = DEFAULT_ALLOC_LIVE;
public boolean gcBeforeDump = DEFAULT_GC_BEFORE_DUMP;
public Map<String, String> httpHeaders = new HashMap<>();
public Duration samplingDuration = DEFAULT_SAMPLING_DURATION;

public Builder() {
}

Expand All @@ -491,6 +548,7 @@ public Builder(Config buildUpon) {
allocLive = buildUpon.allocLive;
gcBeforeDump = buildUpon.gcBeforeDump;
httpHeaders = new HashMap<>(buildUpon.httpHeaders);
samplingDuration = buildUpon.samplingDuration;
}

public Builder setApplicationName(String applicationName) {
Expand Down Expand Up @@ -588,6 +646,11 @@ public Builder addHTTPHeader(String k, String v) {
return this;
}

public Builder setSamplingDuration(Duration samplingDuration) {
this.samplingDuration = samplingDuration;
return this;
}

public Config build() {
if (applicationName == null || applicationName.isEmpty()) {
applicationName = generateApplicationName();
Expand All @@ -609,7 +672,9 @@ public Config build() {
compressionLevelLabels,
allocLive,
gcBeforeDump,
httpHeaders);
httpHeaders,
samplingDuration
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.pyroscope.javaagent.impl;

import static io.pyroscope.javaagent.DateUtils.truncate;

import io.pyroscope.javaagent.Profiler;
import io.pyroscope.javaagent.Snapshot;
import io.pyroscope.javaagent.api.Exporter;
import io.pyroscope.javaagent.api.ProfilingScheduler;
import io.pyroscope.javaagent.config.Config;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* Schedule profiling in sampling mode.
* <p>
* WARNING: still experimental, may go away or behavior may change
*/
public class SamplingProfilingScheduler implements ProfilingScheduler {

private final Config config;
private final Exporter exporter;

private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setName("PyroscopeProfilingScheduler_Sampling");
t.setDaemon(true);
return t;
});

public SamplingProfilingScheduler(Config config, Exporter exporter) {
this.config = config;
this.exporter = exporter;
}

@Override
public void start(Profiler profiler) {
final long samplingDurationMillis = config.samplingDuration.toMillis();
final Duration uploadInterval = config.uploadInterval;
final Runnable dumpProfile = () -> {
Instant profilingStartTime = Instant.now();
profiler.start();
try {
Thread.sleep(samplingDurationMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
profiler.stop();

Snapshot snapshot = profiler.dumpProfile(truncate(profilingStartTime, uploadInterval));
exporter.export(snapshot);
};

Duration initialDelay = getInitialDelay();
executor.scheduleAtFixedRate(
dumpProfile,
initialDelay.toMillis(),
config.uploadInterval.toMillis(),
TimeUnit.MILLISECONDS
);
}

private Duration getInitialDelay() {
Instant now = Instant.now();
Instant prevUploadInterval = truncate(now, config.uploadInterval);
Instant nextUploadInterval = prevUploadInterval.plus(config.uploadInterval);
Duration initialDelay = Duration.between(now, nextUploadInterval);
return initialDelay;
}
}

0 comments on commit 3fc20fa

Please sign in to comment.