diff --git a/README.md b/README.md index bddc125..32dea90 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,29 @@ # Pyroscope Java agent -The Java profiling agent for Pyroscope.io. Based on [async-profiler](https://github.com/jvm-profiling-tools/async-profiler). +The Java profiling agent for Pyroscope.io. Based +on [async-profiler](https://github.com/jvm-profiling-tools/async-profiler). ## Distribution The agent is distributed as a single JAR file `pyroscope.jar`. It contains native async-profiler libraries for: + - Linux on x64; - Linux on ARM64; - MacOS on x64. - MacOS on ARM64. +## Windows OS support + +It also contains support for Windows OS, through JFR profiler. In order to use JFR as profiler in place of +async-profiler, +you need to configure profiler type, either through configuration file or environment variable. + +By setting `PYROSCOPE_PROFILER_TYPE` configuration variable to `JFR`, agent will use JVM built-in profiler. + ## Downloads -Visit [releases](https://github.com/pyroscope-io/pyroscope-java/releases) page to download the latest version of `pyroscope.jar` +Visit [releases](https://github.com/pyroscope-io/pyroscope-java/releases) page to download the latest version +of `pyroscope.jar` ## Usage diff --git a/agent/src/main/java/io/pyroscope/javaagent/Profiler.java b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java similarity index 92% rename from agent/src/main/java/io/pyroscope/javaagent/Profiler.java rename to agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java index 4cd1a34..40818c9 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/Profiler.java +++ b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java @@ -15,7 +15,8 @@ import java.time.Duration; import java.time.Instant; -public final class Profiler { + +public final class AsyncProfilerDelegate implements ProfilerDelegate { private Config config; private EventType eventType; private String alloc; @@ -26,11 +27,12 @@ public final class Profiler { private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler(); - Profiler(Config config) { - reset(config); + public AsyncProfilerDelegate(Config config) { + setConfig(config); } - public void reset(final Config config) { + @Override + public void setConfig(final Config config) { this.config = config; this.alloc = config.profilingAlloc; this.lock = config.profilingLock; @@ -52,6 +54,7 @@ public void reset(final Config config) { /** * Start async-profiler */ + @Override public synchronized void start() { if (format == Format.JFR) { try { @@ -67,22 +70,22 @@ public synchronized void start() { /** * Stop async-profiler */ + @Override public synchronized void stop() { instance.stop(); } /** - * * @param started - time when profiling has been started - * @param ended - time when profiling has ended + * @param ended - time when profiling has ended * @return Profiling data and dynamic labels as {@link Snapshot} */ + @Override public synchronized Snapshot dumpProfile(Instant started, Instant ended) { return dumpImpl(started, ended); } - private String createJFRCommand() { StringBuilder sb = new StringBuilder(); sb.append("start,event=").append(eventType.id); diff --git a/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java new file mode 100644 index 0000000..e85e79d --- /dev/null +++ b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java @@ -0,0 +1,21 @@ +package io.pyroscope.javaagent; + +public class CurrentPidProvider { + public static long getCurrentProcessId() { + return ProcessHandle.current().pid(); +// RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); +// Field jvm = null; +// try { +// jvm = runtime.getClass().getDeclaredField("jvm"); +// jvm.setAccessible(true); +// +// VMManagement management = (VMManagement) jvm.get(runtime); +// Method method = management.getClass().getDeclaredMethod("getProcessId"); +// method.setAccessible(true); +// +// return (Integer) method.invoke(management); +// } catch (NoSuchFieldException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { +// throw new RuntimeException(e); +// } + } +} diff --git a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java new file mode 100644 index 0000000..8319d3a --- /dev/null +++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java @@ -0,0 +1,160 @@ +package io.pyroscope.javaagent; + +import io.pyroscope.http.Format; +import io.pyroscope.javaagent.config.Config; +import io.pyroscope.labels.Pyroscope; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +public final class JFRProfilerDelegate implements ProfilerDelegate { + private static final String RECORDING_NAME = "pyroscope"; + private static final String JFR_SETTINGS_RESOURCE = "/jfr/pyroscope.jfc"; + + private static final String OS_NAME = "os.name"; + private Config config; + private File tempJFRFile; + private Path jcmdBin; + private Path jfrSettingsPath; + + public JFRProfilerDelegate(Config config) { + setConfig(config); + } + + @Override + public void setConfig(final Config config) { + this.config = config; + jcmdBin = findJcmdBin(); + jfrSettingsPath = findJfrSettingsPath(config); + + try { + tempJFRFile = File.createTempFile("pyroscope", ".jfr"); + tempJFRFile.deleteOnExit(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Start JFR profiler + */ + @Override + public synchronized void start() { + List cmdLine = new ArrayList<>(); + cmdLine.add(jcmdBin.toString()); + cmdLine.add(String.valueOf(CurrentPidProvider.getCurrentProcessId())); + cmdLine.add("JFR.start"); + cmdLine.add("name=" + RECORDING_NAME); + cmdLine.add("filename=" + tempJFRFile.getAbsolutePath()); + cmdLine.add("settings=" + jfrSettingsPath); + executeCmd(cmdLine); + } + + /** + * Stop JFR profiler + */ + @Override + public synchronized void stop() { + List cmdLine = new ArrayList<>(); + cmdLine.add(jcmdBin.toString()); + cmdLine.add(String.valueOf(CurrentPidProvider.getCurrentProcessId())); + cmdLine.add("JFR.stop"); + cmdLine.add("name=" + RECORDING_NAME); + executeCmd(cmdLine); + } + + /** + * @param started - time when profiling has been started + * @param ended - time when profiling has ended + * @return Profiling data and dynamic labels as {@link Snapshot} + */ + @Override + public synchronized Snapshot dumpProfile(Instant started, Instant ended) { + return dumpImpl(started, ended); + } + + private Snapshot dumpImpl(Instant started, Instant ended) { + if (config.gcBeforeDump) { + System.gc(); + } + try { + byte[] data = Files.readAllBytes(tempJFRFile.toPath()); + return new Snapshot( + Format.JFR, + EventType.CPU, + started, + ended, + data, + Pyroscope.LabelsWrapper.dump() + ); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static Path findJcmdBin() { + Path javaHome = Paths.get(System.getProperty("java.home")); + String jcmd = jcmdExecutable(); + Path jcmdBin = javaHome.resolve("bin").resolve(jcmd); + //find jcmd binary + if (!Files.isExecutable(jcmdBin)) { + jcmdBin = javaHome.getParent().resolve("bin").resolve(jcmd); + if (!Files.isExecutable(jcmdBin)) { + throw new RuntimeException("cannot find executable jcmd in Java home"); + } + } + return jcmdBin; + } + + private static String jcmdExecutable() { + String jcmd = "jcmd"; + if (isWindowsOS()) { + jcmd = "jcmd.exe"; + } + return jcmd; + } + + private static Path findJfrSettingsPath(Config config) { + // first try to load settings from provided configuration + if (config.jfrProfilerSettings != null) { + return Paths.get(config.jfrProfilerSettings); + } + // otherwise load default settings + try (InputStream inputStream = JFRProfilerDelegate.class.getResourceAsStream(JFR_SETTINGS_RESOURCE)) { + Path jfrSettingsPath = Files.createTempFile("pyroscope", ".jfc"); + Files.copy(inputStream, jfrSettingsPath, StandardCopyOption.REPLACE_EXISTING); + return jfrSettingsPath; + } catch (IOException e) { + throw new UncheckedIOException(format("unable to load %s from classpath", JFR_SETTINGS_RESOURCE), e); + } + } + + private static boolean isWindowsOS() { + String osName = System.getProperty(OS_NAME); + return osName.contains("Windows"); + } + + private static void executeCmd(List cmdLine) { + try { + ProcessBuilder processBuilder = new ProcessBuilder(cmdLine); + Process process = processBuilder.redirectErrorStream(true).start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + String processOutput = new BufferedReader(new InputStreamReader(process.getInputStream())).lines().collect(Collectors.joining("\n")); + throw new RuntimeException(format("Invalid exit code %s, process output %s", exitCode, processOutput)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(format("failed to start process: %s", cmdLine), e); + } + } + +} diff --git a/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java new file mode 100644 index 0000000..313d0d1 --- /dev/null +++ b/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java @@ -0,0 +1,26 @@ +package io.pyroscope.javaagent; + +import io.pyroscope.javaagent.config.Config; +import io.pyroscope.javaagent.config.ProfilerType; + +import java.time.Instant; + +public interface ProfilerDelegate { + /** + * Creates profiler delegate instance based on configuration. + * + * @param config + * @return + */ + static ProfilerDelegate create(Config config) { + return config.profilerType.create(config); + } + + void start(); + + void stop(); + + Snapshot dumpProfile(Instant profilingStartTime, Instant now); + + void setConfig(Config config); +} diff --git a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java index 09d569a..f5e94a6 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java +++ b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java @@ -30,7 +30,8 @@ public static void start() { } public static void start(Config config) { - start(new Options.Builder(config).build()); + start(new Options.Builder(config) + .build()); } public static void start(Options options) { @@ -91,7 +92,7 @@ public static class Options { final Config config; final ProfilingScheduler scheduler; final Logger logger; - final Profiler profiler; + final ProfilerDelegate profiler; final Exporter exporter; private Options(Builder b) { @@ -104,14 +105,13 @@ private Options(Builder b) { public static class Builder { final Config config; - final Profiler profiler; + ProfilerDelegate profiler; Exporter exporter; ProfilingScheduler scheduler; Logger logger; public Builder(Config config) { this.config = config; - this.profiler = new Profiler(config); } public Builder setExporter(Exporter exporter) { @@ -143,6 +143,9 @@ public Options build() { scheduler = new SamplingProfilingScheduler(config, exporter, logger); } } + if (profiler == null) { + profiler = ProfilerDelegate.create(config); + } return new Options(this); } } diff --git a/agent/src/main/java/io/pyroscope/javaagent/api/ProfilingScheduler.java b/agent/src/main/java/io/pyroscope/javaagent/api/ProfilingScheduler.java index 4103c3f..28ae39d 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/api/ProfilingScheduler.java +++ b/agent/src/main/java/io/pyroscope/javaagent/api/ProfilingScheduler.java @@ -1,6 +1,7 @@ package io.pyroscope.javaagent.api; -import io.pyroscope.javaagent.Profiler; +import io.pyroscope.javaagent.AsyncProfilerDelegate; +import io.pyroscope.javaagent.ProfilerDelegate; import java.time.Instant; @@ -9,13 +10,13 @@ */ public interface ProfilingScheduler { /** - * Use Profiler's to start, stop, dumpProfile - * {@link Profiler#start()} - * {@link Profiler#stop()} - * {@link Profiler#dumpProfile(Instant, Instant)} + * Use AsyncProfilerDelegate's to start, stop, dumpProfile + * {@link AsyncProfilerDelegate#start()} + * {@link AsyncProfilerDelegate#stop()} + * {@link AsyncProfilerDelegate#dumpProfile(Instant, Instant)} * Here is an example of naive implementation *
-     * public void start(Profiler profiler) {
+     * public void start(AsyncProfilerDelegate profiler) {
      *      new Thread(() -> {
      *          while (true) {
      *              Instant startTime = Instant.now();
@@ -35,7 +36,7 @@ public interface ProfilingScheduler {
      *     Github issue #40 for more details.
      *
      **/
-    void start(Profiler profiler);
+    void start(ProfilerDelegate profiler);
 
     void stop();
 }
diff --git a/agent/src/main/java/io/pyroscope/javaagent/config/Config.java b/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
index 3448cd8..286f431 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
@@ -14,6 +14,8 @@
 
 import java.lang.reflect.Type;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -28,6 +30,7 @@ public final class Config {
     private static final String PYROSCOPE_AGENT_ENABLED_CONFIG = "PYROSCOPE_AGENT_ENABLED";
     private static final String PYROSCOPE_APPLICATION_NAME_CONFIG = "PYROSCOPE_APPLICATION_NAME";
     private static final String PYROSCOPE_PROFILING_INTERVAL_CONFIG = "PYROSCOPE_PROFILING_INTERVAL";
+    private static final String PYROSCOPE_PROFILER_TYPE_CONFIG = "PYROSCOPE_PROFILER_TYPE";
     private static final String PYROSCOPE_PROFILER_EVENT_CONFIG = "PYROSCOPE_PROFILER_EVENT";
     private static final String PYROSCOPE_PROFILER_ALLOC_CONFIG = "PYROSCOPE_PROFILER_ALLOC";
     private static final String PYROSCOPE_PROFILER_LOCK_CONFIG = "PYROSCOPE_PROFILER_LOCK";
@@ -66,6 +69,12 @@ public final class Config {
      */
     private static final String PYROSCOPE_SAMPLING_EVENT_ORDER_CONFIG = "PYROSCOPE_SAMPLING_EVENT_ORDER";
 
+    // JFR profiler settings
+    /**
+     * Allows you to overwrite default JFR profiler settings
+     */
+    private static final String PYROSCOPE_JFR_PROFILER_SETTINGS = "PYROSCOPE_JFR_PROFILER_SETTINGS";
+
     private static final boolean DEFAULT_AGENT_ENABLED = true;
     public static final String DEFAULT_SPY_NAME = "javaspy";
     private static final Duration DEFAULT_PROFILING_INTERVAL = Duration.ofMillis(10);
@@ -89,6 +98,7 @@ public final class Config {
 
     public final boolean agentEnabled;
     public final String applicationName;
+    public final ProfilerType profilerType;
     public final Duration profilingInterval;
     public final EventType profilingEvent;
     public final String profilingAlloc;
@@ -99,6 +109,7 @@ public final class Config {
     public final Logger.Level logLevel;
     public final String serverAddress;
     public final String authToken;
+    public final String jfrProfilerSettings;
 
     @Deprecated
     public final String timeseriesName;
@@ -123,6 +134,7 @@ public final class Config {
 
     Config(final boolean agentEnabled,
            final String applicationName,
+           final ProfilerType profilerType,
            final Duration profilingInterval,
            final EventType profilingEvent,
            final String profilingAlloc,
@@ -132,7 +144,7 @@ public final class Config {
            final int javaStackDepthMax,
            final Logger.Level logLevel,
            final String serverAddress,
-           final String authToken,
+           final String authToken, String jfrProfilerSettings,
            final Format format,
            final int pushQueueCapacity,
            final Map labels,
@@ -150,6 +162,7 @@ public final class Config {
            String basicAuthPassword) {
         this.agentEnabled = agentEnabled;
         this.applicationName = applicationName;
+        this.profilerType = profilerType;
         this.profilingInterval = profilingInterval;
         this.profilingEvent = profilingEvent;
         this.profilingAlloc = profilingAlloc;
@@ -159,6 +172,7 @@ public final class Config {
         this.logLevel = logLevel;
         this.serverAddress = serverAddress;
         this.authToken = authToken;
+        this.jfrProfilerSettings = jfrProfilerSettings;
         this.ingestMaxTries = ingestMaxRetries;
         this.compressionLevelJFR = validateCompressionLevel(compressionLevelJFR);
         this.compressionLevelLabels = validateCompressionLevel(compressionLevelLabels);
@@ -202,31 +216,33 @@ public long profilingIntervalInHertz() {
     @Override
     public String toString() {
         return "Config{" +
-            "agentEnabled=" + agentEnabled +
-            ", applicationName='" + applicationName + '\'' +
-            ", profilingInterval=" + profilingInterval +
-            ", profilingEvent=" + profilingEvent +
-            ", profilingAlloc='" + profilingAlloc + '\'' +
-            ", profilingLock='" + profilingLock + '\'' +
-            ", samplingEventOrder='" + samplingEventOrder + '\'' +
-            ", uploadInterval=" + uploadInterval +
-            ", javaStackDepthMax=" + javaStackDepthMax +
-            ", logLevel=" + logLevel +
-            ", serverAddress='" + serverAddress + '\'' +
-            ", authToken='" + authToken + '\'' +
-            ", timeseriesName='" + timeseriesName + '\'' +
-            ", timeseries=" + timeseries +
-            ", format=" + format +
-            ", pushQueueCapacity=" + pushQueueCapacity +
-            ", labels=" + labels +
-            ", ingestMaxTries=" + ingestMaxTries +
-            ", compressionLevelJFR=" + compressionLevelJFR +
-            ", compressionLevelLabels=" + compressionLevelLabels +
-            ", allocLive=" + allocLive +
-            ", httpHeaders=" + httpHeaders +
-            ", samplingDuration=" + samplingDuration +
-            ", tenantID=" + tenantID +
-            '}';
+               "agentEnabled=" + agentEnabled +
+               ", applicationName='" + applicationName + '\'' +
+               ", profilerType=" + profilerType +
+               ", profilingInterval=" + profilingInterval +
+               ", profilingEvent=" + profilingEvent +
+               ", profilingAlloc='" + profilingAlloc + '\'' +
+               ", profilingLock='" + profilingLock + '\'' +
+               ", samplingEventOrder='" + samplingEventOrder + '\'' +
+               ", uploadInterval=" + uploadInterval +
+               ", javaStackDepthMax=" + javaStackDepthMax +
+               ", logLevel=" + logLevel +
+               ", serverAddress='" + serverAddress + '\'' +
+               ", authToken='" + authToken + '\'' +
+               ", jfrProfilerSettings='" + jfrProfilerSettings + '\'' +
+               ", timeseriesName='" + timeseriesName + '\'' +
+               ", timeseries=" + timeseries +
+               ", format=" + format +
+               ", pushQueueCapacity=" + pushQueueCapacity +
+               ", labels=" + labels +
+               ", ingestMaxTries=" + ingestMaxTries +
+               ", compressionLevelJFR=" + compressionLevelJFR +
+               ", compressionLevelLabels=" + compressionLevelLabels +
+               ", allocLive=" + allocLive +
+               ", httpHeaders=" + httpHeaders +
+               ", samplingDuration=" + samplingDuration +
+               ", tenantID=" + tenantID +
+               '}';
     }
 
     public Builder newBuilder() {
@@ -254,6 +270,7 @@ public static Config build(ConfigurationProvider cp) {
         return new Config(
             agentEnabled,
             applicationName(cp),
+            profilerType(cp),
             profilingInterval(cp),
             profilingEvent(cp),
             alloc,
@@ -264,6 +281,7 @@ public static Config build(ConfigurationProvider cp) {
             logLevel(cp),
             serverAddress(cp),
             authToken(cp),
+            jfrProfilerSettings(cp),
             format(cp),
             pushQueueCapacity(cp),
             labels(cp),
@@ -277,8 +295,23 @@ public static Config build(ConfigurationProvider cp) {
             tenantID(cp),
             cp.get(PYROSCOPE_AP_LOG_LEVEL_CONFIG),
             cp.get(PYROSCOPE_AP_EXTRA_ARGUMENTS_CONFIG),
-            cp.get(PYROSCOPE_BASIC_AUTH_USER_CONFIG),
-            cp.get(PYROSCOPE_BASIC_AUTH_PASSWORD_CONFIG));
+            cp.get(PYROSCOPE_BASIC_AUTH_USER_CONFIG), cp.get(PYROSCOPE_BASIC_AUTH_PASSWORD_CONFIG));
+    }
+
+    private static String jfrProfilerSettings(ConfigurationProvider configurationProvider) {
+        String jfrProfilerSettings = configurationProvider.get(PYROSCOPE_JFR_PROFILER_SETTINGS);
+        if (jfrProfilerSettings != null && !Files.isRegularFile(Paths.get(jfrProfilerSettings))) {
+            DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.ERROR, "unable to find JFR profiler settings at %s", jfrProfilerSettings);
+        }
+        return null;
+    }
+
+    private static ProfilerType profilerType(ConfigurationProvider configurationProvider) {
+        String profilerTypeName = configurationProvider.get(PYROSCOPE_PROFILER_TYPE_CONFIG);
+        if (profilerTypeName == null || profilerTypeName.isEmpty()) {
+            return ProfilerType.ASYNC;
+        }
+        return ProfilerType.valueOf(profilerTypeName);
     }
 
     private static String applicationName(ConfigurationProvider configurationProvider) {
@@ -377,8 +410,8 @@ private static List samplingEventOrder(final ConfigurationProvider cp
                     return null;
                 }
             })
-            .filter(t -> null != t)
-            .collect(Collectors.toCollection(() -> new ArrayList<>()));
+            .filter(Objects::nonNull)
+            .collect(Collectors.toCollection(ArrayList::new));
     }
 
     // extra args events not supported
@@ -578,13 +611,10 @@ public static Map httpHeaders(ConfigurationProvider cp) {
 
         try {
             Map httpHeaders = adapter.fromJson(sHttpHeaders);
-            if (httpHeaders == null) {
-                return Collections.emptyMap();
-            }
-            return httpHeaders;
+            return Objects.requireNonNullElse(httpHeaders, Collections.emptyMap());
         } catch (Exception e) {
             DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.ERROR, "Failed to parse %s = %s configuration. " +
-                "Falling back to no extra http headers. %s: ", PYROSCOPE_HTTP_HEADERS, sHttpHeaders, e.getMessage());
+                                                                   "Falling back to no extra http headers. %s: ", PYROSCOPE_HTTP_HEADERS, sHttpHeaders, e.getMessage());
             return Collections.emptyMap();
         }
     }
@@ -635,6 +665,7 @@ private static Duration samplingDuration(ConfigurationProvider configurationProv
     public static class Builder {
         public boolean agentEnabled = DEFAULT_AGENT_ENABLED;
         public String applicationName = null;
+        public ProfilerType profilerType = ProfilerType.ASYNC;
         public Duration profilingInterval = DEFAULT_PROFILING_INTERVAL;
         public EventType profilingEvent = DEFAULT_PROFILER_EVENT;
         public String profilingAlloc = "";
@@ -661,6 +692,7 @@ public static class Builder {
         private String APExtraArguments = null;
         private String basicAuthUser;
         private String basicAuthPassword;
+        private String jfrProfilerSettings;
 
         public Builder() {
         }
@@ -668,6 +700,7 @@ public Builder() {
         public Builder(Config buildUpon) {
             agentEnabled = buildUpon.agentEnabled;
             applicationName = buildUpon.applicationName;
+            profilerType = buildUpon.profilerType;
             profilingInterval = buildUpon.profilingInterval;
             profilingEvent = buildUpon.profilingEvent;
             profilingAlloc = buildUpon.profilingAlloc;
@@ -678,6 +711,7 @@ public Builder(Config buildUpon) {
             logLevel = buildUpon.logLevel;
             serverAddress = buildUpon.serverAddress;
             authToken = buildUpon.authToken;
+            jfrProfilerSettings = buildUpon.jfrProfilerSettings;
             format = buildUpon.format;
             pushQueueCapacity = buildUpon.pushQueueCapacity;
             compressionLevelJFR = buildUpon.compressionLevelJFR;
@@ -753,6 +787,11 @@ public Builder setAuthToken(String authToken) {
             return this;
         }
 
+        public Builder setJFRProfilerSettings(String jfrProfilerSettings) {
+            this.jfrProfilerSettings = jfrProfilerSettings;
+            return this;
+        }
+
         public Builder setFormat(Format format) {
             this.format = format;
             return this;
@@ -833,12 +872,18 @@ public Builder setBasicAuthPassword(String basicAuthPassword) {
             return this;
         }
 
+        public Builder setProfilerType(ProfilerType profilerType) {
+            this.profilerType = profilerType;
+            return this;
+        }
+
         public Config build() {
             if (applicationName == null || applicationName.isEmpty()) {
                 applicationName = generateApplicationName();
             }
             return new Config(agentEnabled,
                 applicationName,
+                profilerType,
                 profilingInterval,
                 profilingEvent,
                 profilingAlloc,
@@ -849,6 +894,7 @@ public Config build() {
                 logLevel,
                 serverAddress,
                 authToken,
+                jfrProfilerSettings,
                 format,
                 pushQueueCapacity,
                 labels,
@@ -862,8 +908,7 @@ public Config build() {
                 tenantID,
                 APLogLevel,
                 APExtraArguments,
-                basicAuthUser,
-                basicAuthPassword);
+                basicAuthUser, basicAuthPassword);
         }
     }
 }
diff --git a/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java b/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
new file mode 100644
index 0000000..783b146
--- /dev/null
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
@@ -0,0 +1,26 @@
+package io.pyroscope.javaagent.config;
+
+import io.pyroscope.javaagent.AsyncProfilerDelegate;
+import io.pyroscope.javaagent.JFRProfilerDelegate;
+import io.pyroscope.javaagent.ProfilerDelegate;
+
+import java.lang.reflect.InvocationTargetException;
+
+public enum ProfilerType {
+    JFR(JFRProfilerDelegate.class), ASYNC(AsyncProfilerDelegate.class);
+
+    private final Class profilerDelegateClass;
+
+    ProfilerType(Class profilerDelegateClass) {
+        this.profilerDelegateClass = profilerDelegateClass;
+    }
+
+    public ProfilerDelegate create(Config config) {
+        try {
+            return profilerDelegateClass.getConstructor(Config.class).newInstance(config);
+        } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+                 NoSuchMethodException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java b/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java
index b4a68e5..7f9432d 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java
@@ -1,6 +1,6 @@
 package io.pyroscope.javaagent.impl;
 
-import io.pyroscope.javaagent.Profiler;
+import io.pyroscope.javaagent.ProfilerDelegate;
 import io.pyroscope.javaagent.Snapshot;
 import io.pyroscope.javaagent.api.Exporter;
 import io.pyroscope.javaagent.api.Logger;
@@ -29,7 +29,7 @@ public class ContinuousProfilingScheduler implements ProfilingScheduler {
     private Instant profilingIntervalStartTime;
     private ScheduledFuture job;
     private boolean started;
-    private Profiler profiler;
+    private ProfilerDelegate profiler;
 
     public ContinuousProfilingScheduler(Config config, Exporter exporter, Logger logger) {
         this.config = config;
@@ -38,7 +38,7 @@ public ContinuousProfilingScheduler(Config config, Exporter exporter, Logger log
     }
 
     @Override
-    public void start(Profiler profiler) {
+    public void start(ProfilerDelegate profiler) {
         this.logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler starting");
         synchronized (lock) {
             if (started) {
@@ -143,7 +143,7 @@ private void schedulerTick() {
      *
      * @return Duration of the first profiling interval
      */
-    private Duration startFirst(Profiler profiler) {
+    private Duration startFirst(ProfilerDelegate profiler) {
         Instant now = Instant.now();
 
         long uploadIntervalMillis = config.uploadInterval.toMillis();
diff --git a/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java b/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java
index 023f90e..f008155 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java
@@ -1,8 +1,9 @@
 package io.pyroscope.javaagent.impl;
 
 
+import io.pyroscope.javaagent.AsyncProfilerDelegate;
 import io.pyroscope.javaagent.EventType;
-import io.pyroscope.javaagent.Profiler;
+import io.pyroscope.javaagent.ProfilerDelegate;
 import io.pyroscope.javaagent.Snapshot;
 import io.pyroscope.javaagent.api.Exporter;
 import io.pyroscope.javaagent.api.Logger;
@@ -45,7 +46,7 @@ public SamplingProfilingScheduler(Config config, Exporter exporter, Logger logge
     }
 
     @Override
-    public void start(Profiler profiler) {
+    public void start(ProfilerDelegate profiler) {
         final long samplingDurationMillis = config.samplingDuration.toMillis();
         final Duration uploadInterval = config.uploadInterval;
 
@@ -55,7 +56,7 @@ public void start(Profiler profiler) {
                 final EventType t = config.samplingEventOrder.get(i);
                 final Config tmp = isolate(t, config);
                 logger.log(Logger.Level.DEBUG, "Config for %s ordinal %d: %s", t.id, i, tmp);
-                profiler.reset(tmp);
+                profiler.setConfig(tmp);
                 dumpProfile(profiler, samplingDurationMillis, uploadInterval);
             }
         } :
@@ -75,7 +76,7 @@ public void stop() {
         throw new RuntimeException("not implemented");
     }
 
-    private void dumpProfile(final Profiler profiler, final long samplingDurationMillis, final Duration uploadInterval) {
+    private void dumpProfile(final ProfilerDelegate profiler, final long samplingDurationMillis, final Duration uploadInterval) {
         Instant profilingStartTime = Instant.now();
         try {
             profiler.start();
diff --git a/agent/src/main/resources/jfr/pyroscope.jfc b/agent/src/main/resources/jfr/pyroscope.jfc
new file mode 100644
index 0000000..70a522d
--- /dev/null
+++ b/agent/src/main/resources/jfr/pyroscope.jfc
@@ -0,0 +1,33 @@
+
+
+
+
+    
+        true
+        1 ms
+    
+
+    
+        true
+        true
+        10 ms
+    
+
+    
+        true
+        true
+    
+
+    
+        true
+        true
+    
+
+    
+        true
+        true
+        10 ms
+    
+
diff --git a/demo/build.gradle b/demo/build.gradle
index 7d88c38..75498ba 100644
--- a/demo/build.gradle
+++ b/demo/build.gradle
@@ -10,4 +10,5 @@ repositories {
 }
 dependencies {
     implementation(project(":agent"))
+    implementation("commons-lang:commons-lang:2.6")
 }
diff --git a/demo/src/main/java/App.java b/demo/src/main/java/App.java
index d4d6a83..85ea17f 100644
--- a/demo/src/main/java/App.java
+++ b/demo/src/main/java/App.java
@@ -3,6 +3,7 @@
 import io.pyroscope.javaagent.PyroscopeAgent;
 import io.pyroscope.javaagent.api.Logger;
 import io.pyroscope.javaagent.config.Config;
+import io.pyroscope.javaagent.config.ProfilerType;
 import io.pyroscope.labels.LabelsSet;
 import io.pyroscope.labels.Pyroscope;
 
@@ -23,6 +24,7 @@ public static void main(String[] args) {
                     .setFormat(Format.JFR)
                     .setProfilingEvent(EventType.CTIMER)
                     .setLogLevel(Logger.Level.DEBUG)
+                    .setProfilerType(ProfilerType.JFR)
                     .setLabels(mapOf("user", "tolyan"))
                     .build())
                 .build()