├── .gitignore ├── LICENSE ├── README.md ├── bin ├── copy_all_libs.sh ├── copy_java_sources.py ├── copy_libs.sh ├── releaser.py └── timestamp.py ├── jbang-catalog.json ├── misc └── TestMain.java ├── pom.xml ├── pom_all.xml └── src └── main └── java └── one └── profiler └── AsyncProfilerLoader.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### VS Code ### 16 | .vscode/ 17 | 18 | ### Mac OS ### 19 | .DS_Store 20 | /ap-releases/async-profiler-* 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Loader for AsyncProfiler 2 | ======================== 3 | 4 | [![Maven Central](https://img.shields.io/maven-central/v/me.bechberger/ap-loader-all)](https://search.maven.org/search?q=ap-loader) [![GitHub](https://img.shields.io/github/license/jvm-profiling-tools/ap-loader)](https://github.com/jvm-profiling-tools/ap-loader/blob/main/LICENSE) 5 | 6 | Packages [async-profiler](https://github.com/jvm-profiling-tools/async-profiler) releases in a JAR 7 | with an `AsyncProfilerLoader` (version 4.*, old releases up to -9 support 3.*, 2.* and 1.8.*) 8 | that loads the suitable native library for the current platform. 9 | 10 | It includes the latest [jattach](https://github.com/apangin/jattach) binary. This was previously 11 | part of async-profiler. 12 | 13 | This is usable as a Java agent (same arguments as the async-profiler agent) and as the basis for other libraries. 14 | The real rationale behind this library is that the async-profiler is a nice tool, but it cannot be easily integrated 15 | into other Java-based tools. 16 | 17 | The `AsyncProfilerLoader` API integrates async-profiler and jattach with a user-friendly interface (see below). 18 | 19 | The wrapper is tested against all relevant tests of the async-profiler tool, ensuring that it has the same behavior. 20 | 21 | 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 22 | platforms. 23 | 24 | A changelog can be found in the async-profiler repository, as this library should rarely change itself. 25 | 26 | _This project assumes that you used async-profiler before, if not, don't worry, you can still use this project, 27 | but be aware that its documentation refers you to the async-profiler documentation a lot._ 28 | 29 | _fdtransfer is currently not supported, feel free to create an issue if you need it._ 30 | 31 | I wrote two blog posts about this project: 32 | 33 | 1. [AP-Loader: A new way to use and embed async-profiler](https://mostlynerdless.de/blog/2022/11/21/ap-loader-a-new-way-to-use-and-embed-async-profiler/) 34 | 2. [Using Async-Profiler and Jattach Programmatically with AP-Loader](https://mostlynerdless.de/blog/2023/05/02/using-async-profiler-and-jattach-programmatically-with-ap-loader/) 35 | 36 | Download 37 | -------- 38 | 39 | You can download the latest release from the 40 | [latest release page](https://github.com/jvm-profiling-tools/ap-loader/releases/latest/). 41 | As a shortcut, the wrapper for all platforms can be found 42 | [here](https://github.com/jvm-profiling-tools/ap-loader/releases/latest/download/ap-loader-all.jar). 43 | 44 | It should be up-to-date with the latest async-profiler release, but if not, feel free to create an issue. 45 | 46 | To use the library as a dependency, you can depend on `me.bechberger.ap-loader-:-` 47 | from maven central, e.g: 48 | 49 | ```xml 50 | 51 | me.bechberger 52 | ap-loader-all 53 | 4.0-10 54 | 55 | ``` 56 | 57 | Others are of course available, see [maven central](https://central.sonatype.com/artifact/me.bechberger/ap-loader-all/4.0-10). 58 | 59 | You can also use [JBang](https://jbang.dev) to simplify the usage of ap-loader. There are examples in documentation below. 60 | 61 | Supported Platforms 62 | ------------------- 63 | 64 | - Required Java version: 8 or higher 65 | - Supported OS: Linux and macOS (on all platforms the async-profiler has binaries for) 66 | 67 | Variants 68 | -------- 69 | The JAR can be obtained in the following variants: 70 | 71 | - `macos`, `linux-x64`, ...: `jattach`, `asprof`, `jfrconv` and `libasyncProfiler.so` for the given platform 72 | - `all`: all of the above 73 | 74 | Regarding file sizes: The `all` variant are` `typically around 800KB and the individual variants around 200 to 400KB. 75 | 76 | Commands 77 | -------- 78 | 79 | The following is a more in-depth description of the commands of `java -jar ap-loader.jar`. 80 | 81 | To run with JBang you can do `jbang ap-loader@jvm-profiling-tools/ap-loader` or install it as an application: 82 | 83 | ```sh 84 | jbang app install ap-loader@jvm-profiling-tools/ap-loader 85 | ``` 86 | 87 | and run it directly with `ap-loader` instead of `java -jar ap-loader.jar`. 88 | 89 | If you want to install a specific `ap-loader` rather than latest you can use `jbang app install me.bechberger:ap-loader-all:`. 90 | 91 | Be aware that it is recommended to run the JVM with the 92 | `-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints` flags. 93 | This improves the accuracy of the profiler. 94 | 95 | Overview of the commands: 96 | 97 | ```sh 98 | Usage: java -jar ap-loader.jar [args] 99 | Commands: 100 | help show this help 101 | jattach run the included jattach binary 102 | profiler run the included asprof script 103 | agentpath prints the path of the extracted async-profiler agent 104 | jattachpath prints the path of the extracted jattach binary 105 | supported fails if this JAR does not include a profiler for the current OS and architecture 106 | converter run the included converter 107 | version version of the included async-profiler 108 | clear clear the directory used for storing extracted files 109 | ``` 110 | 111 | ### jattach 112 | 113 | `java -jar ap-loader.jar jattach` is equivalent to calling the suitable `jattach` binary 114 | from [GitHub](https://github.com/apangin/jattach)>: 115 | 116 | ```sh 117 | # Load a native agent 118 | java -jar ap-loader.jar jattach load <.so-path> { true | false, is path absolute? } [ options ] 119 | 120 | # Load a java agent 121 | java -jar ap-loader.jar jattach load instrument false "javaagent.jar=arguments" 122 | 123 | # Running the help command for jcmd 124 | java -jar ap-loader.jar jattach jcmd "help -all" 125 | ``` 126 | 127 | See the [GitHub page of jattach](https://github.com/apangin/jattach) for more details. 128 | 129 | ### profiler 130 | 131 | `java -jar ap-loader.jar profiler` is equivalent to calling the suitable `asprof`: 132 | 133 | ```sh 134 | # Profile a process for `n` seconds 135 | java -jar ap-loader.jar profiler -d 136 | 137 | # Profile a process for allocation, CPU and lock events and save the results to a JFR file 138 | java -jar ap-loader.jar profiler -e alloc,cpu,lock -f 139 | ``` 140 | 141 | See the [GitHub page of async-profiler](https://github.com/jvm-profiling-tools/async-profiler) for more details. 142 | 143 | ### supported 144 | 145 | Allows you to check whether your JAR includes a `jattact`, `profile.sh` and `libasyncProfiler.so` for 146 | your current OS and architecture. 147 | 148 | ### converter 149 | 150 | `java -jar ap-loader.jar converter` is equivalent to calling the included converters: 151 | 152 | ```sh 153 | java -jar ap-loader.jar converter [options] 154 | 155 | # Convert a JFR file to flame graph 156 | java -jar ap-loader.jar converter jfr2flame 157 | ``` 158 | 159 | The available converters depend on the included async-profiler version. 160 | Call `java -jar ap-loader.jar converter` to a list of available converters, see 161 | [the source code on GitHub](https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/converter/Main.java) 162 | for more details. 163 | 164 | ### clear 165 | 166 | Clear the application directory used for storing extracted files, 167 | like `/Users//Library/Application Support/me.bechberger.ap-loader-/` 168 | to redo the extraction on the next run. 169 | 170 | 171 | Running as an agent 172 | ------------------- 173 | 174 | `java -javaagent:ap-loader.jar=` is equivalent to `java -agentpath:libasyncProfiler.so=` 175 | with a suitable library. 176 | This can be used to profile a Java process from the start. 177 | 178 | ```sh 179 | # Profile the application and output a flame graph 180 | java -javaagent:ap-loader.jar=start,event=cpu,file=profile.html 181 | ``` 182 | 183 | With JBang you can do: 184 | 185 | ```sh 186 | jbang --javaagent:ap-loader@jvm-profiling-tools/ap-loader=start,event=cpu,file=profile.html 187 | ``` 188 | 189 | See the [GitHub page of async-profiler](https://github.com/jvm-profiling-tools/async-profiler) for more details. 190 | 191 | Usage in Java Code 192 | ------------------ 193 | 194 | Then you can use the `AsyncProfilerLoader` class to load the native library: 195 | 196 | ```java 197 | AsyncProfiler profiler = one.profiler.AsyncProfilerLoader.load(); 198 | ``` 199 | 200 | `AsyncProfiler` is 201 | the [main API class](https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/api/one/profiler/AsyncProfiler.java) 202 | from the async-profiler.jar. 203 | 204 | The API of the `AsyncProfilerLoader` can be used to execute all commands of the CLI programmatically. 205 | 206 | The converters reside in the `one.converter` package. 207 | 208 | Attaching a Custom Agent Programmatically 209 | --------------------------------- 210 | A notable part of the API are the jattach related methods that allow you to call `jattach` to attach 211 | your own native library to the currently running JVM: 212 | 213 | ```java 214 | // extract the agent first from the resources 215 | Path p = one.profiler.AsyncProfilerLoader.extractCustomLibraryFromResources(....getClassLoader(), "library name"); 216 | // attach the agent to the current JVM 217 | one.profiler.AsyncProfilerLoader.jattach(p, "optional arguments") 218 | // -> returns true if jattach succeeded 219 | ``` 220 | 221 | ### Releases 222 | 223 | ```xml 224 | 225 | me.bechberger 226 | ap-loader-variant 227 | version 228 | 229 | ``` 230 | 231 | The latest `all` version can be added via: 232 | 233 | ```xml 234 | 235 | me.bechberger 236 | ap-loader-all 237 | 4.0-10 238 | 239 | ``` 240 | 241 | Build and test 242 | -------------- 243 | 244 | The following describes how to build the different JARs and how to test them. 245 | It requires a platform supported by async-profiler and Python 3.6+. 246 | 247 | ### Build the JARs using maven 248 | 249 | ```sh 250 | # download the release sources and binaries 251 | python3 ./bin/releaser.py download 4.0 252 | 253 | # build the JAR for the release 254 | # maven might throw warnings, related to the project version setting, 255 | # but the alternative solutions don't work, so we ignore the warning for now 256 | mvn -Dproject.vversion=4.0 -Dproject.subrelease=10 -Dproject.platform=macos package assembly:single 257 | # use it 258 | java -jar target/ap-loader-macos-4.0-10-full.jar ... 259 | # build the all JAR 260 | mvn -Dproject.vversion=4.0 -Dproject.subrelease=10 -Dproject.platform=all package assembly:single 261 | ``` 262 | 263 | Development 264 | ----------- 265 | This project is written in Java 8, to support all relevant platforms. 266 | The feature set should not increase beyond what is currently: 267 | Just build your library on top of it. But I'm of course happy for bug reports and fixes. 268 | 269 | The code is formatted using [google-java-format](https://github.com/google/google-java-format). 270 | 271 | ### bin/releaser.py 272 | 273 | ```sh 274 | Usage: 275 | python3 ./bin/releaser.py ... [release or current if not present] 276 | 277 | Commands: 278 | current_version print the youngest released version of async-profiler 279 | versions print all released versions of async-profiler (supported by this project) 280 | download download and prepare the folders for the given release 281 | build build the wrappers for the given release 282 | test test the given release 283 | deploy_mvn deploy the wrappers for the given release as a snapshot to maven 284 | deploy_gh deploy the wrappers for the given release as a snapshot to GitHub 285 | deploy deploy the wrappers for the given release as a snapshot 286 | deploy_release deploy the wrappers for the given release 287 | clear clear the ap-releases and target folders for a fresh start 288 | ``` 289 | 290 | Deploy the latest version via `bin/releaser.py download build test deploy` as a snapshot. 291 | 292 | For a release use `bin/releaser.py download build test deploy_release`, 293 | but before make sure to do the following for a new sub release: 294 | 295 | - update the version number in the README 296 | - update the changelog in the README and `bin/releaser.py` 297 | - and increment the `SUB_VERSION` variable in `bin/releaser.py` afterwards 298 | 299 | And the following for a new async-profiler release: 300 | - update the version in the README 301 | 302 | Changelog 303 | --------- 304 | 305 | ### v10 306 | 307 | - Drop support for async-profiler < 4.0 versions, as 4.0 changed how its tested 308 | - Major changes in the usage of the JFR conversion made by async-profiler 309 | which are not hidden 310 | - Clean up the code 311 | 312 | ### v9 313 | 314 | - Fix FlameGraph converter [#22](https://github.com/jvm-profiling-tools/ap-loader/issues/22) 315 | 316 | ### v8 317 | 318 | - Support for [async-profiler 3.0](https://github.com/async-profiler/async-profiler/releases/tag/v3.0) 319 | - Breaking changes with async-profiler 3.0: 320 | - async-profiler 3.0 changed the meaning of the `--lib` option from `--lib path full path to libasyncProfiler.so in the container` 321 | to `-l, --lib prepend library names`, so the `AsyncProfilerLoader` will throw an UnsupportedOperation exception 322 | when using the `--lib` option with a path argument and async-profiler 3.0 or higher 323 | 324 | ### v7 325 | 326 | - Drop dev.dirs:directories dependency #13 (thanks to @jsjant for spotting the potential licensing issue and fixing it in #14) 327 | 328 | ### v6 329 | 330 | - Fix Linux Arm64 release #12 (thanks to @dkrawiec-c for fixing this issue) 331 | 332 | ### v5 333 | 334 | - Add new jattach methods (`AsyncProfilerLoader.jattach(Path agent, String args)`) to make using it programmatically easier 335 | - Add new `AsyncProfilerLoader.extractCustomLibraryFromResources(ClassLoader, String)` 336 | method to extract a custom library from the resources 337 | - this also has a variant that looks in an alternative resource directory if the resource does not exist 338 | 339 | ### v4 340 | 341 | - `AsyncProfiler.isSupported()` now returns `false` if the OS is not supported by any async-profiler binary, fixes #5 342 | 343 | ### v3 344 | 345 | - Create specific artifacts for each platform fixing previous issues with maven version updates (issue #4, thanks @ginkel for reporting it) 346 | 347 | 348 | ### v2 349 | - Fixed the library version in the pom #3 again 350 | (thanks to @gavlyukovskiy, @dpsoft and @krzysztofslusarski for spotting the bug) 351 | 352 | ### v1 353 | - Fixed the library version in the pom #3 (thanks to @gavlyukovskiy for spotting the bug) 354 | 355 | ### v0 356 | - 11.11.2022: Improve Converter 357 | 358 | ## License 359 | Apache 2.0, Copyright 2023 SAP SE or an SAP affiliate company, Johannes Bechberger 360 | and ap-loader contributors 361 | 362 | 363 | *This project is maintained by the [SapMachine](https://sapmachine.io) team 364 | at [SAP SE](https://sap.com)* -------------------------------------------------------------------------------- /bin/copy_all_libs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # ./copy_all_libs.sh 3 | 4 | set -e 5 | 6 | OWN_DIR=$(dirname "$0") 7 | VERSION_PLATFORM=$2 8 | # extract the version without the platform suffix 9 | version=$(echo "$VERSION_PLATFORM" | cut -d '-' -f 1) 10 | echo "version: $version" 11 | cd "$1" || exit 1 12 | rm -fr target/classes/libs 13 | mkdir -p target/classes/libs 14 | for f in ap-releases/async-profiler-$version-*; do 15 | if (echo "$f" | grep 'code' || echo "$f" | grep ".tar.gz" || echo "$f" | grep ".zip"); then 16 | continue 17 | fi 18 | # extract the platform suffix 19 | platform=$(echo "$f" | cut -d '-' -f 5-10) 20 | # copy the library 21 | $OWN_DIR/copy_libs.sh "$1" "$version-$platform" no-clean 22 | done -------------------------------------------------------------------------------- /bin/copy_java_sources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | ./copy_java_sources.py 5 | """ 6 | 7 | import os 8 | import shutil 9 | import sys 10 | from pathlib import Path 11 | from typing import Tuple, Optional 12 | 13 | BASEDIR = Path(sys.argv[1]) 14 | RELEASE = sys.argv[2] 15 | VERSION = RELEASE.split("-")[0] 16 | 17 | def either(*dirs: Path) -> Path: 18 | for dir in dirs: 19 | if dir.exists(): 20 | return dir 21 | raise AssertionError(f"None of {', '.join(map(str, dirs))} exists") 22 | 23 | 24 | def either_or_none(*dirs: Path) -> Optional[Path]: 25 | return ([dir for dir in dirs if dir.exists()] + [None]) [0] 26 | 27 | 28 | AP_SOURCE_DIR = either(BASEDIR / "ap-releases" / f"async-profiler-{VERSION}-code" / "src") 29 | AP_CONVERTER_SOURCE_DIR = either(AP_SOURCE_DIR / "converter") 30 | AP_RESOURCES_SOURCE_DIR = either_or_none(AP_SOURCE_DIR / "res", AP_SOURCE_DIR / "main" / "resources") 31 | AP_API_SOURCE_DIR = either(AP_SOURCE_DIR / "api" / "one" / "profiler") 32 | TARGET_SOURCE_DIR = either(BASEDIR / "src" / "main" / "java") 33 | TARGET_ONE_DIR = TARGET_SOURCE_DIR / "one" 34 | TARGET_ONE_PROFILER_DIR = either(TARGET_ONE_DIR / "profiler") 35 | TARGET_CONVERTER_DIR = TARGET_ONE_DIR / "converter" 36 | TARGET_RESOURCES_DIR = BASEDIR / "src" / "main" / "resources" 37 | 38 | 39 | PROJECT_FILES = ["AsyncProfilerLoader.java"] 40 | 41 | DRY_RUN = False 42 | 43 | os.chdir(BASEDIR) 44 | 45 | os.makedirs(TARGET_RESOURCES_DIR, exist_ok=True) 46 | 47 | print("Remove old files") 48 | for f in TARGET_ONE_PROFILER_DIR.glob("*"): 49 | if f.name not in PROJECT_FILES: 50 | if DRY_RUN: 51 | print("would remove " + str(f)) 52 | else: 53 | if f.is_dir(): 54 | f.rmdir() 55 | else: 56 | f.unlink() 57 | 58 | for f in TARGET_ONE_DIR.glob("*"): 59 | if not f.is_dir() or f.name == "profiler": 60 | continue 61 | if DRY_RUN: 62 | print("would remove " + str(f)) 63 | else: 64 | shutil.rmtree(f) 65 | 66 | for f in TARGET_SOURCE_DIR.glob("*.java"): 67 | if DRY_RUN: 68 | print("would remove " + str(f)) 69 | else: 70 | f.unlink() 71 | 72 | print("Copy api files") 73 | assert next(AP_API_SOURCE_DIR.glob("*.java"), "empty") != "empty", f"{AP_API_SOURCE_DIR} is empty" 74 | for f in AP_API_SOURCE_DIR.glob("*.java"): 75 | target_file = TARGET_ONE_PROFILER_DIR / f.name 76 | if DRY_RUN: 77 | print(f"would copy {f} to {target_file}") 78 | else: 79 | shutil.copy(f, target_file) 80 | 81 | if AP_RESOURCES_SOURCE_DIR: 82 | print("Copy converter resource files") 83 | for f in AP_RESOURCES_SOURCE_DIR.glob("*"): 84 | target_file = TARGET_RESOURCES_DIR / f.name 85 | if DRY_RUN: 86 | print(f"would copy {f} to {target_file}") 87 | else: 88 | shutil.copy(f, target_file) 89 | 90 | print("Copy converter directories") 91 | for directory in AP_CONVERTER_SOURCE_DIR.glob("one/*"): 92 | if not directory.is_dir(): 93 | continue 94 | if DRY_RUN: 95 | print(f"would copy {directory} to {TARGET_SOURCE_DIR / 'one' / directory.name}") 96 | else: 97 | shutil.copytree(directory, TARGET_SOURCE_DIR / 'one' / directory.name) 98 | 99 | print("Copy converters") 100 | # Problem: These converters reside in the default package 101 | os.makedirs(TARGET_CONVERTER_DIR, exist_ok=True) 102 | for f in AP_CONVERTER_SOURCE_DIR.glob("*.java"): 103 | target_file = TARGET_CONVERTER_DIR / f.name 104 | if DRY_RUN: 105 | print(f"would modify and copy {f} to {target_file}") 106 | else: 107 | with open(f, "r") as source: 108 | content = "package one.converter;\n" + source.read() 109 | if f.name == "Main.java": 110 | assert "java -cp converter.jar" in content or "jfrconv [options]" in content 111 | content = content.replace("java -cp converter.jar ", "java -cp ap-loader.jar one.converter.") 112 | content = content.replace("jfrconv [options]", "java -jar ap-loader.jar jfrconv [options]") 113 | elif "Usage: java " in content: 114 | content = content.replace("Usage: java ", "Usage: java -cp ap-loader.jar ") 115 | with open(target_file, "w") as target: 116 | target.write(content) 117 | -------------------------------------------------------------------------------- /bin/copy_libs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # ./copy_libs.sh [no-clean] 3 | 4 | set -e 5 | 6 | BASEDIR="$1" 7 | VERSION_PLATFORM=$2 8 | AP_RELEASE="ap-releases/async-profiler-$VERSION_PLATFORM" 9 | OWN_DIR=$(dirname "$0") 10 | 11 | cd "$BASEDIR" || exit 1 12 | 13 | version=$(echo "$2" | cut -d '-' -f 1) 14 | major_version=$(echo "$version" | cut -d '.' -f 1) 15 | minor_version=$(echo "$version" | cut -d '.' -f 2) 16 | if [ -z "$3" ]; then 17 | rm -fr target/classes/libs 18 | fi 19 | mkdir -p target/classes/libs 20 | 21 | # test endings ".so" and ".dylib" in a loop 22 | for ending in "so" "dylib"; do 23 | # if the file exists, copy it 24 | if [ -f "$AP_RELEASE/lib/libasyncProfiler.$ending" ]; then 25 | echo "Copy $AP_RELEASE/lib/libasyncProfiler.$ending" 26 | cp "$AP_RELEASE/lib/libasyncProfiler.$ending" \ 27 | "target/classes/libs/libasyncProfiler-$VERSION_PLATFORM.$ending" 28 | echo "libasyncProfiler-$VERSION_PLATFORM.$ending" > target/classes/libs/ap-profile-lib-$VERSION_PLATFORM 29 | fi 30 | done 31 | 32 | cp "$AP_RELEASE/build/jattach" \ 33 | "target/classes/libs/jattach-$VERSION_PLATFORM" 34 | 35 | cp "$AP_RELEASE/build/jattach" \ 36 | "target/classes/libs/jattach-$VERSION_PLATFORM" 37 | 38 | cp "$AP_RELEASE/bin/jfrconv" \ 39 | "target/classes/libs/jfrconv-$VERSION_PLATFORM" 40 | 41 | python3 "$OWN_DIR/timestamp.py" > "target/classes/libs/ap-timestamp-$version" 42 | echo "$version" > target/classes/libs/ap-version 43 | 44 | echo "Copy $AP_RELEASE/bin/asprof" 45 | cp "$AP_RELEASE/bin/asprof" "target/classes/libs/asprof-$VERSION_PLATFORM" 46 | 47 | echo "asprof-$VERSION_PLATFORM" > target/classes/libs/ap-profile-script-$VERSION_PLATFORM 48 | 49 | echo "Copy Java sources" 50 | python3 "$OWN_DIR/copy_java_sources.py" "$BASEDIR" "$VERSION_PLATFORM" -------------------------------------------------------------------------------- /bin/releaser.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | """ 4 | This is the script to pull async-profiler releases from GitHub, create the wrappers for them (using the POMs) and 5 | to test them. 6 | 7 | It has no dependencies, only requiring Python 3.6+ to be installed. 8 | """ 9 | import json 10 | import os 11 | import re 12 | import shutil 13 | import subprocess 14 | import sys 15 | import tempfile 16 | import time 17 | from enum import Enum 18 | from math import expm1 19 | from pathlib import Path 20 | from typing import Any, Dict, List, Union, Tuple, Optional 21 | from urllib import request 22 | 23 | SUB_VERSION = 10 24 | RELEASE_NOTES = """- Drop support for async-profiler < 4.0 versions, as 4.0 changed how its tested 25 | - Major changes in the usage of the JFR conversion made by async-profiler 26 | which are not hidden 27 | - Clean up the code 28 | """ 29 | 30 | HELP = """ 31 | Usage: 32 | . python3 releaser.py ... [release or current if not present] 33 | 34 | Commands: 35 | current_version print the youngest released version of async-profiler 36 | versions print all released versions of async-profiler (supported by this project) 37 | download download and prepare the folders for the given release 38 | build build the wrappers for the given release 39 | test test the given release 40 | deploy_mvn deploy the wrappers for the given release as a snapshot to maven 41 | deploy_gh deploy the wrappers for the given release as a snapshot to GitHub 42 | deploy deploy the wrappers for the given release as a snapshot 43 | deploy_release deploy the wrappers for the given release 44 | clear clear the ap-releases and target folders for a fresh start 45 | """ 46 | 47 | CURRENT_DIR = os.path.abspath( 48 | os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 49 | 50 | CACHE_DIR = f"{CURRENT_DIR}/.cache" 51 | TESTS_DIR = f"{CURRENT_DIR}/.tests" 52 | TESTS_CODE_DIR = f"{TESTS_DIR}_code" 53 | CACHE_TIME = 60 * 60 * 24 # one day 54 | 55 | 56 | def prepare_poms(release: str, platform: str, snapshot: bool = True) -> Tuple[ 57 | str, str]: 58 | """ Prepare the POMs for the given release and platform """ 59 | folder = CURRENT_DIR 60 | os.makedirs(folder, exist_ok=True) 61 | suffix = f"-{release}{'-SNAPSHOT' if snapshot else ''}" 62 | for pom in ["pom", "pom_all"]: 63 | pom_file = f"{CURRENT_DIR}/{pom}.xml" 64 | dest_pom = f"{folder}/{pom}{suffix}.xml" 65 | with open(pom_file) as f: 66 | pom_content = f.read() 67 | p_suffix = "-SNAPSHOT" if snapshot else "" 68 | pom_content = re.sub(r".*", 69 | f"{release}-{SUB_VERSION}{p_suffix}", 70 | pom_content, count=1) 71 | pom_content = pom_content.replace("${project.platform}", platform) 72 | pom_content = pom_content.replace("${project.artifactId}", 73 | f"ap-loader-{platform}") 74 | pom_content = re.sub(r".*", 75 | f"{release}", 76 | pom_content, count=1) 77 | pom_content = re.sub(r".*", 78 | f"{SUB_VERSION}", 79 | pom_content, count=1) 80 | pom_content = re.sub(r".*", 81 | f"{platform}", 82 | pom_content, count=1) 83 | pom_content = re.sub(r".*", 84 | f"{p_suffix}", 85 | pom_content, count=1) 86 | with open(dest_pom, "w") as f2: 87 | f2.write(pom_content) 88 | return f"{folder}/pom{suffix}.xml", f"{folder}/pom_all{suffix}.xml" 89 | 90 | 91 | class PreparedPOMs: 92 | 93 | def __init__(self, release: str, platform: str, snapshot: bool = True): 94 | self.release = release 95 | self.platform = platform 96 | self.snapshot = snapshot 97 | 98 | def __enter__(self): 99 | self.pom, self.pom_all = prepare_poms(self.release, self.platform, 100 | self.snapshot) 101 | return self 102 | 103 | def __exit__(self, *args): 104 | os.remove(self.pom) 105 | os.remove(self.pom_all) 106 | 107 | 108 | def execute(args: Union[List[str], str]): 109 | subprocess.check_call(args, cwd=CURRENT_DIR, shell=isinstance(args, str), 110 | stdout=subprocess.DEVNULL) 111 | 112 | 113 | def download_json(url, path: str) -> Any: 114 | """ Download the JSON file from the given URL and save it to the given path, return the JSON object """ 115 | if not os.path.exists(CACHE_DIR): 116 | os.makedirs(CACHE_DIR) 117 | 118 | cache_path = f"{CACHE_DIR}/{path}" 119 | 120 | if not os.path.exists(cache_path) or os.path.getmtime( 121 | cache_path) + CACHE_TIME <= time.time(): 122 | request.urlretrieve(url, cache_path) 123 | 124 | with open(cache_path) as f: 125 | return json.load(f) 126 | 127 | 128 | class Tool(Enum): 129 | ASYNC_PROFILER = "async-profiler" 130 | JATTACH = "jattach" 131 | 132 | 133 | def get_releases(tool: Tool) -> List[Dict[str, Any]]: 134 | """ Return the releases of async-profiler from the GitHub API """ 135 | if tool == Tool.ASYNC_PROFILER: 136 | return download_json( 137 | "https://api.github.com/repos/jvm-profiling-tools/async-profiler/releases", 138 | "releases.json") 139 | elif tool == Tool.JATTACH: 140 | return download_json( 141 | "https://api.github.com/repos/jattach/jattach/releases", 142 | "jattach_releases.json") 143 | else: 144 | raise Exception("Unknown tool: " + tool.name) 145 | 146 | 147 | def get_release(tool: Tool, release: str) -> Dict[str, Any]: 148 | rel = [release_dict for release_dict in get_releases(tool) if 149 | release_dict["tag_name"] == "v" + release] 150 | if rel: 151 | return rel[0] 152 | print( 153 | f"Release {release} for {tool} not found, available releases are: {', '.join(get_release_versions(tool))}", 154 | file=sys.stderr) 155 | sys.exit(1) 156 | 157 | 158 | def get_release_versions(tool: Tool) -> List[str]: 159 | return [release["tag_name"][1:] for release in get_releases(tool) if 160 | not release["tag_name"][1:].startswith("1.") and 161 | release["tag_name"][-1].isdigit()] 162 | 163 | 164 | def get_most_recent_release(tool: Tool) -> str: 165 | def check_version(version: str) -> bool: 166 | if tool == Tool.ASYNC_PROFILER: 167 | return version.startswith("4.") 168 | return version.startswith( "2.") 169 | return [version for version in get_release_versions(tool) if check_version(version)][0] 170 | 171 | 172 | def get_release_info(tool: Tool, release: str) -> str: 173 | return get_release(tool, release)["body"] 174 | 175 | 176 | def is_tar_gz(url: str) -> bool: 177 | return url.endswith(".tar.gz") or url.endswith(".tgz") 178 | 179 | 180 | def is_zip(url: str) -> bool: 181 | return url.endswith(".zip") 182 | 183 | 184 | def is_compressed_file(url: str) -> bool: 185 | return is_tar_gz(url) or is_zip(url) 186 | 187 | 188 | def get_release_asset_urls(tool: Tool, release: str) -> List[str]: 189 | return [url for url in (asset["browser_download_url"] for asset in 190 | get_release(tool, release)["assets"]) if 191 | is_compressed_file(url)] 192 | 193 | 194 | def release_platform_for_url(archive_url: str) -> str: 195 | if "jattach" in archive_url: 196 | return archive_url.split("jattach-")[-1].split(".")[-2] 197 | return re.split("[0-9][0-9.]+-", archive_url.split("/")[-1])[-1].split(".")[ 198 | 0] 199 | 200 | 201 | def get_release_platforms(tool: Tool, release: str) -> List[str]: 202 | return [release_platform_for_url(url) for url in 203 | get_release_asset_urls(tool, release)] 204 | 205 | 206 | def get_ending(url: str) -> str: 207 | return \ 208 | [ending for ending in [".zip", ".tar.gz", ".tgz"] if url.endswith(ending)][ 209 | 0] 210 | 211 | 212 | def download_latest_jattach(dest_path: Path, platform: str): 213 | """ Download the latest jattach release from GitHub """ 214 | release = get_most_recent_release(Tool.JATTACH) 215 | urls = get_release_asset_urls(Tool.JATTACH, release) 216 | url = ([url for url in urls if 217 | is_compressed_file(url) and release_platform_for_url( 218 | url) == platform] + [None])[0] 219 | if not url: 220 | raise Exception(f"Could not find jattach release for {platform}") 221 | ending = get_ending(url) 222 | archive_file = f"{CACHE_DIR}/jattach-{release}-{platform}{ending}" 223 | if not os.path.exists(archive_file): 224 | request.urlretrieve(url, archive_file) 225 | Path(dest_path).parent.mkdir(parents=True, exist_ok=True) 226 | execute(["tar", "-xzf", archive_file, "-C", dest_path.parent]) 227 | 228 | 229 | def download_release_url(release: str, release_url: str): 230 | """ 231 | Download the release from the given URL and unpack it 232 | 233 | Downloads the latest jattach release too if needed 234 | """ 235 | dest_folder = f"{CURRENT_DIR}/ap-releases" 236 | os.makedirs(dest_folder, exist_ok=True) 237 | ending = get_ending(release_url) 238 | platform = release_platform_for_url(release_url) 239 | archive_file = f"{dest_folder}/async-profiler-{release}-{platform}{ending}" 240 | if not os.path.exists(archive_file): 241 | request.urlretrieve(release_url, archive_file) 242 | if release_url.endswith(".zip"): 243 | execute(["unzip", "-o", archive_file, "-d", dest_folder]) 244 | elif release_url.endswith(".tar.gz"): 245 | execute(["tar", "-xzf", archive_file, "-C", dest_folder]) 246 | else: 247 | raise Exception("Unknown archive type for " + release_url) 248 | # if dest-folder doesn't contain jattach for this platform, download it 249 | if not os.path.exists( 250 | f"{dest_folder}/async-profiler-{release}-{platform}/build/jattach"): 251 | download_latest_jattach(Path( 252 | f"{dest_folder}/async-profiler-{release}-{platform}/build/jattach"), 253 | platform) 254 | 255 | 256 | def download_release_code(release: str): 257 | """ Download the release code from GitHub """ 258 | dest_folder = f"{CURRENT_DIR}/ap-releases/async-profiler-{release}-code" 259 | archive_file = dest_folder + ".zip" 260 | if not os.path.exists(archive_file): 261 | request.urlretrieve( 262 | f"https://github.com/jvm-profiling-tools/async-profiler/archive/refs/tags/v{release}.zip", 263 | archive_file) 264 | execute(["unzip", "-o", archive_file, "-d", f"{CURRENT_DIR}/ap-releases"]) 265 | shutil.rmtree(dest_folder, ignore_errors=True) 266 | shutil.move(f"{CURRENT_DIR}/ap-releases/async-profiler-{release}", 267 | dest_folder) 268 | os.makedirs(f"{dest_folder}/build", exist_ok=True) 269 | 270 | 271 | def download_release(release: str): 272 | for release_url in get_release_asset_urls(Tool.ASYNC_PROFILER, release): 273 | download_release_url(release, release_url) 274 | download_release_code(release) 275 | 276 | 277 | def release_target_file(release: str, platform: str): 278 | return f"{CURRENT_DIR}/releases/ap-loader-{platform}-{release}-{SUB_VERSION}.jar" 279 | 280 | 281 | def build_release(release: str): 282 | print("Download release files") 283 | release_folder = CURRENT_DIR + "/releases" 284 | os.makedirs(release_folder, exist_ok=True) 285 | download_release(release) 286 | for platform in get_release_platforms(Tool.ASYNC_PROFILER, release): 287 | release_file = f"ap-loader-{platform}-{release}-{SUB_VERSION}-full.jar" 288 | dest_release_file = f"{release_folder}/ap-loader-{platform}-{release}-{SUB_VERSION}.jar" 289 | print(f"Build release for {platform}") 290 | execute( 291 | f"mvn -Duser.name='' -Dproject.vversion={release} -Dproject.subversion={SUB_VERSION} -Dproject.platform={platform} package assembly:single") 292 | shutil.copy(f"{CURRENT_DIR}/target/{release_file}", dest_release_file) 293 | all_target = release_target_file(release, "all") 294 | print("Build release for all") 295 | execute( 296 | f"mvn -Duser.name='' -Dproject.vversion={release} -Dproject.subversion={SUB_VERSION} -Dproject.platform=all package assembly:single -f pom_all.xml") 297 | shutil.copy( 298 | f"{CURRENT_DIR}/target/ap-loader-all-{release}-{SUB_VERSION}-full.jar", 299 | all_target) 300 | 301 | 302 | def build_tests(release: str): 303 | print("Build tests") 304 | shutil.rmtree(TESTS_CODE_DIR, ignore_errors=True) 305 | subprocess.check_call( 306 | f"java -jar {release_target_file(release, 'all')} clear", shell=True) 307 | os.makedirs(TESTS_DIR, exist_ok=True) 308 | code_folder = f"{CURRENT_DIR}/ap-releases/async-profiler-{release}-code" 309 | release_file = release_target_file(release, "all") 310 | shutil.copytree(code_folder, TESTS_CODE_DIR) 311 | test_folder = f"{TESTS_CODE_DIR}/test" 312 | # walk all files in the folder recursively and replace the paths 313 | # to the release file 314 | for root, dirs, files in os.walk(TESTS_CODE_DIR): 315 | for file in files: 316 | if file.endswith(".java") or file.endswith("Makefile"): 317 | with open(f"{root}/{file}") as f: 318 | content = f.read() 319 | content = (content 320 | .replace("-source 7 -target 7", "-source 8 -target 8") 321 | .replace('cmd.add("-agentpath:" + profilerLibPath() + "=" +', 322 | f'cmd.add("-javaagent:{release_file}" + "=" +') 323 | .replace( 324 | 'cmd.add("build/bin/asprof")', 325 | f'cmd.addAll(List.of("java", "-jar", "{release_file}", "profiler"))')) 326 | if file == "AllocTests.java": 327 | # the startup test doesn't work 328 | # so let's find the @Test annotation in the line before 329 | # 'public void startup(TestProcess p)' and remove it 330 | lines = content.splitlines() 331 | # find startup line 332 | startup_line = next(i for i, line in enumerate(lines) if 333 | "public void startup(TestProcess p)" in line) 334 | # find the line before 335 | before_startup_line = startup_line - 1 336 | # find the @Test annotation 337 | lines = lines[:before_startup_line] + lines[startup_line:] 338 | content = "\n".join(lines) 339 | with open(f"{root}/{file}", "w") as f: 340 | f.write(content) 341 | 342 | 343 | def clear_tests_dir(): 344 | for f in os.listdir(TESTS_DIR): 345 | shutil.rmtree(f"{TESTS_DIR}/{f}", ignore_errors=True) 346 | 347 | 348 | def test_release_basic_execution(release: str, platform: str, 349 | ignore_output=True) -> bool: 350 | """ 351 | Tests that the agentpath command returns a usable agent on this platform 352 | """ 353 | release_file = release_target_file(release, platform) 354 | cmd = "" 355 | try: 356 | pipe = subprocess.PIPE if not ignore_output else subprocess.DEVNULL 357 | clear_tests_dir() 358 | agentpath_cmd = f"java -jar '{release_file}' agentpath" 359 | if not ignore_output: 360 | print(f"Execute {agentpath_cmd}") 361 | agentpath = subprocess.check_output(agentpath_cmd, shell=True, 362 | stderr=subprocess.DEVNULL).decode().strip() 363 | if (not agentpath.endswith(".so") and not agentpath.endswith( 364 | ".dylib")) or not os.path.exists(agentpath): 365 | print(f"Invalid agentpath: {agentpath}") 366 | return False 367 | profile_file = f"{TESTS_DIR}/profile.jfr" 368 | cmd = f"java -javaagent:{release_file}=start,file={profile_file},jfr " \ 369 | f"{CURRENT_DIR}/misc/TestMain.java" 370 | if not ignore_output: 371 | print(f"Execute cd {CURRENT_DIR}; {cmd}") 372 | subprocess.check_call(cmd, shell=True, cwd=CURRENT_DIR, stdout=pipe, 373 | stderr=pipe) 374 | if not os.path.exists(profile_file): 375 | return False 376 | flamegraph_file = f"{TESTS_DIR}/flamegraph.html" 377 | cmd = f"java -jar '{release_file}' converter -o html {profile_file} {flamegraph_file}" 378 | if not ignore_output: 379 | print(f"Execute cd {CURRENT_DIR}; {cmd}") 380 | subprocess.check_call(cmd, shell=True, cwd=CURRENT_DIR, stdout=pipe, 381 | stderr=pipe) 382 | if not os.path.exists(flamegraph_file): 383 | print("no flamegraph file") 384 | return False 385 | return True 386 | except subprocess.CalledProcessError: 387 | if not ignore_output: 388 | print(f"Error executing command: {cmd}") 389 | return False 390 | 391 | 392 | def test_releases_basic_execution(release: str): 393 | """ Throws an error if none of the platform JARs works (and the all JAR should also work) """ 394 | print("Test basic execution of javaagent") 395 | results = [platform for platform in 396 | get_release_platforms(Tool.ASYNC_PROFILER, release) if 397 | test_release_basic_execution(release, platform)] 398 | if not results: 399 | for platform in get_release_platforms(Tool.ASYNC_PROFILER, release): 400 | print(f"Test release {release} for {platform} failed:") 401 | test_release_basic_execution(release, platform, ignore_output=False) 402 | raise Exception(f"None of the platform JARs for {release} works") 403 | if len(results) > 1: 404 | raise Exception( 405 | f"Multiple platform JARs work for {release}: {results}, this should not be the case") 406 | if not test_release_basic_execution(release, "all"): 407 | print(f"Test release {release} for all failed:") 408 | if not test_release_basic_execution(release, "all", 409 | ignore_output=False): 410 | raise Exception(f"The all JAR for {release} does not work") 411 | 412 | 413 | def run_async_profiler_test(test_script: str) -> bool: 414 | print(f"Execute {test_script}") 415 | cmd = f"bash '{TESTS_CODE_DIR}/test/{test_script}'" 416 | try: 417 | subprocess.check_call(cmd, shell=True, stdout=subprocess.PIPE, 418 | stderr=subprocess.PIPE) 419 | return True 420 | except BaseException as ex: 421 | print(f"Test {test_script} failed, {cmd}: {ex}") 422 | return False 423 | 424 | 425 | def run_async_profiler_tests(): 426 | print("Run async-profiler tests") 427 | try: 428 | subprocess.check_call("make test", cwd=TESTS_CODE_DIR, shell=True,) 429 | except subprocess.CalledProcessError as ex: 430 | raise Exception(f"Some async-profiler tests failed {ex}") 431 | 432 | 433 | def test_release(release: str): 434 | """ Be sure to build it before """ 435 | print("Basic release tests") 436 | build_tests(release) 437 | test_releases_basic_execution(release) 438 | run_async_profiler_tests() 439 | 440 | 441 | def deploy_maven_platform(release: str, platform: str, snapshot: bool): 442 | print(f"Deploy {release}-{SUB_VERSION} for {platform} to maven") 443 | with PreparedPOMs(release, platform, snapshot) as poms: 444 | pom = poms.pom_all if platform == "all" else poms.pom 445 | cmd = f"mvn -Duser.name='' -Dproject.vversion={release} -Dproject.subversion={SUB_VERSION} -Dproject.platform={platform} " \ 446 | f"-Dproject.suffix='{'-SNAPSHOT' if snapshot else ''}' -f {pom} clean deploy" 447 | try: 448 | subprocess.check_call(cmd, shell=True, cwd=CURRENT_DIR, 449 | stdout=subprocess.PIPE, 450 | stderr=subprocess.PIPE) # os.system(f"cd {CURRENT_DIR}; {cmd}") 451 | except BaseException as ex: 452 | raise Exception(f"Error deploying {release}-{SUB_VERSION} for {platform} to maven: {ex}, cwd: {CURRENT_DIR}, cmd: {cmd}") 453 | 454 | 455 | def deploy_maven(release: str, snapshot: bool = True): 456 | print(f"Deploy {release}-{SUB_VERSION}{' snapshot' if snapshot else ''}") 457 | for platform in get_release_platforms(Tool.ASYNC_PROFILER, release): 458 | deploy_maven_platform(release, platform, snapshot) 459 | deploy_maven_platform(release, "all", snapshot) 460 | 461 | 462 | def get_changelog(release: str) -> str: 463 | url = get_release(Tool.ASYNC_PROFILER, release)["html_url"] 464 | return f"## ap-loader v{SUB_VERSION}\n\n{RELEASE_NOTES}\n\n" \ 465 | f"_The following is copied from the wrapped [async-profiler release]({url}) " \ 466 | f"by [Andrei Pangin](https://github.com/apangin). " \ 467 | f"The source code linked below should be ignored._\n\n{get_release_info(Tool.ASYNC_PROFILER, release)}" 468 | 469 | 470 | def deploy_github(release: str): 471 | changelog = get_changelog(release) 472 | is_latest = release == get_most_recent_release(Tool.ASYNC_PROFILER) 473 | title = f"Loader for {release} (v{SUB_VERSION}): {get_release(Tool.ASYNC_PROFILER, release)['name']}" 474 | prerelease = get_release(Tool.ASYNC_PROFILER, release)["prerelease"] 475 | print(f"Deploy {release}-{SUB_VERSION} ({title}) to GitHub") 476 | if not os.path.exists( 477 | f"{CURRENT_DIR}/releases/ap-loader-all-{release}-{SUB_VERSION}.jar"): 478 | build_release(release) 479 | with tempfile.TemporaryDirectory() as d: 480 | changelog_file = f"{d}/CHANGELOG.md" 481 | with open(changelog_file, "w") as of: 482 | of.write(changelog) 483 | of.close() 484 | releases_dir = f"{CURRENT_DIR}/releases" 485 | platform_paths = [] 486 | for platform in get_release_platforms(Tool.ASYNC_PROFILER, release) + [ 487 | "all"]: 488 | path = f"{d}/ap-loader-{platform}.jar" 489 | shutil.copy( 490 | f"{releases_dir}/ap-loader-{platform}-{release}-{SUB_VERSION}.jar", 491 | path) 492 | platform_paths.append(path) 493 | 494 | flags_str = f"-F {changelog_file} -t '{title}' {'--latest' if is_latest else ''}" \ 495 | f" {'--prerelease' if prerelease else ''}" 496 | paths_str = " ".join(f'"{p}"' for p in platform_paths) 497 | cmd = f"gh release create {release}-{SUB_VERSION} {flags_str} {paths_str}" 498 | try: 499 | subprocess.check_call(cmd, shell=True, cwd=CURRENT_DIR, 500 | stdout=subprocess.DEVNULL, 501 | stderr=subprocess.DEVNULL) 502 | except subprocess.CalledProcessError: 503 | # this is either a real problem or it means that the release already exists 504 | # in the latter case, we can just update it 505 | cmd = f"gh release edit {release}-{SUB_VERSION} {flags_str}; gh release upload {release}-{SUB_VERSION} {paths_str} --clobber" 506 | try: 507 | subprocess.check_call(cmd, shell=True, cwd=CURRENT_DIR, 508 | stdout=subprocess.DEVNULL, 509 | stderr=subprocess.DEVNULL) 510 | except subprocess.CalledProcessError: 511 | os.system(f"cd {CURRENT_DIR}; {cmd}") 512 | 513 | 514 | def deploy(release: str, snapshot: bool = True): 515 | deploy_maven(release, snapshot) 516 | deploy_github(release) 517 | 518 | 519 | def clear(): 520 | shutil.rmtree(f"{CURRENT_DIR}/ap-releases", ignore_errors=True) 521 | shutil.rmtree(f"{CURRENT_DIR}/releases", ignore_errors=True) 522 | shutil.rmtree(f"{CURRENT_DIR}/target", ignore_errors=True) 523 | shutil.rmtree(TESTS_DIR, ignore_errors=True) 524 | shutil.rmtree(TESTS_CODE_DIR, ignore_errors=True) 525 | 526 | 527 | def parse_cli_args() -> Tuple[List[str], Optional[str]]: 528 | available_commands = ["current_version", "versions", "download", "build", 529 | "test", "deploy_mvn", "deploy_mvn_release", 530 | "deploy_gh", "deploy", 531 | "deploy_release", "clear"] 532 | commands = [] 533 | release = sys.argv[-1] if sys.argv[-1][0].isnumeric() else None 534 | for arg in sys.argv[1:(-1 if release else None)]: 535 | if arg not in available_commands: 536 | print(f"Unknown command: {arg}") 537 | print(HELP) 538 | sys.exit(1) 539 | commands.append(arg) 540 | if not commands: 541 | print(HELP) 542 | if not release: 543 | release = get_most_recent_release(Tool.ASYNC_PROFILER) 544 | return commands, release 545 | 546 | 547 | def cli(): 548 | commands, release = parse_cli_args() 549 | coms = {"current_version": lambda: print( 550 | get_most_recent_release(Tool.ASYNC_PROFILER)), 551 | "versions": lambda: print(" ".join( 552 | version for version in get_release_versions(Tool.ASYNC_PROFILER) if version.startswith("4."))), 553 | "download": lambda: download_release(release), 554 | "build": lambda: build_release(release), 555 | "test": lambda: test_release(release), 556 | "deploy_mvn": lambda: deploy_maven(release), 557 | "deploy_mvn_release": lambda: deploy_maven(release, snapshot=False), 558 | "deploy_gh": lambda: deploy_github(release), 559 | "deploy": lambda: deploy(release, snapshot=True), 560 | "deploy_release": lambda: deploy(release, snapshot=False), 561 | "clear": clear, } 562 | for command in commands: 563 | coms[command]() 564 | 565 | 566 | if __name__ == "__main__": 567 | cli() 568 | -------------------------------------------------------------------------------- /bin/timestamp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import time 3 | print(time.time()) 4 | -------------------------------------------------------------------------------- /jbang-catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "catalogs": {}, 3 | "aliases": { 4 | "ap-loader": { 5 | "script-ref": "https://github.com/jvm-profiling-tools/ap-loader/releases/latest/download/ap-loader-all.jar", 6 | "description": "async profiler loader. Use directly with `jbang ap-loader@jvm-profiling-tools/ap-loader` or use as agent with `jbang --javaagent\u003dap-loader@jvm-profile-tools/ap-loader ...`." 7 | }, 8 | "converter": { 9 | "script-ref": "https://github.com/jvm-profiling-tools/ap-loader/releases/latest/download/ap-loader-all.jar", 10 | "arguments": [ 11 | "converter" 12 | ], 13 | "java-agents": [] 14 | }, 15 | "profiler": { 16 | "script-ref": "ap-loader@jvm-profiling-tools/ap-loader", 17 | "arguments": [ 18 | "profiler" 19 | ], 20 | "java-agents": [] 21 | }, 22 | "jattach": { 23 | "script-ref": "ap-loader@jvm-profiling-tools/ap-loader", 24 | "arguments": [ 25 | "jattach" 26 | ], 27 | "java-agents": [] 28 | } 29 | }, 30 | "templates": {} 31 | } -------------------------------------------------------------------------------- /misc/TestMain.java: -------------------------------------------------------------------------------- 1 | // a main class that just waste some cpu for two seconds 2 | 3 | public class TestMain { 4 | public static void main(String[] args) throws Exception { 5 | long start = System.currentTimeMillis(); 6 | int i = 0; 7 | while (System.currentTimeMillis() - start < 2000) { 8 | i = Math.pow(i + 1, 2) > 1000000 ? 0 : i + 1; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | ap-loader 7 | me.bechberger 8 | ap-loader-${project.platform} 9 | ${project.vversion}-${project.subversion}-${project.suffix} 10 | https://github.com/jvm-profiling-tools/ap-loader 11 | 12 | 13 | Apache License, Version 2.0 14 | http://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | parttimenerd 20 | Johannes Bechberger 21 | johannes.bechberger@sap.com 22 | 23 | 24 | 25 | scm:git:https://github.com/jvm-profiling-tools/ap-loader.git 26 | scm:git:https://github.com/jvm-profiling-tools/ap-loader.git 27 | https://github.com/jvm-profiling-tools/ap-loader.git 28 | 29 | 2022 30 | Packages async-profiler, including the converter and jattach, in a single JAR. 31 | 32 | 33 | 8 34 | 8 35 | UTF-8 36 | 2.8.3 37 | 1 38 | macos 39 | -SNAPSHOT 40 | 41 | 42 | 43 | 44 | 45 | maven-clean-plugin 46 | 2.5 47 | 48 | 49 | auto-clean 50 | initialize 51 | 52 | clean 53 | 54 | 55 | 56 | 57 | ${basedir}/target/classes/libs 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.codehaus.mojo 66 | exec-maven-plugin 67 | 3.1.0 68 | 69 | 70 | generate-sources 71 | 72 | exec 73 | 74 | 75 | 76 | 77 | sh 78 | ${basedir} 79 | 80 | ${basedir}/bin/copy_libs.sh 81 | ${basedir} 82 | ${project.vversion}-${project.platform} 83 | 84 | 85 | en_US 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-jar-plugin 92 | 2.4 93 | 94 | 95 | 96 | true 97 | true 98 | true 99 | one.profiler.AsyncProfilerLoader 100 | 101 | 102 | one.profiler.AsyncProfilerLoader 103 | one.profiler.AsyncProfilerLoader 104 | 105 | 106 | 107 | 108 | 109 | maven-assembly-plugin 110 | 111 | 112 | 113 | true 114 | false 115 | false 116 | one.profiler.AsyncProfilerLoader 117 | 118 | 119 | Johannes Bechberger 120 | one.profiler.AsyncProfilerLoader 121 | one.profiler.AsyncProfilerLoader 122 | ap-loader-${project.platform} 123 | ${project.version} 124 | ap-loader-${project.platform} 125 | ${project.version} 126 | me.bechberger 127 | 128 | 129 | 130 | jar-with-dependencies 131 | 132 | ${project.artifactId}-${project.vversion}-${project.subversion}-full 133 | false 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-source-plugin 139 | 2.2.1 140 | 141 | 142 | attach-sources 143 | 144 | jar-no-fork 145 | 146 | 147 | 148 | 149 | 150 | org.codehaus.mojo 151 | exec-maven-plugin 152 | 3.1.0 153 | 154 | 155 | copy-file 156 | generate-sources 157 | 158 | 159 | 160 | 161 | org.apache.maven.plugins 162 | maven-javadoc-plugin 163 | 3.4.1 164 | 165 | 166 | attach-javadoc 167 | package 168 | 169 | jar 170 | 171 | 172 | 173 | 174 | -Xdoclint:none 175 | 176 | 177 | 178 | maven-deploy-plugin 179 | 2.8.2 180 | 181 | 182 | default-deploy 183 | deploy 184 | 185 | deploy 186 | 187 | 188 | 189 | 190 | 191 | org.apache.maven.plugins 192 | maven-gpg-plugin 193 | 1.6 194 | 195 | 196 | sign-artifacts 197 | verify 198 | 199 | sign 200 | 201 | 202 | 203 | 204 | 205 | --pinentry-mode 206 | loopback 207 | 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-deploy-plugin 213 | 2.8.2 214 | 215 | 216 | default-deploy 217 | deploy 218 | 219 | deploy 220 | 221 | 222 | true 223 | 224 | 225 | 226 | release 227 | deploy 228 | 229 | deploy 230 | 231 | 232 | 233 | 234 | 235 | org.sonatype.central 236 | central-publishing-maven-plugin 237 | 0.7.0 238 | true 239 | 240 | ossrh 241 | true 242 | uploaded 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /pom_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | ap-loader 7 | me.bechberger 8 | ap-loader-${project.platform} 9 | ${project.vversion}-${project.subversion} 10 | https://github.com/jvm-profiling-tools/ap-loader 11 | 12 | 13 | Apache License, Version 2.0 14 | http://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | parttimenerd 20 | Johannes Bechberger 21 | johannes.bechberger@sap.com 22 | 23 | 24 | 25 | scm:git:https://github.com/jvm-profiling-tools/ap-loader.git 26 | scm:git:https://github.com/jvm-profiling-tools/ap-loader.git 27 | https://github.com/jvm-profiling-tools/ap-loader.git 28 | 29 | 2022 30 | Packages async-profiler, including the converter and jattach, in a single JAR. 31 | 32 | 33 | 8 34 | 8 35 | UTF-8 36 | 2.8.3 37 | 1 38 | all 39 | -SNAPSHOT 40 | 41 | 42 | 43 | 44 | 45 | maven-clean-plugin 46 | 2.5 47 | 48 | 49 | auto-clean 50 | initialize 51 | 52 | clean 53 | 54 | 55 | 56 | 57 | ${basedir}/target/classes/libs 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.codehaus.mojo 66 | exec-maven-plugin 67 | 3.1.0 68 | 69 | 70 | generate-sources 71 | 72 | exec 73 | 74 | 75 | 76 | 77 | sh 78 | ${basedir} 79 | 80 | ${basedir}/bin/copy_all_libs.sh 81 | ${basedir} 82 | ${project.vversion}-${project.platform} 83 | 84 | 85 | en_US 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-jar-plugin 92 | 2.4 93 | 94 | 95 | 96 | true 97 | true 98 | true 99 | one.profiler.AsyncProfilerLoader 100 | 101 | 102 | one.profiler.AsyncProfilerLoader 103 | one.profiler.AsyncProfilerLoader 104 | 105 | 106 | 107 | 108 | 109 | maven-assembly-plugin 110 | 111 | 112 | 113 | true 114 | false 115 | false 116 | one.profiler.AsyncProfilerLoader 117 | 118 | 119 | Johannes Bechberger 120 | one.profiler.AsyncProfilerLoader 121 | one.profiler.AsyncProfilerLoader 122 | ap-loader-${project.platform} 123 | ${project.version} 124 | ap-loader-${project.platform} 125 | ${project.version} 126 | me.bechberger 127 | 128 | 129 | 130 | jar-with-dependencies 131 | 132 | ${project.artifactId}-${project.vversion}-${project.subversion}-full 133 | false 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-source-plugin 139 | 2.2.1 140 | 141 | 142 | attach-sources 143 | 144 | jar-no-fork 145 | 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-javadoc-plugin 152 | 3.4.1 153 | 154 | 155 | attach-javadoc 156 | package 157 | 158 | jar 159 | 160 | 161 | 162 | 163 | -Xdoclint:none 164 | 165 | 166 | 167 | maven-deploy-plugin 168 | 2.8.2 169 | 170 | 171 | default-deploy 172 | deploy 173 | 174 | deploy 175 | 176 | 177 | 178 | 179 | 180 | org.apache.maven.plugins 181 | maven-gpg-plugin 182 | 1.6 183 | 184 | 185 | sign-artifacts 186 | verify 187 | 188 | sign 189 | 190 | 191 | 192 | 193 | 194 | --pinentry-mode 195 | loopback 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-deploy-plugin 202 | 2.8.2 203 | 204 | 205 | default-deploy 206 | deploy 207 | 208 | deploy 209 | 210 | 211 | true 212 | 213 | 214 | 215 | release 216 | deploy 217 | 218 | deploy 219 | 220 | 221 | 222 | 223 | 224 | org.sonatype.central 225 | central-publishing-maven-plugin 226 | 0.7.0 227 | true 228 | 229 | ossrh 230 | true 231 | uploaded 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/java/one/profiler/AsyncProfilerLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 SAP SE or an SAP affiliate company, Johannes Bechberger 3 | * and ap-loader contributors 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package one.profiler; 19 | 20 | import java.io.*; 21 | import java.lang.instrument.Instrumentation; 22 | import java.lang.management.ManagementFactory; 23 | import java.net.URL; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.util.*; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | 31 | /** 32 | * Allows to work with the async-profiler libraries and tools stored in the resources folder. 33 | * 34 | *

