From fd1c8779eb44ff903de3ab7df200d2a5ddab7b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= Date: Sat, 23 Dec 2023 20:33:18 +0100 Subject: [PATCH 01/11] extracted ProfilerDelegate interface --- .../{Profiler.java => AsyncProfilerDelegate.java} | 5 +++-- .../io/pyroscope/javaagent/ProfilerDelegate.java | 15 +++++++++++++++ .../io/pyroscope/javaagent/PyroscopeAgent.java | 6 +++--- .../javaagent/api/ProfilingScheduler.java | 15 ++++++++------- .../impl/ContinuousProfilingScheduler.java | 7 ++++--- .../impl/SamplingProfilingScheduler.java | 7 ++++--- 6 files changed, 37 insertions(+), 18 deletions(-) rename agent/src/main/java/io/pyroscope/javaagent/{Profiler.java => AsyncProfilerDelegate.java} (97%) create mode 100644 agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java diff --git a/agent/src/main/java/io/pyroscope/javaagent/Profiler.java b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java similarity index 97% rename from agent/src/main/java/io/pyroscope/javaagent/Profiler.java rename to agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java index 4cd1a34..68e993b 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,7 +27,7 @@ public final class Profiler { private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler(); - Profiler(Config config) { + AsyncProfilerDelegate(Config config) { reset(config); } 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..0801687 --- /dev/null +++ b/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java @@ -0,0 +1,15 @@ +package io.pyroscope.javaagent; + +import io.pyroscope.javaagent.config.Config; + +import java.time.Instant; + +public interface ProfilerDelegate { + 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..536f714 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java +++ b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java @@ -91,7 +91,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 +104,14 @@ private Options(Builder b) { public static class Builder { final Config config; - final Profiler profiler; + final ProfilerDelegate profiler; Exporter exporter; ProfilingScheduler scheduler; Logger logger; public Builder(Config config) { this.config = config; - this.profiler = new Profiler(config); + this.profiler = new AsyncProfilerDelegate(config); } public Builder setExporter(Exporter exporter) { 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/impl/ContinuousProfilingScheduler.java b/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java
index b4a68e5..2de2cab 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,7 @@
 package io.pyroscope.javaagent.impl;
 
-import io.pyroscope.javaagent.Profiler;
+import io.pyroscope.javaagent.AsyncProfilerDelegate;
+import io.pyroscope.javaagent.ProfilerDelegate;
 import io.pyroscope.javaagent.Snapshot;
 import io.pyroscope.javaagent.api.Exporter;
 import io.pyroscope.javaagent.api.Logger;
@@ -38,7 +39,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 +144,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..98e793f 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;
 
@@ -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();

From d4fac07657c4747b324f5fd8a96a2c6c74728ae0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Sat, 23 Dec 2023 20:45:41 +0100
Subject: [PATCH 02/11] skeleton of JFR profiler

---
 .../javaagent/JFRProfilerDelegate.java        | 144 ++++++++++++++++++
 1 file changed, 144 insertions(+)
 create mode 100644 agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java

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..4233aa4
--- /dev/null
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -0,0 +1,144 @@
+package io.pyroscope.javaagent;
+
+import io.pyroscope.http.Format;
+import io.pyroscope.javaagent.config.Config;
+import io.pyroscope.labels.Pyroscope;
+import io.pyroscope.labels.io.pyroscope.PyroscopeAsyncProfiler;
+import one.profiler.AsyncProfiler;
+import one.profiler.Counter;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+
+
+public final class JFRProfilerDelegate implements ProfilerDelegate {
+    private Config config;
+    private EventType eventType;
+    private String alloc;
+    private String lock;
+    private Duration interval;
+    private Format format;
+    private File tempJFRFile;
+
+    private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler();
+
+    JFRProfilerDelegate(Config config) {
+        setConfig(config);
+    }
+
+    public void setConfig(final Config config) {
+        this.config = config;
+        this.alloc = config.profilingAlloc;
+        this.lock = config.profilingLock;
+        this.eventType = config.profilingEvent;
+        this.interval = config.profilingInterval;
+        this.format = config.format;
+
+        if (format == Format.JFR) {
+            try {
+                // flight recorder is built on top of a file descriptor, so we need a file.
+                tempJFRFile = File.createTempFile("pyroscope", ".jfr");
+                tempJFRFile.deleteOnExit();
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
+        } else {
+            tempJFRFile = null;
+        }
+    }
+
+    /**
+     * Start async-profiler
+     */
+    public synchronized void start() {
+        if (format == Format.JFR) {
+            try {
+                instance.execute(createJFRCommand());
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
+        } else {
+            instance.start(eventType.id, interval.toNanos());
+        }
+    }
+
+    /**
+     * Stop async-profiler
+     */
+    public synchronized void stop() {
+        instance.stop();
+    }
+
+    /**
+     *
+     * @param started - time when profiling has been started
+     * @param ended - time when profiling has ended
+     * @return Profiling data and dynamic labels as {@link Snapshot}
+     */
+    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);
+        if (alloc != null && !alloc.isEmpty()) {
+            sb.append(",alloc=").append(alloc);
+            if (config.allocLive) {
+                sb.append(",live");
+            }
+        }
+        if (lock != null && !lock.isEmpty()) {
+            sb.append(",lock=").append(lock);
+        }
+        sb.append(",interval=").append(interval.toNanos())
+            .append(",file=").append(tempJFRFile.toString());
+        if (config.APLogLevel != null) {
+            sb.append(",loglevel=").append(config.APLogLevel);
+        }
+        sb.append(",jstackdepth=").append(config.javaStackDepthMax);
+        if (config.APExtraArguments != null) {
+            sb.append(",").append(config.APExtraArguments);
+        }
+        return sb.toString();
+    }
+
+    private Snapshot dumpImpl(Instant started, Instant ended) {
+        if (config.gcBeforeDump) {
+            System.gc();
+        }
+        final byte[] data;
+        if (format == Format.JFR) {
+            data = dumpJFR();
+        } else {
+            data = instance.dumpCollapsed(Counter.SAMPLES).getBytes(StandardCharsets.UTF_8);
+        }
+        return new Snapshot(
+            format,
+            eventType,
+            started,
+            ended,
+            data,
+            Pyroscope.LabelsWrapper.dump()
+        );
+    }
+
+    private byte[] dumpJFR() {
+        try {
+            byte[] bytes = new byte[(int) tempJFRFile.length()];
+            try (DataInputStream ds = new DataInputStream(new FileInputStream(tempJFRFile))) {
+                ds.readFully(bytes);
+            }
+            return bytes;
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}

From 4af47b3fccc9179e47111c3967e2ec8264ca14d3 Mon Sep 17 00:00:00 2001
From: Wiktor-Sztajerowski 
Date: Sat, 23 Dec 2023 21:44:00 +0100
Subject: [PATCH 03/11] JFR delegate implementation

---
 .../javaagent/CurrentPidProvider.java         |  29 +++++
 .../javaagent/JFRProfilerDelegate.java        | 110 +++++++-----------
 .../pyroscope/javaagent/PyroscopeAgent.java   |   3 +-
 .../io/pyroscope/javaagent/config/Config.java |  17 +++
 .../javaagent/config/ProfilerType.java        |   5 +
 5 files changed, 98 insertions(+), 66 deletions(-)
 create mode 100644 agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
 create mode 100644 agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java

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..869b929
--- /dev/null
+++ b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
@@ -0,0 +1,29 @@
+package io.pyroscope.javaagent;
+
+import sun.management.VMManagement;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.RuntimeMXBean;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class CurrentPidProvider {
+    public static int getCurrentProcessId() {
+
+        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
index 4233aa4..2c29630 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -14,64 +14,74 @@
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
-
+import java.util.ArrayList;
+import java.util.List;
 
 public final class JFRProfilerDelegate implements ProfilerDelegate {
     private Config config;
-    private EventType eventType;
-    private String alloc;
-    private String lock;
-    private Duration interval;
-    private Format format;
     private File tempJFRFile;
 
-    private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler();
-
     JFRProfilerDelegate(Config config) {
         setConfig(config);
     }
 
     public void setConfig(final Config config) {
         this.config = config;
-        this.alloc = config.profilingAlloc;
-        this.lock = config.profilingLock;
-        this.eventType = config.profilingEvent;
-        this.interval = config.profilingInterval;
-        this.format = config.format;
 
-        if (format == Format.JFR) {
-            try {
-                // flight recorder is built on top of a file descriptor, so we need a file.
-                tempJFRFile = File.createTempFile("pyroscope", ".jfr");
-                tempJFRFile.deleteOnExit();
-            } catch (IOException e) {
-                throw new IllegalStateException(e);
-            }
-        } else {
-            tempJFRFile = null;
+        try {
+            // flight recorder is built on top of a file descriptor, so we need a file.
+            tempJFRFile = File.createTempFile("pyroscope", ".jfr");
+            tempJFRFile.deleteOnExit();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
         }
     }
 
     /**
-     * Start async-profiler
+     * Start JFR profiler
      */
     public synchronized void start() {
-        if (format == Format.JFR) {
-            try {
-                instance.execute(createJFRCommand());
-            } catch (IOException e) {
-                throw new IllegalStateException(e);
+        try {
+            List commands = new ArrayList<>();
+            commands.add("jcmd");
+            commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
+            commands.add("JFR.start");
+            commands.add("name=Pyroscope");
+            commands.add("filename="+tempJFRFile.getAbsolutePath());
+            ProcessBuilder processBuilder = new ProcessBuilder(commands);
+            Process process = processBuilder.start();
+            int exitCode = process.waitFor();
+            if (exitCode != 0){
+                throw new RuntimeException("Invalid exit code: " + exitCode);
             }
-        } else {
-            instance.start(eventType.id, interval.toNanos());
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
         }
     }
 
     /**
-     * Stop async-profiler
+     * Stop JFR profiler
      */
     public synchronized void stop() {
-        instance.stop();
+        try {
+            List commands = new ArrayList<>();
+            commands.add("jcmd");
+            commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
+            commands.add("JFR.stop");
+            commands.add("name=Pyroscope");
+            ProcessBuilder processBuilder = new ProcessBuilder(commands);
+            Process process = processBuilder.start();
+            int exitCode = process.waitFor();
+            if (exitCode != 0){
+                throw new RuntimeException("Invalid exit code: " + exitCode);
+            }
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
@@ -84,45 +94,15 @@ 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);
-        if (alloc != null && !alloc.isEmpty()) {
-            sb.append(",alloc=").append(alloc);
-            if (config.allocLive) {
-                sb.append(",live");
-            }
-        }
-        if (lock != null && !lock.isEmpty()) {
-            sb.append(",lock=").append(lock);
-        }
-        sb.append(",interval=").append(interval.toNanos())
-            .append(",file=").append(tempJFRFile.toString());
-        if (config.APLogLevel != null) {
-            sb.append(",loglevel=").append(config.APLogLevel);
-        }
-        sb.append(",jstackdepth=").append(config.javaStackDepthMax);
-        if (config.APExtraArguments != null) {
-            sb.append(",").append(config.APExtraArguments);
-        }
-        return sb.toString();
-    }
-
     private Snapshot dumpImpl(Instant started, Instant ended) {
         if (config.gcBeforeDump) {
             System.gc();
         }
         final byte[] data;
-        if (format == Format.JFR) {
             data = dumpJFR();
-        } else {
-            data = instance.dumpCollapsed(Counter.SAMPLES).getBytes(StandardCharsets.UTF_8);
-        }
         return new Snapshot(
-            format,
-            eventType,
+            Format.JFR,
+            EventType.CPU,
             started,
             ended,
             data,
diff --git a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java
index 536f714..0bab2cd 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) {
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..1bcf950 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
@@ -28,6 +28,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";
@@ -89,6 +90,7 @@ public final class Config {
 
     public final boolean agentEnabled;
     public final String applicationName;
+    private final ProfilerType profilerType;
     public final Duration profilingInterval;
     public final EventType profilingEvent;
     public final String profilingAlloc;
@@ -123,6 +125,7 @@ public final class Config {
 
     Config(final boolean agentEnabled,
            final String applicationName,
+           final ProfilerType profilerType,
            final Duration profilingInterval,
            final EventType profilingEvent,
            final String profilingAlloc,
@@ -150,6 +153,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;
@@ -204,6 +208,7 @@ public String toString() {
         return "Config{" +
             "agentEnabled=" + agentEnabled +
             ", applicationName='" + applicationName + '\'' +
+            ", profilerType=" + profilerType +
             ", profilingInterval=" + profilingInterval +
             ", profilingEvent=" + profilingEvent +
             ", profilingAlloc='" + profilingAlloc + '\'' +
@@ -254,6 +259,7 @@ public static Config build(ConfigurationProvider cp) {
         return new Config(
             agentEnabled,
             applicationName(cp),
+            profilerType(cp),
             profilingInterval(cp),
             profilingEvent(cp),
             alloc,
@@ -281,6 +287,14 @@ public static Config build(ConfigurationProvider cp) {
             cp.get(PYROSCOPE_BASIC_AUTH_PASSWORD_CONFIG));
     }
 
+    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) {
         String applicationName = configurationProvider.get(PYROSCOPE_APPLICATION_NAME_CONFIG);
         if (applicationName == null || applicationName.isEmpty()) {
@@ -635,6 +649,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 = "";
@@ -668,6 +683,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;
@@ -839,6 +855,7 @@ public Config build() {
             }
             return new Config(agentEnabled,
                 applicationName,
+                profilerType,
                 profilingInterval,
                 profilingEvent,
                 profilingAlloc,
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..8a7c778
--- /dev/null
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
@@ -0,0 +1,5 @@
+package io.pyroscope.javaagent.config;
+
+public enum ProfilerType {
+    JFR, ASYNC;
+}

From 05f3853e358d8ba5b2a7cae873a5531203c7b3d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Fri, 29 Dec 2023 12:13:59 +0100
Subject: [PATCH 04/11] added JFR configuration which enables only supported
 JFR event types

---
 .../javaagent/AsyncProfilerDelegate.java      |  2 +-
 .../javaagent/CurrentPidProvider.java         | 11 ++-
 .../javaagent/JFRProfilerDelegate.java        | 74 ++++++++++---------
 .../pyroscope/javaagent/ProfilerDelegate.java | 11 +++
 .../pyroscope/javaagent/PyroscopeAgent.java   |  6 +-
 .../io/pyroscope/javaagent/config/Config.java |  7 +-
 .../javaagent/config/ProfilerType.java        | 23 +++++-
 agent/src/main/resources/pyroscope.jfc        | 44 +++++++++++
 8 files changed, 131 insertions(+), 47 deletions(-)
 create mode 100644 agent/src/main/resources/pyroscope.jfc

diff --git a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
index 68e993b..8362855 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
@@ -27,7 +27,7 @@ public final class AsyncProfilerDelegate implements ProfilerDelegate {
 
     private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler();
 
-    AsyncProfilerDelegate(Config config) {
+    public AsyncProfilerDelegate(Config config) {
         reset(config);
     }
 
diff --git a/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
index 869b929..089651e 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
@@ -10,18 +10,17 @@
 
 public class CurrentPidProvider {
     public static int getCurrentProcessId() {
-
         RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
         Field jvm = null;
         try {
             jvm = runtime.getClass().getDeclaredField("jvm");
-        jvm.setAccessible(true);
+            jvm.setAccessible(true);
 
-        VMManagement management = (VMManagement) jvm.get(runtime);
-        Method method = management.getClass().getDeclaredMethod("getProcessId");
-        method.setAccessible(true);
+            VMManagement management = (VMManagement) jvm.get(runtime);
+            Method method = management.getClass().getDeclaredMethod("getProcessId");
+            method.setAccessible(true);
 
-        return (Integer) method.invoke(management);
+            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
index 2c29630..aefc735 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -3,31 +3,29 @@
 import io.pyroscope.http.Format;
 import io.pyroscope.javaagent.config.Config;
 import io.pyroscope.labels.Pyroscope;
-import io.pyroscope.labels.io.pyroscope.PyroscopeAsyncProfiler;
-import one.profiler.AsyncProfiler;
-import one.profiler.Counter;
 
-import java.io.DataInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
 public final class JFRProfilerDelegate implements ProfilerDelegate {
+    private static final String RECORDING_NAME = "pyroscope";
     private Config config;
     private File tempJFRFile;
+    private Path jcmdBin;
 
-    JFRProfilerDelegate(Config config) {
+    public JFRProfilerDelegate(Config config) {
         setConfig(config);
     }
 
     public void setConfig(final Config config) {
         this.config = config;
-
+        jcmdBin = findJcmdBin();
         try {
             // flight recorder is built on top of a file descriptor, so we need a file.
             tempJFRFile = File.createTempFile("pyroscope", ".jfr");
@@ -43,15 +41,16 @@ public void setConfig(final Config config) {
     public synchronized void start() {
         try {
             List commands = new ArrayList<>();
-            commands.add("jcmd");
+            commands.add(jcmdBin.toString());
             commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
             commands.add("JFR.start");
-            commands.add("name=Pyroscope");
-            commands.add("filename="+tempJFRFile.getAbsolutePath());
+            commands.add("name=" + RECORDING_NAME);
+            commands.add("filename=" + tempJFRFile.getAbsolutePath());
+            commands.add("settings=pyroscope");
             ProcessBuilder processBuilder = new ProcessBuilder(commands);
             Process process = processBuilder.start();
             int exitCode = process.waitFor();
-            if (exitCode != 0){
+            if (exitCode != 0) {
                 throw new RuntimeException("Invalid exit code: " + exitCode);
             }
         } catch (IOException e) {
@@ -67,14 +66,14 @@ public synchronized void start() {
     public synchronized void stop() {
         try {
             List commands = new ArrayList<>();
-            commands.add("jcmd");
+            commands.add(jcmdBin.toString());
             commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
             commands.add("JFR.stop");
-            commands.add("name=Pyroscope");
+            commands.add("name=" + RECORDING_NAME);
             ProcessBuilder processBuilder = new ProcessBuilder(commands);
             Process process = processBuilder.start();
             int exitCode = process.waitFor();
-            if (exitCode != 0){
+            if (exitCode != 0) {
                 throw new RuntimeException("Invalid exit code: " + exitCode);
             }
         } catch (IOException e) {
@@ -85,9 +84,8 @@ public synchronized void 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}
      */
     public synchronized Snapshot dumpProfile(Instant started, Instant ended) {
@@ -98,27 +96,31 @@ private Snapshot dumpImpl(Instant started, Instant ended) {
         if (config.gcBeforeDump) {
             System.gc();
         }
-        final byte[] data;
-            data = dumpJFR();
-        return new Snapshot(
-            Format.JFR,
-            EventType.CPU,
-            started,
-            ended,
-            data,
-            Pyroscope.LabelsWrapper.dump()
-        );
-    }
-
-    private byte[] dumpJFR() {
         try {
-            byte[] bytes = new byte[(int) tempJFRFile.length()];
-            try (DataInputStream ds = new DataInputStream(new FileInputStream(tempJFRFile))) {
-                ds.readFully(bytes);
-            }
-            return bytes;
+            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"));
+        //find jcmd binary
+        Path jcmdBin = javaHome.resolve("bin/jcmd");
+        if (!Files.isExecutable(jcmdBin)) {
+            jcmdBin = javaHome.getParent().resolve("bin/jcmd");
+            if (!Files.isExecutable(jcmdBin)) {
+                throw new RuntimeException("cannot find executable jcmd in Java home");
+            }
+        }
+        return jcmdBin;
+    }
 }
diff --git a/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java
index 0801687..313d0d1 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java
@@ -1,10 +1,21 @@
 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();
diff --git a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java
index 0bab2cd..f5e94a6 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java
@@ -105,14 +105,13 @@ private Options(Builder b) {
 
         public static class Builder {
             final Config config;
-            final ProfilerDelegate profiler;
+            ProfilerDelegate profiler;
             Exporter exporter;
             ProfilingScheduler scheduler;
             Logger logger;
 
             public Builder(Config config) {
                 this.config = config;
-                this.profiler = new AsyncProfilerDelegate(config);
             }
 
             public Builder setExporter(Exporter exporter) {
@@ -144,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/config/Config.java b/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
index 1bcf950..6cfb7bd 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/Config.java
@@ -90,7 +90,7 @@ public final class Config {
 
     public final boolean agentEnabled;
     public final String applicationName;
-    private final ProfilerType profilerType;
+    public final ProfilerType profilerType;
     public final Duration profilingInterval;
     public final EventType profilingEvent;
     public final String profilingAlloc;
@@ -849,6 +849,11 @@ 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();
diff --git a/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java b/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
index 8a7c778..783b146 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java
@@ -1,5 +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, ASYNC;
+    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/resources/pyroscope.jfc b/agent/src/main/resources/pyroscope.jfc
new file mode 100644
index 0000000..51ef780
--- /dev/null
+++ b/agent/src/main/resources/pyroscope.jfc
@@ -0,0 +1,44 @@
+
+
+
+
+    
+        true
+        1 ms
+    
+
+    
+        true
+        true
+        10 ms
+    
+
+    
+        true
+        true
+    
+
+    
+        true
+        true
+    
+
+    
+        true
+        true
+        10 ms
+    
+
+    
+        true
+        true
+        10 ms
+    
+
+    
+        true
+    
+
+

From 8acf3c49e74c1871d18a2685eb189505d51e00bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Wed, 10 Jan 2024 15:38:46 +0100
Subject: [PATCH 05/11] addressed comments to handle custom JFR settings

---
 .../javaagent/AsyncProfilerDelegate.java      |  8 +++--
 .../javaagent/JFRProfilerDelegate.java        | 32 ++++++++++++++++---
 .../main/resources/{ => jfr}/pyroscope.jfc    |  0
 demo/build.gradle                             |  1 +
 4 files changed, 33 insertions(+), 8 deletions(-)
 rename agent/src/main/resources/{ => jfr}/pyroscope.jfc (100%)

diff --git a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
index 8362855..e8d4e79 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
@@ -31,6 +31,7 @@ public AsyncProfilerDelegate(Config config) {
         reset(config);
     }
 
+    @Override
     public void reset(final Config config) {
         this.config = config;
         this.alloc = config.profilingAlloc;
@@ -53,6 +54,7 @@ public void reset(final Config config) {
     /**
      * Start async-profiler
      */
+    @Override
     public synchronized void start() {
         if (format == Format.JFR) {
             try {
@@ -68,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/JFRProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
index aefc735..7a4e4f7 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -6,26 +6,33 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.*;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
+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 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();
+
         try {
             // flight recorder is built on top of a file descriptor, so we need a file.
             tempJFRFile = File.createTempFile("pyroscope", ".jfr");
@@ -38,6 +45,7 @@ public void setConfig(final Config config) {
     /**
      * Start JFR profiler
      */
+    @Override
     public synchronized void start() {
         try {
             List commands = new ArrayList<>();
@@ -46,9 +54,9 @@ public synchronized void start() {
             commands.add("JFR.start");
             commands.add("name=" + RECORDING_NAME);
             commands.add("filename=" + tempJFRFile.getAbsolutePath());
-            commands.add("settings=pyroscope");
+            commands.add("settings=" + jfrSettingsPath);
             ProcessBuilder processBuilder = new ProcessBuilder(commands);
-            Process process = processBuilder.start();
+            Process process = processBuilder.inheritIO().start();
             int exitCode = process.waitFor();
             if (exitCode != 0) {
                 throw new RuntimeException("Invalid exit code: " + exitCode);
@@ -63,6 +71,7 @@ public synchronized void start() {
     /**
      * Stop JFR profiler
      */
+    @Override
     public synchronized void stop() {
         try {
             List commands = new ArrayList<>();
@@ -88,6 +97,7 @@ public synchronized void stop() {
      * @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);
     }
@@ -123,4 +133,16 @@ private static Path findJcmdBin() {
         }
         return jcmdBin;
     }
+
+    private static Path findJfrSettingsPath() {
+        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);
+        }
+    }
+
+
 }
diff --git a/agent/src/main/resources/pyroscope.jfc b/agent/src/main/resources/jfr/pyroscope.jfc
similarity index 100%
rename from agent/src/main/resources/pyroscope.jfc
rename to agent/src/main/resources/jfr/pyroscope.jfc
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")
 }

From d90cb70a891e90753ff498251908126b2162bee5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Wed, 10 Jan 2024 16:15:44 +0100
Subject: [PATCH 06/11] updated README on how to use JFR based agent profiler

---
 README.md | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

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
 

From 295783aa76d93303c6dbe4f32f5d098ddde268e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Fri, 2 Feb 2024 08:59:11 +0100
Subject: [PATCH 07/11] proper name for exectuable under windows os

---
 .../javaagent/JFRProfilerDelegate.java        | 33 +++++++++++++++----
 1 file changed, 26 insertions(+), 7 deletions(-)

diff --git a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
index 7a4e4f7..342571f 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -8,7 +8,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UncheckedIOException;
-import java.nio.file.*;
+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;
@@ -18,6 +21,8 @@
 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;
@@ -85,10 +90,8 @@ public synchronized void stop() {
             if (exitCode != 0) {
                 throw new RuntimeException("Invalid exit code: " + exitCode);
             }
-        } catch (IOException e) {
-            throw new IllegalStateException(e);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
+        } catch (IOException | InterruptedException e) {
+            throw new RuntimeException("cannot stop JFR recording", e);
         }
     }
 
@@ -123,10 +126,11 @@ private Snapshot dumpImpl(Instant started, Instant ended) {
 
     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
-        Path jcmdBin = javaHome.resolve("bin/jcmd");
         if (!Files.isExecutable(jcmdBin)) {
-            jcmdBin = javaHome.getParent().resolve("bin/jcmd");
+            jcmdBin = javaHome.getParent().resolve("bin").resolve(jcmd);
             if (!Files.isExecutable(jcmdBin)) {
                 throw new RuntimeException("cannot find executable jcmd in Java home");
             }
@@ -134,6 +138,14 @@ private static Path findJcmdBin() {
         return jcmdBin;
     }
 
+    private static String jcmdExecutable() {
+        String jcmd = "jcmd";
+        if (isWindowsOS()) {
+            jcmd = "jcmd.exe";
+        }
+        return jcmd;
+    }
+
     private static Path findJfrSettingsPath() {
         try (InputStream inputStream = JFRProfilerDelegate.class.getResourceAsStream(JFR_SETTINGS_RESOURCE)) {
             Path jfrSettingsPath = Files.createTempFile("pyroscope", ".jfc");
@@ -144,5 +156,12 @@ private static Path findJfrSettingsPath() {
         }
     }
 
+    private static boolean isWindowsOS() {
+        String osName = System.getProperty(OS_NAME);
+        if (osName.contains("Windows")) {
+            return true;
+        }
+        return false;
+    }
 
 }

From 2b7069c2762b1b711406004cefe7b6c915942108 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Mon, 24 Jun 2024 15:58:40 +0200
Subject: [PATCH 08/11] rebased latest version

---
 .../javaagent/AsyncProfilerDelegate.java      |  4 +-
 .../javaagent/CurrentPidProvider.java         | 39 ++++++++-----------
 .../impl/ContinuousProfilingScheduler.java    |  3 +-
 .../impl/SamplingProfilingScheduler.java      |  2 +-
 4 files changed, 20 insertions(+), 28 deletions(-)

diff --git a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
index e8d4e79..40818c9 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java
@@ -28,11 +28,11 @@ public final class AsyncProfilerDelegate implements ProfilerDelegate {
     private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler();
 
     public AsyncProfilerDelegate(Config config) {
-        reset(config);
+        setConfig(config);
     }
 
     @Override
-    public void reset(final Config config) {
+    public void setConfig(final Config config) {
         this.config = config;
         this.alloc = config.profilingAlloc;
         this.lock = config.profilingLock;
diff --git a/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
index 089651e..e85e79d 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java
@@ -1,28 +1,21 @@
 package io.pyroscope.javaagent;
 
-import sun.management.VMManagement;
-
-import java.lang.management.ManagementFactory;
-import java.lang.management.RuntimeMXBean;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
 public class CurrentPidProvider {
-    public static int getCurrentProcessId() {
-        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);
-        }
+    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/impl/ContinuousProfilingScheduler.java b/agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java
index 2de2cab..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,5 @@
 package io.pyroscope.javaagent.impl;
 
-import io.pyroscope.javaagent.AsyncProfilerDelegate;
 import io.pyroscope.javaagent.ProfilerDelegate;
 import io.pyroscope.javaagent.Snapshot;
 import io.pyroscope.javaagent.api.Exporter;
@@ -30,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;
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 98e793f..f008155 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java
@@ -56,7 +56,7 @@ public void start(ProfilerDelegate 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);
             }
         } :

From 9521d602c9338be447b3ebaca3cc508f44635121 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Mon, 24 Jun 2024 16:34:47 +0200
Subject: [PATCH 09/11] fixed failing JFR event parsing

---
 agent/src/main/resources/jfr/pyroscope.jfc | 11 -----------
 demo/src/main/java/App.java                |  2 ++
 2 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/agent/src/main/resources/jfr/pyroscope.jfc b/agent/src/main/resources/jfr/pyroscope.jfc
index 51ef780..70a522d 100644
--- a/agent/src/main/resources/jfr/pyroscope.jfc
+++ b/agent/src/main/resources/jfr/pyroscope.jfc
@@ -30,15 +30,4 @@
         true
         10 ms
     
-
-    
-        true
-        true
-        10 ms
-    
-
-    
-        true
-    
-
 
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()

From eb36c90f37b91453cd0faa739e15b379ae109b5e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Wed, 3 Jul 2024 12:00:36 +0200
Subject: [PATCH 10/11] printing jcmd command output only on error

---
 .../javaagent/JFRProfilerDelegate.java        | 84 ++++++++-----------
 1 file changed, 36 insertions(+), 48 deletions(-)

diff --git a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
index 342571f..8751bb6 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -4,10 +4,7 @@
 import io.pyroscope.javaagent.config.Config;
 import io.pyroscope.labels.Pyroscope;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
+import java.io.*;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -15,6 +12,7 @@
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import static java.lang.String.format;
 
@@ -39,7 +37,6 @@ public void setConfig(final Config config) {
         jfrSettingsPath = findJfrSettingsPath();
 
         try {
-            // flight recorder is built on top of a file descriptor, so we need a file.
             tempJFRFile = File.createTempFile("pyroscope", ".jfr");
             tempJFRFile.deleteOnExit();
         } catch (IOException e) {
@@ -52,25 +49,14 @@ public void setConfig(final Config config) {
      */
     @Override
     public synchronized void start() {
-        try {
-            List commands = new ArrayList<>();
-            commands.add(jcmdBin.toString());
-            commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
-            commands.add("JFR.start");
-            commands.add("name=" + RECORDING_NAME);
-            commands.add("filename=" + tempJFRFile.getAbsolutePath());
-            commands.add("settings=" + jfrSettingsPath);
-            ProcessBuilder processBuilder = new ProcessBuilder(commands);
-            Process process = processBuilder.inheritIO().start();
-            int exitCode = process.waitFor();
-            if (exitCode != 0) {
-                throw new RuntimeException("Invalid exit code: " + exitCode);
-            }
-        } catch (IOException e) {
-            throw new IllegalStateException(e);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
+        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);
     }
 
     /**
@@ -78,21 +64,12 @@ public synchronized void start() {
      */
     @Override
     public synchronized void stop() {
-        try {
-            List commands = new ArrayList<>();
-            commands.add(jcmdBin.toString());
-            commands.add(String.valueOf(CurrentPidProvider.getCurrentProcessId()));
-            commands.add("JFR.stop");
-            commands.add("name=" + RECORDING_NAME);
-            ProcessBuilder processBuilder = new ProcessBuilder(commands);
-            Process process = processBuilder.start();
-            int exitCode = process.waitFor();
-            if (exitCode != 0) {
-                throw new RuntimeException("Invalid exit code: " + exitCode);
-            }
-        } catch (IOException | InterruptedException e) {
-            throw new RuntimeException("cannot stop JFR recording", e);
-        }
+        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);
     }
 
     /**
@@ -112,12 +89,12 @@ private Snapshot dumpImpl(Instant started, Instant ended) {
         try {
             byte[] data = Files.readAllBytes(tempJFRFile.toPath());
             return new Snapshot(
-                Format.JFR,
-                EventType.CPU,
-                started,
-                ended,
-                data,
-                Pyroscope.LabelsWrapper.dump()
+                    Format.JFR,
+                    EventType.CPU,
+                    started,
+                    ended,
+                    data,
+                    Pyroscope.LabelsWrapper.dump()
             );
         } catch (IOException e) {
             throw new IllegalStateException(e);
@@ -158,10 +135,21 @@ private static Path findJfrSettingsPath() {
 
     private static boolean isWindowsOS() {
         String osName = System.getProperty(OS_NAME);
-        if (osName.contains("Windows")) {
-            return true;
+        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);
         }
-        return false;
     }
 
 }

From 6713966e94fed88238c809ca6025db2794c1a09b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaros=C5=82aw=20Pa=C5=82ka?= 
Date: Wed, 3 Jul 2024 12:18:53 +0200
Subject: [PATCH 11/11] added config parameter to overwrite JFR settings

---
 .../javaagent/JFRProfilerDelegate.java        | 21 ++--
 .../io/pyroscope/javaagent/config/Config.java | 99 ++++++++++++-------
 2 files changed, 74 insertions(+), 46 deletions(-)

diff --git a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
index 8751bb6..8319d3a 100644
--- a/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
+++ b/agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
@@ -34,7 +34,7 @@ public JFRProfilerDelegate(Config config) {
     public void setConfig(final Config config) {
         this.config = config;
         jcmdBin = findJcmdBin();
-        jfrSettingsPath = findJfrSettingsPath();
+        jfrSettingsPath = findJfrSettingsPath(config);
 
         try {
             tempJFRFile = File.createTempFile("pyroscope", ".jfr");
@@ -89,12 +89,12 @@ private Snapshot dumpImpl(Instant started, Instant ended) {
         try {
             byte[] data = Files.readAllBytes(tempJFRFile.toPath());
             return new Snapshot(
-                    Format.JFR,
-                    EventType.CPU,
-                    started,
-                    ended,
-                    data,
-                    Pyroscope.LabelsWrapper.dump()
+                Format.JFR,
+                EventType.CPU,
+                started,
+                ended,
+                data,
+                Pyroscope.LabelsWrapper.dump()
             );
         } catch (IOException e) {
             throw new IllegalStateException(e);
@@ -123,7 +123,12 @@ private static String jcmdExecutable() {
         return jcmd;
     }
 
-    private static Path findJfrSettingsPath() {
+    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);
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 6cfb7bd..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;
@@ -67,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);
@@ -101,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;
@@ -135,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,
@@ -163,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);
@@ -206,32 +216,33 @@ public long profilingIntervalInHertz() {
     @Override
     public String toString() {
         return "Config{" +
-            "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 + '\'' +
-            ", 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() {
@@ -270,6 +281,7 @@ public static Config build(ConfigurationProvider cp) {
             logLevel(cp),
             serverAddress(cp),
             authToken(cp),
+            jfrProfilerSettings(cp),
             format(cp),
             pushQueueCapacity(cp),
             labels(cp),
@@ -283,8 +295,15 @@ 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) {
@@ -391,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
@@ -592,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();
         }
     }
@@ -676,6 +692,7 @@ public static class Builder {
         private String APExtraArguments = null;
         private String basicAuthUser;
         private String basicAuthPassword;
+        private String jfrProfilerSettings;
 
         public Builder() {
         }
@@ -694,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;
@@ -769,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;
@@ -871,6 +894,7 @@ public Config build() {
                 logLevel,
                 serverAddress,
                 authToken,
+                jfrProfilerSettings,
                 format,
                 pushQueueCapacity,
                 labels,
@@ -884,8 +908,7 @@ public Config build() {
                 tenantID,
                 APLogLevel,
                 APExtraArguments,
-                basicAuthUser,
-                basicAuthPassword);
+                basicAuthUser, basicAuthPassword);
         }
     }
 }