Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jfr profiler support #136

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -52,6 +54,7 @@ public void reset(final Config config) {
/**
* Start async-profiler
*/
@Override
public synchronized void start() {
if (format == Format.JFR) {
try {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
// }
}
}
160 changes: 160 additions & 0 deletions agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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();
kcrimson marked this conversation as resolved.
Show resolved Hide resolved

void stop();

Snapshot dumpProfile(Instant profilingStartTime, Instant now);

void setConfig(Config config);
}
11 changes: 7 additions & 4 deletions agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -143,6 +143,9 @@ public Options build() {
scheduler = new SamplingProfilingScheduler(config, exporter, logger);
}
}
if (profiler == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we add a setter for profiler field in the builder? Otherwise it looks like profiler is always null here

profiler = ProfilerDelegate.create(config);
}
return new Options(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
* <pre>
* public void start(Profiler profiler) {
* public void start(AsyncProfilerDelegate profiler) {
* new Thread(() -&#062; {
* while (true) {
* Instant startTime = Instant.now();
Expand All @@ -35,7 +36,7 @@ public interface ProfilingScheduler {
* Github issue #40</a> for more details.
*
**/
void start(Profiler profiler);
void start(ProfilerDelegate profiler);

void stop();
}
Loading