Upon extraction the libraries and tools are stored in an application, version and user 35 | * specific application folder. The resulting files are therefore cached between executions of the 36 | * JVM. This folder can be manually specified by either setting the property 37 | * ap_loader_extraction_dir or by using the {@link 38 | * AsyncProfilerLoader#setExtractionDirectory(Path)}} method. 39 | * 40 | *

Running this class as an agent main class, makes it an agent that behaves the same as the 41 | * libasyncProfiler.so agent. Running the main method exposed the asprof, jattach and jfrconv 42 | * features. 43 | */ 44 | public final class AsyncProfilerLoader { 45 | 46 | private static class OSNotSupportedException extends Exception { 47 | OSNotSupportedException(String message) { 48 | super(message); 49 | } 50 | } 51 | 52 | private static final String EXTRACTION_PROPERTY_NAME = "ap_loader_extraction_dir"; 53 | private static final String PROFILE_LIB_PROPERTY_FILE_PREFIX = "libs/ap-profile-lib-"; 54 | private static final String PROFILE_SCRIPT_PROPERTY_FILE_PREFIX = "libs/ap-profile-script-"; 55 | private static String versionAndPlatformSuffix; 56 | private static Path extractedAsyncProfiler; 57 | private static Path extractedJattach; 58 | private static Path extractedJfrconv; 59 | private static Path extractedProfiler; 60 | private static Path extractionDir; 61 | private static List availableVersions; 62 | private static String version; 63 | private static final Map propertyFileFirstLineCache = new HashMap<>(); 64 | 65 | static { 66 | String dir = System.getProperty(EXTRACTION_PROPERTY_NAME, ""); 67 | if (!dir.isEmpty()) { 68 | Path path = Paths.get(dir); 69 | if (Files.exists(path)) { 70 | if (!Files.isDirectory(path)) { 71 | throw new IllegalArgumentException("The extraction directory is not a directory: " + dir); 72 | } 73 | if (!Files.isWritable(path)) { 74 | throw new IllegalArgumentException("The extraction directory is not writable: " + dir); 75 | } 76 | } 77 | extractionDir = path; 78 | } 79 | } 80 | 81 | private static String getCurrentJARFileName() { 82 | // source https://stackoverflow.com/a/320595/19040822 83 | String[] pathParts = 84 | AsyncProfilerLoader.class 85 | .getProtectionDomain() 86 | .getCodeSource() 87 | .getLocation() 88 | .getFile() 89 | .split("/"); 90 | String last = pathParts[pathParts.length - 1]; 91 | if (last.endsWith(".jar")) { 92 | return last; 93 | } 94 | return null; 95 | } 96 | 97 | /** Returns directory used for storing the extracted libraries, binaries and JARs */ 98 | public static void setExtractionDirectory(Path extractionDir) { 99 | AsyncProfilerLoader.extractionDir = extractionDir; 100 | } 101 | 102 | /** 103 | * Set the used version of async-profiler. This is required if there are multiple versions of this 104 | * library in the dependencies. 105 | * 106 | * @param version set version of async-profiler to use 107 | */ 108 | public static void setVersion(String version) throws IOException { 109 | deleteExtractionDirectory(); 110 | AsyncProfilerLoader.version = version; 111 | } 112 | 113 | /** Deletes the directory used for extraction */ 114 | public static void deleteExtractionDirectory() throws IOException { 115 | if (extractionDir != null) { 116 | try (Stream stream = Files.walk(getExtractionDirectory())) { 117 | stream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); 118 | } 119 | } 120 | extractedAsyncProfiler = null; 121 | extractedJattach = null; 122 | extractedJfrconv = null; 123 | extractedProfiler = null; 124 | extractionDir = null; 125 | } 126 | 127 | /** Returns the directory used for storing the extracted libraries, binaries and JARs */ 128 | public static Path getExtractionDirectory() throws IOException { 129 | if (extractionDir == null) { 130 | extractionDir = 131 | getApplicationsDir().resolve(Paths.get("me.bechberger.ap-loader", getVersion())); 132 | if (Files.notExists(extractionDir)) { 133 | Files.createDirectories(extractionDir); 134 | } 135 | } 136 | return extractionDir; 137 | } 138 | 139 | /** Returns directory where applications places their files. Specific to operating system */ 140 | private static Path getApplicationsDir() { 141 | String os = System.getProperty("os.name").toLowerCase(); 142 | if (os.startsWith("linux")) { 143 | String xdgDataHome = System.getenv("XDG_DATA_HOME"); 144 | if (xdgDataHome != null && !xdgDataHome.isEmpty()) { 145 | return Paths.get(xdgDataHome); 146 | } 147 | return Paths.get(System.getProperty("user.home"), ".local", "share"); 148 | } else if (os.startsWith("macosx") || os.startsWith("mac os x")) { 149 | return Paths.get(System.getProperty("user.home"), "Library", "Application Support"); 150 | } 151 | throw new UnsupportedOperationException("Unsupported os " + os); 152 | } 153 | 154 | /** 155 | * Returns the version and platform suffix like {@code 3.0-linux-x64} or {@code 2.9-macos} 156 | * 157 | * @throws IllegalStateException if OS or Arch not supported 158 | */ 159 | private static String getVersionAndPlatformSuffix() throws OSNotSupportedException { 160 | if (versionAndPlatformSuffix == null) { 161 | String version = getVersion(); 162 | boolean versionOne = version.startsWith("1."); 163 | boolean versionThree = version.startsWith("3."); 164 | String os = System.getProperty("os.name").toLowerCase(); 165 | String arch = System.getProperty("os.arch").toLowerCase(); 166 | if (os.startsWith("linux")) { 167 | if (arch.equals("arm64") || arch.equals("aarch64")) { 168 | versionAndPlatformSuffix = version + "-linux-arm64"; 169 | } else if (arch.equals("x86") && versionOne) { 170 | versionAndPlatformSuffix = version + "-linux-x86"; 171 | } else if (arch.equals("x86_64") || arch.equals("x64") || arch.equals("amd64")) { 172 | if (versionThree) { 173 | versionAndPlatformSuffix = version + "-linux-x64"; 174 | } else if (isOnMusl()) { 175 | versionAndPlatformSuffix = version + "-linux-x64-musl"; 176 | } else { 177 | versionAndPlatformSuffix = version + "-linux-x64"; 178 | } 179 | } else { 180 | throw new OSNotSupportedException("Async-profiler does not work on Linux " + arch); 181 | } 182 | } else if (os.startsWith("macosx") || os.startsWith("mac os x")) { 183 | if (versionOne) { 184 | if (!arch.contains("x86")) { 185 | throw new OSNotSupportedException("Async-profiler does not work on MacOS " + arch); 186 | } 187 | versionAndPlatformSuffix = version + "-macosx-x86"; 188 | } else { 189 | versionAndPlatformSuffix = version + "-macos"; 190 | } 191 | } else { 192 | throw new OSNotSupportedException("Async-profiler does not work on " + os); 193 | } 194 | } 195 | return versionAndPlatformSuffix; 196 | } 197 | 198 | /** Get available versions of the library */ 199 | public static List getAvailableVersions() { 200 | if (availableVersions == null) { 201 | availableVersions = new ArrayList<>(); 202 | 203 | try { 204 | Enumeration indexFiles = 205 | AsyncProfilerLoader.class.getClassLoader().getResources("libs/ap-version"); 206 | while (indexFiles.hasMoreElements()) { 207 | URL indexFile = indexFiles.nextElement(); 208 | try (BufferedReader reader = 209 | new BufferedReader(new InputStreamReader(indexFile.openStream()))) { 210 | String line = reader.readLine(); 211 | if (line != null) { 212 | availableVersions.add(line); 213 | } 214 | } 215 | } 216 | } catch (IOException e) { 217 | e.printStackTrace(); 218 | return Collections.emptyList(); 219 | } 220 | } 221 | return availableVersions; 222 | } 223 | 224 | /** 225 | * Reads the first line of the file in the resources folder with the given name. 226 | * 227 | *

Caches this information 228 | */ 229 | private static String readFirstPropertyFileLine(String file) { 230 | if (propertyFileFirstLineCache.containsKey(file)) { 231 | return propertyFileFirstLineCache.get(file); 232 | } 233 | try { 234 | Enumeration indexFiles = AsyncProfilerLoader.class.getClassLoader().getResources(file); 235 | if (indexFiles.hasMoreElements()) { 236 | URL indexFile = indexFiles.nextElement(); 237 | try (BufferedReader reader = 238 | new BufferedReader(new InputStreamReader(indexFile.openStream()))) { 239 | String firstLine = reader.readLine().trim(); 240 | propertyFileFirstLineCache.put(file, firstLine); 241 | return firstLine; 242 | } 243 | } 244 | } catch (IOException e) { 245 | e.printStackTrace(); 246 | } 247 | throw new AssertionError("Reading property file " + file + " failed, this should never fail"); 248 | } 249 | 250 | /** 251 | * Returns the version of the included async-profiler library (or the version set by {@link 252 | * #setVersion(String)} 253 | * 254 | * @throws IllegalStateException if the version could not be determined 255 | */ 256 | public static String getVersion() { 257 | if (version == null) { 258 | if (getAvailableVersions().size() > 1) { 259 | throw new IllegalStateException( 260 | "Multiple versions of async-profiler found: " + getAvailableVersions()); 261 | } 262 | version = getAvailableVersions().get(0); 263 | } 264 | return version; 265 | } 266 | 267 | private static boolean isOnMusl() { 268 | // based on https://docs.pleroma.social/backend/installation/otp_en/#detecting-flavour 269 | try { 270 | Process process = Runtime.getRuntime().exec(new String[] {"ldd", "--version"}); 271 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 272 | return reader.lines().anyMatch(line -> line.contains("musl")); 273 | } catch (IOException e) { 274 | return false; 275 | } 276 | } 277 | 278 | private static String getAsyncProfilerFileName() throws OSNotSupportedException { 279 | return readFirstPropertyFileLine( 280 | PROFILE_LIB_PROPERTY_FILE_PREFIX + getVersionAndPlatformSuffix()); 281 | } 282 | 283 | private static String getLibrarySuffix(String fileName) { 284 | for (String suffix : Arrays.asList(".so", ".dylib", ".dll")) { 285 | if (fileName.endsWith(suffix)) { 286 | return suffix; 287 | } 288 | } 289 | throw new IllegalStateException("Could not determine library suffix for " + fileName); 290 | } 291 | 292 | private static String getExtractedAsyncProfilerFile() throws OSNotSupportedException { 293 | return "lib/libasyncProfiler" + getLibrarySuffix(getAsyncProfilerFileName()); 294 | } 295 | 296 | private static String getJattachFileName() throws OSNotSupportedException { 297 | return "jattach-" + getVersionAndPlatformSuffix(); 298 | } 299 | 300 | private static String getJfrconvFileName() throws OSNotSupportedException { 301 | return "jfrconv-" + getVersionAndPlatformSuffix(); 302 | } 303 | 304 | private static String getProfilerFileName() throws OSNotSupportedException { 305 | return readFirstPropertyFileLine( 306 | PROFILE_SCRIPT_PROPERTY_FILE_PREFIX + getVersionAndPlatformSuffix()); 307 | } 308 | 309 | private static boolean hasFileInResources(String fileName) { 310 | try { 311 | Enumeration indexFiles = 312 | AsyncProfilerLoader.class.getClassLoader().getResources(fileName); 313 | return indexFiles.hasMoreElements(); 314 | } catch (IOException e) { 315 | e.printStackTrace(); 316 | return false; 317 | } 318 | } 319 | 320 | private static URL getUrl(String fileName) { 321 | try { 322 | Enumeration indexFiles = 323 | AsyncProfilerLoader.class.getClassLoader().getResources("libs/" + fileName); 324 | if (indexFiles.hasMoreElements()) { 325 | return indexFiles.nextElement(); 326 | } 327 | throw new IllegalStateException("Could not find file " + fileName); 328 | } catch (IOException e) { 329 | throw new IllegalStateException("Could not find file " + fileName, e); 330 | } 331 | } 332 | 333 | /** 334 | * Checks if an async-profiler library for the current OS, architecture and glibc is available in 335 | * this JAR. 336 | * 337 | * @return true if a library is available, false otherwise 338 | */ 339 | public static boolean isSupported() { 340 | try { 341 | return hasFileInResources(PROFILE_LIB_PROPERTY_FILE_PREFIX + getVersionAndPlatformSuffix()); 342 | } catch (OSNotSupportedException e) { 343 | return false; 344 | } 345 | } 346 | 347 | /** Copy from resources if needed */ 348 | private static Path copyFromResources(String fileName, Path destination) throws IOException { 349 | if (!isSupported()) { 350 | throw new IllegalStateException( 351 | "Async-profiler is not supported on this OS and architecture"); 352 | } 353 | if (Files.exists(destination)) { 354 | // already copied sometime before, but certainly the same file as it belongs to the same 355 | // async-profiler version 356 | return destination; 357 | } 358 | try { 359 | URL url = getUrl(fileName); 360 | try (InputStream in = url.openStream()) { 361 | Files.copy(in, destination); 362 | } 363 | return destination; 364 | } catch (IOException e) { 365 | throw new IOException("Could not copy file " + fileName + " to " + destination, e); 366 | } 367 | } 368 | 369 | /** 370 | * Extracts a custom agent from the resources 371 | * 372 | *

373 | * 374 | * @param classLoader the class loader to load the resources from 375 | * @param fileName the name of the file to copy, maps the library name if the fileName does not 376 | * start with "lib", e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" 377 | * on macOS 378 | * @return the path of the library 379 | * @throws IOException if the extraction fails 380 | */ 381 | public static Path extractCustomLibraryFromResources(ClassLoader classLoader, String fileName) 382 | throws IOException { 383 | return extractCustomLibraryFromResources(classLoader, fileName, null); 384 | } 385 | 386 | /** 387 | * Extracts a custom native library from the resources and returns the alternative source if the 388 | * file is not in the resources. 389 | * 390 | *

If the file is extracted, then it is copied to a new temporary folder which is deleted upon 391 | * JVM exit. 392 | * 393 | *

This method is mainly seen as a helper method to obtain custom native agents for {@link 394 | * #jattach(Path)} and {@link #jattach(Path, String)}. It is included in ap-loader to make it 395 | * easier to write applications that need custom native libraries. 396 | * 397 | *

This method works on all architectures. 398 | * 399 | * @param classLoader the class loader to load the resources from 400 | * @param fileName the name of the file to copy, maps the library name if the fileName does not 401 | * start with "lib", e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" 402 | * on macOS 403 | * @param alternativeSource the optional resource directory to use if the resource is not found in 404 | * the resources, this is typically the case when running the application from an IDE, an 405 | * example would be "src/main/resources" or "target/classes" for maven projects 406 | * @return the path of the library 407 | * @throws IOException if the extraction fails and the alternative source is not present for the 408 | * current architecture 409 | */ 410 | public static Path extractCustomLibraryFromResources( 411 | ClassLoader classLoader, String fileName, Path alternativeSource) throws IOException { 412 | Path filePath = Paths.get(fileName); 413 | String name = filePath.getFileName().toString(); 414 | if (!name.startsWith("lib")) { 415 | name = System.mapLibraryName(name); 416 | } 417 | Path realFilePath = 418 | filePath.getParent() == null ? Paths.get(name) : filePath.getParent().resolve(name); 419 | Enumeration indexFiles = classLoader.getResources(realFilePath.toString()); 420 | if (!indexFiles.hasMoreElements()) { 421 | if (alternativeSource == null) { 422 | throw new IOException("Could not find library " + fileName + " in resources"); 423 | } 424 | if (!alternativeSource.toFile().isDirectory()) { 425 | throw new IOException( 426 | "Could not find library " 427 | + fileName 428 | + " in resources and alternative source " 429 | + alternativeSource 430 | + " is not a directory"); 431 | } 432 | if (alternativeSource.resolve(realFilePath).toFile().exists()) { 433 | return alternativeSource.resolve(realFilePath); 434 | } 435 | throw new IOException( 436 | "Could not find library " 437 | + fileName 438 | + " in resources and alternative source " 439 | + alternativeSource 440 | + " does not contain " 441 | + realFilePath); 442 | } 443 | URL url = indexFiles.nextElement(); 444 | Path tempDir = Files.createTempDirectory("ap-loader"); 445 | try { 446 | Runtime.getRuntime() 447 | .addShutdownHook( 448 | new Thread( 449 | () -> { 450 | try (Stream stream = Files.walk(getExtractionDirectory())) { 451 | stream 452 | .sorted(Comparator.reverseOrder()) 453 | .map(Path::toFile) 454 | .forEach(File::delete); 455 | } catch (IOException ex) { 456 | throw new RuntimeException(ex); 457 | } 458 | })); 459 | } catch (RuntimeException e) { 460 | throw (IOException) e.getCause(); 461 | } 462 | Path destination = tempDir.resolve(name); 463 | try { 464 | try (InputStream in = url.openStream()) { 465 | Files.copy(in, destination); 466 | } 467 | return destination; 468 | } catch (IOException e) { 469 | throw new IOException("Could not copy file " + fileName + " to " + destination, e); 470 | } 471 | } 472 | 473 | /** 474 | * Extracts the jattach tool 475 | * 476 | * @return path to the extracted jattach tool 477 | * @throws IllegalStateException if OS or arch are not supported 478 | * @throws IOException if the extraction fails 479 | */ 480 | public static Path getJattachPath() throws IOException { 481 | if (extractedJattach == null) { 482 | Path path = null; 483 | try { 484 | path = 485 | copyFromResources( 486 | getJattachFileName(), getExtractionDirectory().resolve(getJattachFileName())); 487 | } catch (OSNotSupportedException e) { 488 | throw new IllegalStateException(e.getMessage()); 489 | } 490 | if (!path.toFile().setExecutable(true)) { 491 | throw new IOException("Could not make jattach (" + path + ") executable"); 492 | } 493 | extractedJattach = path; 494 | } 495 | return extractedJattach; 496 | } 497 | 498 | /** 499 | * Extracts the jfrconv tool 500 | * 501 | * @return path to the extracted jfrconv tool 502 | * @throws IllegalStateException if OS or arch are not supported 503 | * @throws IOException if the extraction fails 504 | */ 505 | public static Path getJfrconvPath() throws IOException { 506 | if (extractedJfrconv == null) { 507 | Path path = null; 508 | try { 509 | path = 510 | copyFromResources( 511 | getJfrconvFileName(), getExtractionDirectory().resolve(getJfrconvFileName())); 512 | } catch (OSNotSupportedException e) { 513 | throw new IllegalStateException(e.getMessage()); 514 | } 515 | if (!path.toFile().setExecutable(true)) { 516 | throw new IOException("Could not make jfrconv (" + path + ") executable"); 517 | } 518 | extractedJfrconv = path; 519 | } 520 | return extractedJfrconv; 521 | } 522 | 523 | private static String getExtractedProfilerFile() throws OSNotSupportedException { 524 | return "bin/" + getProfilerFileName(); 525 | } 526 | 527 | /** 528 | * Extracts the asprof binary 529 | * 530 | * @return path to the extracted asprof 531 | * @throws IllegalStateException if OS or arch are not supported 532 | * @throws IOException if the extraction fails 533 | */ 534 | private static Path getProfilerPath() throws IOException { 535 | if (extractedProfiler == null) { 536 | Path path; 537 | try { 538 | Path extracted = getExtractionDirectory().resolve(getExtractedProfilerFile()); 539 | Files.createDirectories(extracted.getParent()); 540 | path = copyFromResources(getProfilerFileName(), extracted); 541 | } catch (OSNotSupportedException e) { 542 | throw new IllegalStateException(e.getMessage()); 543 | } 544 | if (!path.toFile().setExecutable(true)) { 545 | throw new IOException("Could not make asprof (" + path + ") executable"); 546 | } 547 | extractedProfiler = path; 548 | } 549 | return extractedProfiler; 550 | } 551 | 552 | /** 553 | * Extracts the async-profiler and returns the path to the extracted file. 554 | * 555 | * @return the path to the extracted library 556 | * @throws IOException if the library could not be loaded 557 | * @throws IllegalStateException if OS or Arch are not supported 558 | */ 559 | public static Path getAsyncProfilerPath() throws IOException { 560 | if (extractedAsyncProfiler == null) { 561 | try { 562 | Path extracted = getExtractionDirectory().resolve(getExtractedAsyncProfilerFile()); 563 | Files.createDirectories(extracted.getParent()); 564 | extractedAsyncProfiler = copyFromResources(getAsyncProfilerFileName(), extracted); 565 | } catch (OSNotSupportedException e) { 566 | throw new IllegalStateException(e.getMessage()); 567 | } 568 | } 569 | return extractedAsyncProfiler; 570 | } 571 | 572 | private static void ensureAsyncProfilerLoaded() throws IOException { 573 | getAsyncProfilerPath(); 574 | } 575 | 576 | /** Output and error output of a successful process execution. */ 577 | public static class ExecutionResult { 578 | private final String stdout; 579 | private final String stderr; 580 | 581 | private ExecutionResult(String stdout, String stderr) { 582 | this.stdout = stdout; 583 | this.stderr = stderr; 584 | } 585 | 586 | @Override 587 | public String toString() { 588 | return "ExecutionResult{" + "stdout='" + stdout + '\'' + ", stderr='" + stderr + '\'' + '}'; 589 | } 590 | 591 | public String getStdout() { 592 | return stdout; 593 | } 594 | 595 | public String getStderr() { 596 | return stderr; 597 | } 598 | } 599 | 600 | private static String[] processJattachArgs(String[] args) throws IOException { 601 | List argList = new ArrayList<>(Arrays.asList(args)); 602 | if (argList.size() >= 4 603 | && argList.get(1).equals("load") 604 | && (argList.get(2).endsWith("libasyncProfiler.so") 605 | || argList.get(2).endsWith("libasyncProfiler.dylib"))) { 606 | argList.set(2, getAsyncProfilerPath().toString()); 607 | argList.set(3, "true"); 608 | } 609 | argList.add(0, getJattachPath().toString()); 610 | return argList.toArray(new String[0]); 611 | } 612 | 613 | /** 614 | * See jattach for more information. 615 | * 616 | *

It runs the same as jattach with the only exception that every string that ends with 617 | * "libasyncProfiler.so" and "libasyncProfiler.dylib" are mapped to the extracted async-profiler 618 | * library for the load command. One can therefore start/stop the async-profiler via 619 | * executeJattach(PID, "load", "libasyncProfiler.so", true, "start"/"stop"). 620 | * 621 | *

Use the {@link #jattach(Path)} or {@link #jattach(Path, String)} to load agents via jattach 622 | * directly, without the need to construct the command line arguments yourself. 623 | * 624 | * @throws IOException if something went wrong (e.g. the jattach binary is not found or the 625 | * execution fails) 626 | * @throws IllegalStateException if OS or Arch are not supported 627 | */ 628 | public static ExecutionResult executeJattach(String... args) throws IOException { 629 | return executeCommand("jattach", processJattachArgs(args)); 630 | } 631 | 632 | private static void executeJattachInteractively(String[] args) throws IOException { 633 | executeCommandInteractively("jattach", processJattachArgs(args)); 634 | } 635 | 636 | /** 637 | * See jattach for more information. 638 | * 639 | *

It loads the passed agent via jattach to the current JVM, mapping "libasyncProfiler.so" and 640 | * "libasyncProfiler.dylib" to the extracted async-profiler library for the load command. 641 | * 642 | * @return true if the agent was successfully attached, false otherwise 643 | * @throws IllegalStateException if OS or Arch are not supported 644 | */ 645 | public static boolean jattach(Path agentPath) { 646 | return jattach(agentPath, null); 647 | } 648 | 649 | /** 650 | * See jattach for more information. 651 | * 652 | *

It loads the passed agent via jattach to the current JVM, mapping "libasyncProfiler.so" and 653 | * "libasyncProfiler.dylib" to the extracted async-profiler library for the load command. 654 | * 655 | * @return true if the agent was successfully attached, false otherwise 656 | * @throws IllegalStateException if OS or Arch are not supported 657 | */ 658 | public static boolean jattach(Path agentPath, String arguments) { 659 | List args = new ArrayList<>(); 660 | args.add(String.valueOf(getProcessId())); 661 | args.add("load"); 662 | args.add(agentPath.toString()); 663 | args.add("true"); 664 | if (arguments != null) { 665 | args.add(arguments); 666 | } 667 | try { 668 | executeJattach(args.toArray(new String[0])); 669 | return true; 670 | } catch (IOException e) { 671 | e.printStackTrace(); 672 | return false; 673 | } 674 | } 675 | 676 | private static String[] processConverterArgs(String[] args) throws IOException { 677 | List newArgs = new ArrayList<>(); 678 | newArgs.add(getJfrconvPath().toString()); 679 | newArgs.addAll(Arrays.asList(args)); 680 | System.out.println(newArgs); 681 | return newArgs.toArray(new String[0]); 682 | } 683 | 684 | /** 685 | * See Converter Usage 687 | * for more information. 688 | * 689 | *

Just pass it the arguments that you would normally pass to the JVM after jfrconv

690 | * 691 | * @throws IOException if something went wrong (e.g. the execution fails) 692 | * @throws IllegalStateException if OS or Arch are not supported 693 | */ 694 | public static ExecutionResult executeConverter(String... args) throws IOException { 695 | return executeCommand("jfrconv", processConverterArgs(args)); 696 | } 697 | 698 | private static void executeConverterInteractively(String[] args) throws IOException { 699 | executeCommandInteractively("jfrconv", processConverterArgs(args)); 700 | } 701 | 702 | private static String[] getEnv() throws IOException { 703 | if (getVersion().startsWith("3.")) { 704 | return new String[0]; 705 | } 706 | return new String[] { 707 | "JATTACH=" + getJattachPath().toString(), "PROFILER=" + getAsyncProfilerPath() 708 | }; 709 | } 710 | 711 | private static boolean isShellFile(Path file) { 712 | try (BufferedReader reader = new BufferedReader(new FileReader(file.toFile()))) { 713 | String firstLine = reader.readLine(); 714 | return firstLine != null && firstLine.startsWith("#!"); 715 | } catch (IOException e) { 716 | return false; 717 | } 718 | } 719 | 720 | private static boolean isPossibleLibraryArgument(String arg) { 721 | return arg.endsWith(".so") || arg.endsWith(".dylib") || arg.endsWith(".dll"); 722 | } 723 | 724 | private static String[] processProfilerArgs(String[] args) throws IOException { 725 | List argList = new ArrayList<>(); 726 | if (isShellFile(getProfilerPath())) { 727 | argList.add("sh"); 728 | } 729 | if (getVersion().startsWith("3.") && Arrays.asList(args).contains("--lib")) { 730 | // --lib changed from `--lib path full path to libasyncProfiler.so in the container` 731 | // to `-l, --lib prepend library names` 732 | // check that this is followed by a non argument, as `--lib` has a new meaning 733 | int index = Arrays.asList(args).indexOf("--lib"); 734 | if (index + 1 < args.length 735 | && !args[index + 1].startsWith("-") 736 | && isPossibleLibraryArgument(args[index + 1])) { 737 | throw new UnsupportedOperationException( 738 | "The `--lib path` option is not supported in async-profiler 3.x. " 739 | + "Feel free to create an issue at https://github.com/jvm-profiling-tools/ap-loader/issues if you need support for this option."); 740 | } 741 | } 742 | argList.add(getProfilerPath().toString()); 743 | argList.addAll(Arrays.asList(args)); 744 | return argList.toArray(new String[0]); 745 | } 746 | 747 | /** 748 | * See Profiler Options 750 | * for more information. 751 | * 752 | *

753 | * 754 | * @throws IOException if something went wrong (e.g. the execution fails) 755 | * @throws IllegalStateException if OS or Arch are not supported 756 | */ 757 | public static ExecutionResult executeProfiler(String... args) throws IOException { 758 | return executeCommand("asprof", processProfilerArgs(args)); 759 | } 760 | 761 | private static String getApplicationCall() { 762 | String fileName = getCurrentJARFileName(); 763 | if (fileName == null) { 764 | return "java -jar ap-loader.jar"; 765 | } 766 | return "java -jar " + fileName; 767 | } 768 | 769 | private static String processProfilerOutput(String string) throws IOException { 770 | return string.replace(getProfilerPath().toString(), getApplicationCall() + " profiler"); 771 | } 772 | 773 | private static void executeProfilerInteractively(String[] args) throws IOException { 774 | ensureAsyncProfilerLoaded(); 775 | String[] command = processProfilerArgs(args); 776 | Process proc = Runtime.getRuntime().exec(command, getEnv()); 777 | try (BufferedReader stdout = new BufferedReader(new InputStreamReader(proc.getInputStream())); 778 | BufferedReader stderr = new BufferedReader(new InputStreamReader(proc.getErrorStream()))) { 779 | String stdoutStr = stdout.lines().collect(Collectors.joining("\n")); 780 | String stderrStr = stderr.lines().collect(Collectors.joining("\n")); 781 | int exitCode = proc.waitFor(); 782 | System.out.println(processProfilerOutput(stdoutStr)); 783 | System.err.println(processProfilerOutput(stderrStr)); 784 | if (exitCode != 0) { 785 | System.exit(exitCode); 786 | } 787 | } catch (InterruptedException e) { 788 | Thread.currentThread().interrupt(); 789 | System.exit(1); 790 | } 791 | } 792 | 793 | private static ExecutionResult executeCommand(String name, String[] args) throws IOException { 794 | Process process = Runtime.getRuntime().exec(args, getEnv()); 795 | try (BufferedReader stdout = 796 | new BufferedReader(new InputStreamReader(process.getInputStream())); 797 | BufferedReader stderr = 798 | new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 799 | String stdoutStr = stdout.lines().collect(Collectors.joining("\n")); 800 | String stderrStr = stderr.lines().collect(Collectors.joining("\n")); 801 | int exitCode = process.waitFor(); 802 | if (exitCode != 0) { 803 | throw new IOException( 804 | name 805 | + " failed with exit code " 806 | + exitCode 807 | + ", stderr: " 808 | + stderrStr 809 | + ", " 810 | + "stdout: " 811 | + stdoutStr); 812 | } 813 | return new ExecutionResult(stdoutStr, stderrStr); 814 | } catch (InterruptedException e) { 815 | Thread.currentThread().interrupt(); 816 | throw new IOException(name + " failed because it got interrupted"); 817 | } 818 | } 819 | 820 | private static void executeCommandInteractively(String name, String[] args) throws IOException { 821 | ProcessBuilder pb = new ProcessBuilder(args); 822 | pb.inheritIO(); 823 | Map env = pb.environment(); 824 | String[] envArray = getEnv(); 825 | for (String s : envArray) { 826 | String[] parts = s.split("=", 2); 827 | env.put(parts[0], parts[1]); 828 | } 829 | Process proc = pb.start(); 830 | try { 831 | System.exit(proc.waitFor()); 832 | } catch (InterruptedException e) { 833 | Thread.currentThread().interrupt(); 834 | throw new IOException(name + " failed because it got interrupted"); 835 | } 836 | } 837 | 838 | /** 839 | * Loads the included async-profiler for the current OS, architecture and glibc. 840 | * 841 | * @return the loaded async-profiler or null if anything went wrong (e.g. unsupported OS) 842 | */ 843 | public static AsyncProfiler loadOrNull() { 844 | try { 845 | return load(); 846 | } catch (IOException | IllegalStateException e) { 847 | return null; 848 | } 849 | } 850 | 851 | /** 852 | * Loads the included async-profiler for the current OS, architecture and glibc 853 | * 854 | * @return the loaded async-profiler 855 | * @throws IOException if the library could not be loaded 856 | * @throws IllegalStateException if OS or arch are not supported 857 | */ 858 | public static AsyncProfiler load() throws IOException { 859 | synchronized (AsyncProfiler.class) { 860 | try { 861 | return AsyncProfiler.getInstance(getAsyncProfilerPath().toString()); 862 | } catch (UnsatisfiedLinkError e) { 863 | throw new IllegalStateException( 864 | "Could not load async-profiler from the extraction directory " 865 | + getExtractionDirectory() 866 | + ". " 867 | + "Please make sure that the extraction directory allows execution. " 868 | + "You can specify an alternative using the system property " 869 | + EXTRACTION_PROPERTY_NAME 870 | + ".", 871 | e); 872 | } 873 | } 874 | } 875 | 876 | public static void premain(String agentArgs, Instrumentation instrumentation) { 877 | agentmain(agentArgs, instrumentation); 878 | } 879 | 880 | /** 881 | * Returns the id of the current process 882 | * 883 | * @throws IllegalStateException if the id can not be obtained, this should never happen 884 | */ 885 | public static int getProcessId() { 886 | String name = ManagementFactory.getRuntimeMXBean().getName(); 887 | int index = name.indexOf('@'); 888 | if (index < 1) { 889 | throw new IllegalStateException("Could not get process id from " + name); 890 | } 891 | try { 892 | return Integer.parseInt(name.substring(0, index)); 893 | } catch (NumberFormatException e) { 894 | throw new IllegalStateException("Could not get process id from " + name); 895 | } 896 | } 897 | 898 | /** 899 | * Attach the extracted async-profiler agent to the current JVM. 900 | * 901 | * @param arguments arguments string passed to the agent, might be null 902 | * @throws IllegalStateException if the agent could not be attached 903 | */ 904 | public static void attach(String arguments) { 905 | try { 906 | List args = new ArrayList<>(); 907 | args.add(getProcessId() + ""); 908 | args.add("load"); 909 | args.add(getAsyncProfilerPath().toString()); 910 | args.add("true"); 911 | if (arguments != null) { 912 | args.add(arguments); 913 | } 914 | executeJattach(args.toArray(new String[0])); 915 | } catch (Exception e) { 916 | throw new IllegalStateException("Could not attach to the currentd process", e); 917 | } 918 | } 919 | 920 | public static void agentmain(String agentArgs, Instrumentation instrumentation) { 921 | attach(agentArgs); 922 | } 923 | 924 | private static void printUsage(PrintStream out) { 925 | out.println("Usage: " + getApplicationCall() + " [args]"); 926 | out.println("Commands:"); 927 | out.println(" help show this help"); 928 | if (isSupported()) { 929 | out.println(" jattach run the included jattach binary"); 930 | out.println(" profiler run the included asprof"); 931 | out.println(" agentpath prints the path of the extracted async-profiler agent"); 932 | out.println(" jattachpath prints the path of the extracted jattach binary"); 933 | } 934 | out.println(" supported fails if this JAR does not include a profiler"); 935 | out.println(" for the current OS and architecture"); 936 | out.println(" converter run the included converter"); 937 | out.println(" version version of the included async-profiler"); 938 | out.println(" clear clear the directory used for storing extracted files"); 939 | } 940 | 941 | private static void checkCommandAvailability(String command) { 942 | switch (command) { 943 | case "jattach": 944 | case "profiler": 945 | case "agentpath": 946 | case "jattachpath": 947 | if (!isSupported()) { 948 | System.err.println( 949 | "The " 950 | + command 951 | + " command is not supported on this OS and architecture, using this " 952 | + "JAR"); 953 | System.exit(1); 954 | } 955 | break; 956 | default: 957 | break; 958 | } 959 | } 960 | 961 | public static void main(String[] args) throws IOException { 962 | if (args.length == 0 || args[0].equals("help")) { 963 | printUsage(System.out); 964 | return; 965 | } 966 | String command = args[0]; 967 | String[] commandArgs = Arrays.copyOfRange(args, 1, args.length); 968 | checkCommandAvailability(command); 969 | switch (command) { 970 | case "jattach": 971 | executeJattachInteractively(commandArgs); 972 | break; 973 | case "profiler": 974 | executeProfilerInteractively(commandArgs); 975 | break; 976 | case "agentpath": 977 | System.out.println(getAsyncProfilerPath()); 978 | break; 979 | case "jattachpath": 980 | System.out.println(getJattachPath()); 981 | break; 982 | case "supported": 983 | if (!isSupported()) { 984 | System.exit(1); 985 | } 986 | break; 987 | case "converter": 988 | executeConverterInteractively(commandArgs); 989 | break; 990 | case "version": 991 | System.out.println(getVersion()); 992 | break; 993 | case "clear": 994 | deleteExtractionDirectory(); 995 | break; 996 | default: 997 | System.err.println("Unknown command: " + command); 998 | System.err.println(); 999 | printUsage(System.err); 1000 | } 1001 | } 1002 | } 1003 | --------------------------------------------------------------------------------