Skip to content

Commit

Permalink
Implement better jattach support and bump version
Browse files Browse the repository at this point in the history
  • Loading branch information
parttimenerd committed Apr 28, 2023
1 parent 04019c7 commit f3bd8dd
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 10 deletions.
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ Loader for AsyncProfiler
Packages [async-profiler](https://github.com/jvm-profiling-tools/async-profiler) releases in a JAR
with an `AsyncProfilerLoader` (version 2.* and 1.8.*) that loads the suitable native library for the current platform.

This is usable as a java agent (same arguments as the async-profiler agent) and as the basis for other libraries.
This is usable as a Java agent (same arguments as the async-profiler agent) and as the basis for other libraries.
The real rationale behind this library is that the async-profiler is a nice tool, but it cannot be easily integrated
into other Java-based tools.

The `AsyncProfilerLoader` API integrates async-profiler and jattach with a user-friendly interface (see below).

The wrapper is tested against all relevant tests of the async-profiler tool, ensuring that it has the same behavior.

Take the [`all` build](https://github.com/jvm-profiling-tools/ap-loader/releases/latest/download/ap-loader-all.jar) and you have a JAR that provides the important features of async-profiler on all supported
Expand Down Expand Up @@ -42,7 +44,7 @@ Or you can depend on the artifacts from maven central, they should be slightly m
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>ap-loader-all</artifactId>
<version>2.9-4</version>
<version>2.9-5</version>
</dependency>
```

Expand Down Expand Up @@ -197,6 +199,19 @@ The API of the `AsyncProfilerLoader` can be used to execute all commands of the
The converters reside in the `one.converter` package.
Attaching a Custom Agent Programmatically
---------------------------------
A notable part of the API are the jattach related methods that allow you to call `jattach` to attach
your own native library to the currently running JVM:
```java
// extract the agent first from the resources
Path p = one.profiler.AsyncProfilerLoader.extractCustomLibraryFromResources(....getClassLoader(), "library name");
// attach the agent to the current JVM
one.profiler.AsyncProfilerLoader.jattach(p, "optional arguments")
// -> returns true if jattach succeeded
```
### Releases
```xml
Expand All @@ -213,7 +228,7 @@ The latest `all` version can be added via:
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>ap-loader-all</artifactId>
<version>2.9-4</version>
<version>2.9-5</version>
</dependency>
```
Expand All @@ -233,7 +248,7 @@ For example for the `all` variant of version 2.9:
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>ap-loader-all</artifactId>
<version>2.9-4-SNAPSHOT</version>
<version>2.9-5-SNAPSHOT</version>
</dependency>
```
Expand Down Expand Up @@ -270,11 +285,11 @@ python3 ./bin/releaser.py download 2.9
# build the JAR for the release
# maven might throw warnings, related to the project version setting,
# but the alternative solutions don't work, so we ignore the warning for now
mvn -Dproject.vversion=2.9 -Dproject.subrelease=4 -Dproject.platform=macos package assembly:single
mvn -Dproject.vversion=2.9 -Dproject.subrelease=5 -Dproject.platform=macos package assembly:single
# use it
java -jar target/ap-loader-macos-2.9-4-full.jar ...
java -jar target/ap-loader-macos-2.9-5-full.jar ...
# build the all JAR
mvn -Dproject.vversion=2.9 -Dproject.subrelease=4 -Dproject.platform=all package assembly:single
mvn -Dproject.vversion=2.9 -Dproject.subrelease=5 -Dproject.platform=all package assembly:single
```
Development
Expand Down Expand Up @@ -319,6 +334,13 @@ And the following for a new async-profiler release:
Changelog
---------
### v5
- Add new jattach methods (`AsyncProfilerLoader.jattach(Path agent, String args)`) to make using it programmatically easier
- Add new `AsyncProfilerLoader.extractCustomLibraryFromResources(ClassLoader, String)`
method to extract a custom library from the resources
- this also has a variant that looks in an alternative resource directory if the resource does not exist
### v4
- `AsyncProfiler.isSupported()` now returns `false` if the OS is not supported by any async-profiler binary, fixes #5
Expand Down
8 changes: 6 additions & 2 deletions bin/releaser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
from typing import Any, Dict, List, Union, Tuple, Optional
from urllib import request

SUB_VERSION = 4
RELEASE_NOTES = """- `AsyncProfiler.isSupported()` now returns `false` if the OS is not supported by any async-profiler binary, fixes #5"""
SUB_VERSION = 5
RELEASE_NOTES = """- Add new jattach methods (`AsyncProfilerLoader.jattach(Path agent, String args)`) to make using it programmatically easier
- Add new `AsyncProfilerLoader.extractCustomLibraryFromResources(ClassLoader, String)`
method to extract a custom library from the resources
- this also has a variant that looks in an alternative resource directory if the resource does not exist
"""

HELP = """
Usage:
Expand Down
130 changes: 129 additions & 1 deletion src/main/java/one/profiler/AsyncProfilerLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,86 @@ private static Path copyFromResources(String fileName, Path destination) throws
}
}

/**
* Extracts a custom agent from the resources
*
* <p>
*
* @param classLoader the class loader to load the resources from
* @param fileName the name of the file to copy, maps the library name if the fileName does not start with "lib",
* e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" on macOS
* @return the path of the library
* @throws IOException if the extraction fails
*/
public static Path extractCustomLibraryFromResources(ClassLoader classLoader, String fileName) throws IOException {
return extractCustomLibraryFromResources(classLoader, fileName, null);
}

/**
* Extracts a custom native library from the resources and returns the alternative source
* if the file is not in the resources.
*
* <p>If the file is extracted, then it is copied to a new temporary folder which is deleted upon JVM exit.</p>
*
* <p>This method is mainly seen as a helper method to obtain custom native agents for {@link #jattach(Path)} and
* {@link #jattach(Path, String)}. It is included in ap-loader to make it easier to write applications that need
* custom native libraries.</p>
*
* <p>This method works on all architectures.</p>
*
* @param classLoader the class loader to load the resources from
* @param fileName the name of the file to copy, maps the library name if the fileName does not start with "lib",
* e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" on macOS
* @param alternativeSource the optional resource directory to use if the resource is not found in the resources,
* this is typically the case when running the application from an IDE, an example would be
* "src/main/resources" or "target/classes" for maven projects
* @return the path of the library
* @throws IOException if the extraction fails and the alternative source is not present for the current architecture
*/
public static Path extractCustomLibraryFromResources(ClassLoader classLoader, String fileName, Path alternativeSource) throws IOException {
Path filePath = Paths.get(fileName);
String name = filePath.getFileName().toString();
if (!name.startsWith("lib")) {
name = System.mapLibraryName(name);
}
Path realFilePath = filePath.getParent() == null ? Paths.get(name) : filePath.getParent().resolve(name);
Enumeration<URL> indexFiles = classLoader.getResources(realFilePath.toString());
if (!indexFiles.hasMoreElements()) {
if (alternativeSource == null) {
throw new IOException("Could not find library " + fileName + " in resources");
}
if (!alternativeSource.toFile().isDirectory()) {
throw new IOException("Could not find library " + fileName + " in resources and alternative source " + alternativeSource + " is not a directory");
}
if (alternativeSource.resolve(realFilePath).toFile().exists()) {
return alternativeSource.resolve(realFilePath);
}
throw new IOException("Could not find library " + fileName + " in resources and alternative source " + alternativeSource + " does not contain " + realFilePath);
}
URL url = indexFiles.nextElement();
Path tempDir = Files.createTempDirectory("ap-loader");
try {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try (Stream<Path> stream = Files.walk(getExtractionDirectory())) {
stream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}));
} catch (RuntimeException e) {
throw (IOException) e.getCause();
}
Path destination = tempDir.resolve(name);
try {
try (InputStream in = url.openStream()) {
Files.copy(in, destination);
}
return destination;
} catch (IOException e) {
throw new IOException("Could not copy file " + fileName + " to " + destination, e);
}
}

/**
* Extracts the jattach tool
*
Expand Down Expand Up @@ -405,6 +485,9 @@ private static String[] processJattachArgs(String[] args) throws IOException {
* One can therefore start/stop the async-profiler via <code>
* executeJattach(PID, "load", "libasyncProfiler.so", true, "start"/"stop")</code>.
*
* Use the {@link #jattach(Path)} or {@link #jattach(Path, String)} to load agents via jattach directly,
* without the need to construct the command line arguments yourself.
*
* @throws IOException if something went wrong (e.g. the jattach binary is not found or the
* execution fails)
* @throws IllegalStateException if OS or Arch are not supported
Expand All @@ -417,6 +500,46 @@ private static void executeJattachInteractively(String[] args) throws IOExceptio
executeCommandInteractively("jattach", processJattachArgs(args));
}

/**
* See <a href="https://github.com/apangin/jattach">jattach</a> for more information.
*
* <p>It loads the passed agent via jattach to the current JVM, mapping
* "libasyncProfiler.so" to the extracted async-profiler library for the load command.</p>
*
* @return true if the agent was successfully attached, false otherwise
* @throws IllegalStateException if OS or Arch are not supported
*/
public static boolean jattach(Path agentPath) {
return jattach(agentPath, null);
}

/**
* See <a href="https://github.com/apangin/jattach">jattach</a> for more information.
*
* <p>It loads the passed agent via jattach to the current JVM, mapping
* "libasyncProfiler.so" to the extracted async-profiler library for the load command.</p>
*
* @return true if the agent was successfully attached, false otherwise
* @throws IllegalStateException if OS or Arch are not supported
*/
public static boolean jattach(Path agentPath, String arguments) {
List<String> args = new ArrayList<>();
args.add(String.valueOf(getProcessId()));
args.add("load");
args.add(agentPath.toString());
args.add("true");
if (arguments != null) {
args.add(arguments);
}
try {
executeJattach(args.toArray(new String[0]));
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}

private static String[] processConverterArgs(String[] args) throws IOException {
List<String> argList = new ArrayList<>();
argList.add(System.getProperty("java.home") + "/bin/java");
Expand Down Expand Up @@ -597,7 +720,12 @@ public static void premain(String agentArgs, Instrumentation instrumentation) {
agentmain(agentArgs, instrumentation);
}

private static int getProcessId() {
/**
* Returns the id of the current process
*
* @throws IllegalStateException if the id can not be obtained, this should never happen
*/
public static int getProcessId() {
String name = ManagementFactory.getRuntimeMXBean().getName();
int index = name.indexOf('@');
if (index < 1) {
Expand Down

0 comments on commit f3bd8dd

Please sign in to comment.