├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── itest.yml │ ├── javadoc.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── agent ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── pyroscope │ │ │ ├── http │ │ │ ├── AggregationType.java │ │ │ ├── Format.java │ │ │ └── Units.java │ │ │ └── javaagent │ │ │ ├── AsyncProfilerDelegate.java │ │ │ ├── CurrentPidProvider.java │ │ │ ├── EventType.java │ │ │ ├── JFRJCMDProfilerDelegate.java │ │ │ ├── JFRJDKProfilerDelegate.java │ │ │ ├── JFRProfilerDelegate.java │ │ │ ├── OverfillQueue.java │ │ │ ├── ProfilerDelegate.java │ │ │ ├── ProfilerSdk.java │ │ │ ├── PyroscopeAgent.java │ │ │ ├── Snapshot.java │ │ │ ├── api │ │ │ ├── ConfigurationProvider.java │ │ │ ├── Exporter.java │ │ │ ├── Logger.java │ │ │ ├── ProfilerApi.java │ │ │ ├── ProfilerScopedContext.java │ │ │ └── ProfilingScheduler.java │ │ │ ├── config │ │ │ ├── AppName.java │ │ │ ├── Config.java │ │ │ ├── IntervalParser.java │ │ │ └── ProfilerType.java │ │ │ ├── impl │ │ │ ├── ContinuousProfilingScheduler.java │ │ │ ├── DefaultConfigurationProvider.java │ │ │ ├── DefaultLogger.java │ │ │ ├── EnvConfigurationProvider.java │ │ │ ├── ExponentialBackoff.java │ │ │ ├── ProfilerScopedContextWrapper.java │ │ │ ├── PropertiesConfigurationProvider.java │ │ │ ├── PyroscopeExporter.java │ │ │ ├── QueuedExporter.java │ │ │ └── SamplingProfilingScheduler.java │ │ │ └── util │ │ │ └── zip │ │ │ ├── GzipSink.java │ │ │ └── Util.java │ └── resources │ │ └── jfr │ │ └── pyroscope.jfc │ └── test │ └── java │ └── io │ └── pyroscope │ └── javaagent │ ├── OverfillQueueTest.java │ ├── PyroscopeAgentTest.java │ ├── StartStopTest.java │ ├── config │ ├── AppNameTest.java │ └── IntervalParserTest.java │ └── impl │ └── ExponentialBackoffTest.java ├── alpine-test.Dockerfile ├── async-profiler-context ├── README.md ├── build.gradle ├── jfr_labels.proto └── src │ ├── main │ └── java │ │ └── io │ │ └── pyroscope │ │ ├── Preconditions.java │ │ ├── PyroscopeAsyncProfiler.java │ │ └── labels │ │ ├── pb │ │ └── JfrLabels.java │ │ └── v2 │ │ ├── ConstantContext.java │ │ ├── LabelsSet.java │ │ ├── Pyroscope.java │ │ ├── ScopedContext.java │ │ └── package-info.java │ └── test │ └── java │ └── io │ └── pyroscope │ ├── ConcurrentUsageTest.java │ ├── TestApplication.java │ └── labels │ └── v2 │ └── LabelsTest.java ├── build.gradle ├── demo ├── build.gradle └── src │ └── main │ ├── java │ ├── App.java │ ├── Fib.class │ ├── Fib.java │ └── StartStopApp.java │ └── kotlin │ └── TracingContext.kt ├── docker-compose-itest.yaml ├── examples ├── Dockerfile ├── docker-compose-base.yml └── docker-compose-expt.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── itest └── query │ ├── go.mod │ ├── go.sum │ └── main.go ├── settings.gradle └── ubuntu-test.Dockerfile /.dockerignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | **/build/ 3 | **/out/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | 7 | [{*.java,build.gradle,settings.gradle}] 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }}-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu, macos] 19 | java: ['8', '11', '17', '21'] 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | - name: Set up JDK 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: ${{ matrix.java }} 28 | distribution: 'zulu' 29 | - run: ./gradlew shadowJar 30 | -------------------------------------------------------------------------------- /.github/workflows/itest.yml: -------------------------------------------------------------------------------- 1 | name: Integration smoke test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | integration-smoke-test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | svc: 19 | - 'alpine-3.16-8' 20 | - 'alpine-3.16-11' 21 | - 'alpine-3.16-17' 22 | - 'alpine-3.17-8' 23 | - 'alpine-3.17-11' 24 | - 'alpine-3.17-17' 25 | - 'alpine-3.18-8' 26 | - 'alpine-3.18-11' 27 | - 'alpine-3.18-17' 28 | - 'alpine-3.19-8' 29 | - 'alpine-3.19-11' 30 | - 'alpine-3.19-17' 31 | - 'ubuntu-18.04-8' 32 | - 'ubuntu-18.04-11' 33 | - 'ubuntu-18.04-17' 34 | - 'ubuntu-20.04-8' 35 | - 'ubuntu-20.04-11' 36 | - 'ubuntu-20.04-17' 37 | - 'ubuntu-20.04-21' 38 | - 'ubuntu-22.04-8' 39 | - 'ubuntu-22.04-11' 40 | - 'ubuntu-22.04-17' 41 | - 'ubuntu-22.04-21' 42 | steps: 43 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | with: 45 | persist-credentials: false 46 | - name: Install Go 47 | uses: actions/setup-go@v4 48 | with: 49 | go-version: 1.23.1 50 | - run: make itest 51 | shell: bash 52 | env: 53 | ITEST_SERVICE: ${{ matrix.svc }} 54 | -------------------------------------------------------------------------------- /.github/workflows/javadoc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Javadoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write # pushes to the gh-pages branch 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | with: 17 | fetch-depth: 0 18 | persist-credentials: false 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: 11 22 | - name: Generate Javadoc 23 | run: ./gradlew javaDoc 24 | - name: Deploy 🚀 25 | uses: JamesIves/github-pages-deploy-action@e6d003d0839927f5a4b998bfd92ed8e448fde37a # v4.3.4 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | branch: gh-pages 29 | clean: true 30 | folder: agent/build/docs/javadoc 31 | target-folder: javadoc 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_bump: 7 | description: 'Version Bump Type' 8 | required: true 9 | default: 'minor' 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | 16 | permissions: 17 | contents: write 18 | packages: write 19 | id-token: write 20 | 21 | jobs: 22 | release: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Get secrets 26 | uses: grafana/shared-workflows/actions/get-vault-secrets@fa48192dac470ae356b3f7007229f3ac28c48a25 27 | with: 28 | repo_secrets: | 29 | NEXUS_USERNAME=publishing:nexus_username 30 | NEXUS_PASSWORD=publishing:nexus_password 31 | NEXUS_GPG_KEY_ID=publishing:nexus_gpg_key_id 32 | NEXUS_GPG_PASSWORD=publishing:nexus_gpg_password 33 | NEXUS_GPG_SECRING_FILE_BASE64=publishing:nexus_gpg_secring_file 34 | GITHUB_APP_ID=pyroscope-development-app:app-id 35 | GITHUB_APP_PRIVATE_KEY=pyroscope-development-app:app-private-key 36 | 37 | - name: Generate GitHub token 38 | uses: actions/create-github-app-token@v1 39 | id: app-token 40 | with: 41 | app-id: ${{ env.GITHUB_APP_ID }} 42 | private-key: ${{ env.GITHUB_APP_PRIVATE_KEY }} 43 | 44 | - name: Checkout code 45 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | with: 47 | token: ${{ steps.app-token.outputs.token }} 48 | persist-credentials: true # push the tag later 49 | 50 | - name: Set up Java 8 51 | uses: actions/setup-java@v4 52 | with: 53 | java-version: '8' 54 | distribution: 'temurin' 55 | 56 | - name: Bump Version 57 | id: bump_version 58 | env: 59 | VERSION_BUMP: ${{ inputs.version_bump }} 60 | run: | 61 | current_version=$(grep 'pyroscope_version=' gradle.properties | cut -d'=' -f2) 62 | echo "Current version: $current_version" 63 | IFS='.' read -r major minor patch <<< "$current_version" 64 | 65 | case "$VERSION_BUMP" in 66 | "major") 67 | major=$((major + 1)) 68 | minor=0 69 | patch=0 70 | ;; 71 | "minor") 72 | minor=$((minor + 1)) 73 | patch=0 74 | ;; 75 | "patch") 76 | patch=$((patch + 1)) 77 | ;; 78 | esac 79 | 80 | new_version="${major}.${minor}.${patch}" 81 | echo "New version: $new_version" 82 | 83 | sed -i "s/pyroscope_version=.*/pyroscope_version=$new_version/" gradle.properties 84 | echo "version=$new_version" >> $GITHUB_OUTPUT 85 | 86 | - name: Prepare GPG Keyring 87 | id: prepare_gpg_keyring 88 | run: | 89 | mkdir -p ${{ github.workspace }}/gpg 90 | echo "$NEXUS_GPG_SECRING_FILE_BASE64" | base64 -d > ${{ github.workspace }}/gpg/secring.gpg 91 | chmod 600 ${{ github.workspace }}/gpg/secring.gpg 92 | echo "keyring_path=${{ github.workspace }}/gpg/secring.gpg" >> $GITHUB_OUTPUT 93 | 94 | - name: Build and Publish 95 | env: 96 | NEXUS_GPG_SECRING_FILE: ${{ steps.prepare_gpg_keyring.outputs.keyring_path }} 97 | run: | 98 | make publish 99 | 100 | - name: Get GitHub App User ID 101 | id: get-user-id 102 | env: 103 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 104 | APP_SLUG: ${{ steps.app-token.outputs.app-slug }} 105 | run: | 106 | APP_BOT="${APP_SLUG}[bot]" 107 | echo "user-id=$(gh api "/users/${APP_BOT}" --jq .id)" >> "$GITHUB_OUTPUT" 108 | 109 | - name: Commit Version Bump 110 | env: 111 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 112 | APP_SLUG: ${{ steps.app-token.outputs.app-slug }} 113 | USER_ID: ${{ steps.get-user-id.outputs.user-id }} 114 | VERSION: ${{ steps.bump_version.outputs.version }} 115 | run: | 116 | APP_BOT="${APP_SLUG}[bot]" 117 | git config --global user.name "${APP_BOT}" 118 | git config --global user.email "${USER_ID}+${APP_BOT}@users.noreply.github.com" 119 | git add gradle.properties 120 | git commit -m "version ${VERSION}" 121 | git tag "v${VERSION}" 122 | git push --atomic origin "refs/heads/main" "refs/tags/v${VERSION}" 123 | 124 | - name: Create GitHub Release 125 | env: 126 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 127 | VERSION: ${{ steps.bump_version.outputs.version }} 128 | run: | 129 | gh release create "v${VERSION}" \ 130 | agent/build/libs/pyroscope.jar \ 131 | --title "Release v${VERSION}" \ 132 | --notes "Automated release" 133 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [github-hosted-ubuntu-x64-small , macos-latest] 19 | java: ['8', '11', '17', '21'] 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | - name: Set up JDK 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: ${{ matrix.java }} 28 | distribution: 'zulu' 29 | - run: ./gradlew test --stacktrace 30 | javadoc: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | fetch-depth: 0 36 | persist-credentials: false 37 | - uses: actions/setup-java@v1 38 | with: 39 | java-version: 11 40 | - run: ./gradlew javaDoc --info 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | /.idea/ 3 | /.vscode/ 4 | 5 | # Ignore Gradle project-specific cache directory 6 | .gradle 7 | 8 | # Ignore Gradle build output directory 9 | build 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Mobile Tools for Java (J2ME) 21 | .mtj.tmp/ 22 | 23 | # Package Files # 24 | *.jar 25 | *.war 26 | *.nar 27 | *.ear 28 | *.zip 29 | *.tar.gz 30 | *.rar 31 | 32 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 33 | hs_err_pid* 34 | 35 | .settings 36 | .project 37 | .classpath 38 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/pyroscope-java @grafana/pyroscope-team 2 | -------------------------------------------------------------------------------- /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 2020 Pyroscope, Inc 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | rm -rf agent/build 4 | 5 | .PHONY: build 6 | build: 7 | ./gradlew shadowJar 8 | 9 | .PHONY: publish 10 | publish: 11 | @echo "./gradlew clean :agent:shadowJar publishToSonatype closeAndReleaseSonatypeStagingRepository" 12 | @./gradlew clean :agent:shadowJar publishToSonatype closeAndReleaseSonatypeStagingRepository \ 13 | -PsonatypeUsername="$(NEXUS_USERNAME)" \ 14 | -PsonatypePassword="$(NEXUS_PASSWORD)" \ 15 | -Psigning.secretKeyRingFile="$(NEXUS_GPG_SECRING_FILE)" \ 16 | -Psigning.password="$(NEXUS_GPG_PASSWORD)" \ 17 | -Psigning.keyId="$(NEXUS_GPG_KEY_ID)" 18 | @echo "https://central.sonatype.org/publish/release/#locate-and-examine-your-staging-repository" 19 | 20 | .PHONY: test 21 | test: 22 | ./gradlew test 23 | 24 | .PHONY: docker-example-base 25 | docker-example-base: build 26 | cp agent/build/libs/pyroscope.jar examples 27 | docker-compose -f examples/docker-compose-base.yml build 28 | docker-compose -f examples/docker-compose-base.yml up 29 | 30 | .PHONY: docker-example-expt 31 | docker-example-expt: build 32 | cp agent/build/libs/pyroscope.jar examples 33 | docker-compose -f examples/docker-compose-expt.yml build 34 | docker-compose -f examples/docker-compose-expt.yml up 35 | 36 | ITEST_SERVICE ?= 37 | 38 | .PHONY: itest 39 | itest: 40 | docker compose -f docker-compose-itest.yaml up --build --force-recreate -d pyroscope $(ITEST_SERVICE) 41 | cd itest/query && go run . $(ITEST_SERVICE) 42 | docker compose -f docker-compose-itest.yaml down pyroscope $(ITEST_SERVICE) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyroscope Java agent 2 | 3 | The Java profiling agent for Pyroscope.io. Based 4 | on [async-profiler](https://github.com/jvm-profiling-tools/async-profiler). 5 | 6 | ## Distribution 7 | 8 | The agent is distributed as a single JAR file `pyroscope.jar`. It contains native async-profiler libraries for: 9 | 10 | - Linux on x64; 11 | - Linux on ARM64; 12 | - MacOS on x64. 13 | - MacOS on ARM64. 14 | 15 | ## Windows OS support 16 | 17 | It also contains support for Windows OS, through JFR profiler. In order to use JFR as profiler in place of 18 | async-profiler, 19 | you need to configure profiler type, either through configuration file or environment variable. 20 | 21 | By setting `PYROSCOPE_PROFILER_TYPE` configuration variable to `JFR`, agent will use JVM built-in profiler. 22 | 23 | ## Downloads 24 | 25 | Visit [releases](https://github.com/pyroscope-io/pyroscope-java/releases) page to download the latest version 26 | of `pyroscope.jar` 27 | 28 | ## Usage 29 | 30 | Visit [docs](https://pyroscope.io/docs/java/) page for usage and configuration documentation. 31 | 32 | ## Building 33 | 34 | If you want to build the agent JAR yourself, from this repo run: 35 | 36 | ```shell 37 | ./gradlew shadowJar 38 | ``` 39 | 40 | The file will be in `agent/build/libs/pyroscope.jar`. 41 | 42 | ## Maintainers 43 | 44 | This package is maintained by [@grafana/pyroscope-java](https://github.com/orgs/grafana/teams/pyroscope-java). 45 | Mention this team on issues or PRs for feedback. 46 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO support multiple simultaneous profiling modes (profiling event types) 2 | - async-profiler dump format 3 | - ATM, the collapsed dump format (what we're using) is not supported for multiple-event profiling 4 | - can it be supported? 5 | - see 6 | - https://github.com/jvm-profiling-tools/async-profiler/issues/150 7 | - https://github.com/jvm-profiling-tools/async-profiler/issues/357 8 | - data flow changes 9 | - ATM, sample snapshots are uploaded specifying event-type-dependent parameters per snapshot 10 | - so multiple-event profiling needs a dedicated queue+snapshot+upload pipeline for each event type used simultaneously 11 | 12 | TODO support per-thread profiling 13 | - use AsnycProfiler::execute 14 | - see 15 | - https://github.com/jvm-profiling-tools/async-profiler/issues/473 16 | -------------------------------------------------------------------------------- /agent/build.gradle: -------------------------------------------------------------------------------- 1 | import java.util.jar.JarFile 2 | 3 | plugins { 4 | id 'java-library' 5 | id 'maven-publish' 6 | id 'signing' 7 | id "com.gradleup.shadow" version '8.3.1' 8 | } 9 | 10 | sourceCompatibility = JavaVersion.VERSION_1_8 11 | targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | def pyroscopeVersion = project.properties['pyroscope_version'] 18 | dependencies { 19 | api project(":async-profiler-context") 20 | compileOnly 'org.jetbrains:annotations:24.1.0' 21 | implementation('com.squareup.okhttp3:okhttp:4.12.0') 22 | implementation("com.squareup.moshi:moshi:1.14.0") 23 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0") 24 | api 'com.google.protobuf:protobuf-java:4.29.2' 25 | testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.2' 26 | testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.2' 27 | testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.10.0' 28 | testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.10.0' 29 | } 30 | 31 | javadoc{ 32 | // excluded as javadoc generation fails on inaccessible sun.management.VMManagement class 33 | exclude("io/pyroscope/javaagent/CurrentPidProvider.java") 34 | } 35 | 36 | jar { 37 | manifest { 38 | attributes( 39 | 'Premain-Class': 'io.pyroscope.javaagent.PyroscopeAgent' 40 | ) 41 | } 42 | } 43 | 44 | test { 45 | useJUnitPlatform() 46 | } 47 | 48 | java { 49 | withJavadocJar() 50 | withSourcesJar() 51 | } 52 | 53 | shadowJar { 54 | exclude 'Log4j-*' 55 | exclude 'META-INF/org/apache/logging/log4j/**' 56 | exclude 'META-INF/services/**' 57 | 58 | from("$buildDir/async-profiler/native") { 59 | include "**.so" 60 | include "**.so.sha1" 61 | } 62 | 63 | archiveFileName = "pyroscope.jar" 64 | 65 | minimize() 66 | archiveClassifier.set('') 67 | relocate("com.google", "io.pyroscope.vendor.com.google") 68 | relocate("one.profiler", "io.pyroscope.vendor.one.profiler") 69 | relocate("okio", "io.pyroscope.vendor.okio") 70 | relocate("okhttp3", "io.pyroscope.vendor.okhttp3") 71 | relocate("kotlin", "io.pyroscope.vendor.kotlin") 72 | relocate("com", "io.pyroscope.vendor.com") 73 | 74 | dependencies { 75 | exclude "org/jetbrains/**" 76 | exclude "org/intellij/**" 77 | exclude "google/protobuf/**" 78 | } 79 | } 80 | 81 | publishing { 82 | publications { 83 | shadow(MavenPublication) { publication -> 84 | project.shadow.component(publication) 85 | groupId = 'io.pyroscope' 86 | artifactId = 'agent' 87 | version = pyroscopeVersion 88 | artifacts = [ shadowJar, javadocJar, sourcesJar ] 89 | pom { 90 | name = 'Pyroscope Java agent' 91 | description = 'The Java profiling agent for Pyroscope.io. Based on async-profiler.' 92 | url = 'https://pyroscope.io' 93 | licenses { 94 | license { 95 | name = 'The Apache License, Version 2.0' 96 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 97 | } 98 | } 99 | developers { 100 | developer { 101 | id = 'pyroscope' 102 | name = 'Pyroscope' 103 | email = 'anatoly@pyroscope.io' 104 | } 105 | } 106 | scm { 107 | connection = 'scm:git:git://github.com/pyroscope-io/pyroscope-java.git' 108 | developerConnection = 'scm:git:ssh://github.com/pyroscope-io/pyroscope-java.git' 109 | url = 'https://github.com/pyroscope-io/pyroscope-java' 110 | } 111 | } 112 | } 113 | } 114 | repositories { 115 | maven { 116 | credentials { 117 | username project.hasProperty('nexusUsername') ? project.nexusUsername : '' 118 | password project.hasProperty('nexusPassword') ? project.nexusPassword : '' 119 | } 120 | url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 121 | } 122 | } 123 | } 124 | 125 | signing { 126 | sign publishing.publications.shadow 127 | } 128 | 129 | afterEvaluate { 130 | if (project.tasks.findByName('signShadowPublication')) { 131 | project.tasks.named('signShadowPublication').configure { 132 | dependsOn 'jar' 133 | dependsOn 'sourcesJar' 134 | dependsOn 'javadocJar' 135 | } 136 | } 137 | } 138 | 139 | generateMetadataFileForShadowPublication.dependsOn 'jar' 140 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/http/AggregationType.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.http; 2 | 3 | public enum AggregationType { 4 | SUM ("sum"), 5 | AVERAGE ("average"); 6 | 7 | /** 8 | * Pyroscope aggregation type id, as expected by Pyroscope's HTTP API. 9 | */ 10 | public final String id; 11 | 12 | AggregationType(String id) { 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/http/Format.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.http; 2 | 3 | public enum Format { 4 | JFR ("jfr"); 5 | 6 | /** 7 | * Profile data format, as expected by Pyroscope's HTTP API. 8 | */ 9 | public final String id; 10 | 11 | Format(String id) { 12 | this.id = id; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/http/Units.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.http; 2 | 3 | public enum Units { 4 | SAMPLES ("samples"), 5 | OBJECTS ("objects"), 6 | BYTES ("bytes"); 7 | 8 | /** 9 | * Pyroscope units id, as expected by Pyroscope's HTTP API. 10 | */ 11 | public final String id; 12 | 13 | Units(String id) { 14 | this.id = id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/AsyncProfilerDelegate.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.javaagent.config.Config; 5 | import io.pyroscope.PyroscopeAsyncProfiler; 6 | import io.pyroscope.labels.v2.Pyroscope; 7 | import one.profiler.AsyncProfiler; 8 | import one.profiler.Counter; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.io.DataInputStream; 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.time.Duration; 17 | import java.time.Instant; 18 | 19 | 20 | import static io.pyroscope.Preconditions.checkNotNull; 21 | 22 | public final class AsyncProfilerDelegate implements ProfilerDelegate { 23 | private Config config; 24 | private EventType eventType; 25 | private String alloc; 26 | private String lock; 27 | private Duration interval; 28 | private Format format; 29 | private File tempJFRFile; 30 | 31 | private final AsyncProfiler instance = PyroscopeAsyncProfiler.getAsyncProfiler(); 32 | 33 | public AsyncProfilerDelegate(@NotNull Config config) { 34 | setConfig(config); 35 | } 36 | 37 | @Override 38 | public void setConfig(@NotNull final Config config) { 39 | checkNotNull(config, "config"); 40 | this.config = config; 41 | this.alloc = config.profilingAlloc; 42 | this.lock = config.profilingLock; 43 | this.eventType = config.profilingEvent; 44 | this.interval = config.profilingInterval; 45 | this.format = config.format; 46 | 47 | if (format == Format.JFR && null == tempJFRFile) { 48 | try { 49 | // flight recorder is built on top of a file descriptor, so we need a file. 50 | tempJFRFile = File.createTempFile("pyroscope", ".jfr"); 51 | tempJFRFile.deleteOnExit(); 52 | } catch (IOException e) { 53 | throw new IllegalStateException(e); 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * Start async-profiler 60 | */ 61 | @Override 62 | public synchronized void start() { 63 | if (format == Format.JFR) { 64 | try { 65 | instance.execute(createJFRCommand()); 66 | } catch (IOException e) { 67 | throw new IllegalStateException(e); 68 | } 69 | } else { 70 | instance.start(eventType.id, interval.toNanos()); 71 | } 72 | } 73 | 74 | /** 75 | * Stop async-profiler 76 | */ 77 | @Override 78 | public synchronized void stop() { 79 | instance.stop(); 80 | } 81 | 82 | /** 83 | * @param started - time when profiling has been started 84 | * @param ended - time when profiling has ended 85 | * @return Profiling data and dynamic labels as {@link Snapshot} 86 | */ 87 | @Override 88 | @NotNull 89 | public synchronized Snapshot dumpProfile(@NotNull Instant started, @NotNull Instant ended) { 90 | return dumpImpl(started, ended); 91 | } 92 | 93 | private String createJFRCommand() { 94 | StringBuilder sb = new StringBuilder(); 95 | sb.append("start,event=").append(eventType.id); 96 | if (alloc != null && !alloc.isEmpty()) { 97 | sb.append(",alloc=").append(alloc); 98 | if (config.allocLive) { 99 | sb.append(",live"); 100 | } 101 | } 102 | if (lock != null && !lock.isEmpty()) { 103 | sb.append(",lock=").append(lock); 104 | } 105 | sb.append(",interval=").append(interval.toNanos()) 106 | .append(",file=").append(tempJFRFile.toString()); 107 | if (config.APLogLevel != null) { 108 | sb.append(",loglevel=").append(config.APLogLevel); 109 | } 110 | sb.append(",jstackdepth=").append(config.javaStackDepthMax); 111 | if (config.APExtraArguments != null) { 112 | sb.append(",").append(config.APExtraArguments); 113 | } 114 | return sb.toString(); 115 | } 116 | 117 | private Snapshot dumpImpl(Instant started, Instant ended) { 118 | if (config.gcBeforeDump) { 119 | System.gc(); 120 | } 121 | final byte[] data; 122 | if (format == Format.JFR) { 123 | data = dumpJFR(); 124 | } else { 125 | data = instance.dumpCollapsed(Counter.SAMPLES).getBytes(StandardCharsets.UTF_8); 126 | } 127 | return new Snapshot( 128 | format, 129 | eventType, 130 | started, 131 | ended, 132 | data, 133 | Pyroscope.LabelsWrapper.dump() 134 | ); 135 | } 136 | 137 | private byte[] dumpJFR() { 138 | try { 139 | byte[] bytes = new byte[(int) tempJFRFile.length()]; 140 | try (DataInputStream ds = new DataInputStream(new FileInputStream(tempJFRFile))) { 141 | ds.readFully(bytes); 142 | } 143 | return bytes; 144 | } catch (IOException e) { 145 | throw new IllegalStateException(e); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/CurrentPidProvider.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import sun.management.VMManagement; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.lang.management.RuntimeMXBean; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | /** 12 | * Hacky implementation of JVM process id (pid) handler, only used with JDK 8 implementation of {@link JFRJCMDProfilerDelegate} 13 | */ 14 | public final class CurrentPidProvider { 15 | public static long getCurrentProcessId() { 16 | RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); 17 | Field jvm = null; 18 | try { 19 | jvm = runtime.getClass().getDeclaredField("jvm"); 20 | jvm.setAccessible(true); 21 | 22 | VMManagement management = (VMManagement) jvm.get(runtime); 23 | Method method = management.getClass().getDeclaredMethod("getProcessId"); 24 | method.setAccessible(true); 25 | 26 | return (Integer) method.invoke(management); 27 | } catch (NoSuchFieldException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/EventType.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import java.util.EnumSet; 4 | import java.util.Optional; 5 | 6 | import one.profiler.Events; 7 | import io.pyroscope.http.Units; 8 | import io.pyroscope.http.AggregationType; 9 | 10 | public enum EventType { 11 | CPU (Events.CPU, Units.SAMPLES, AggregationType.SUM), 12 | ALLOC (Events.ALLOC, Units.OBJECTS, AggregationType.SUM), 13 | LOCK (Events.LOCK, Units.SAMPLES, AggregationType.SUM), 14 | WALL (Events.WALL, Units.SAMPLES, AggregationType.SUM), 15 | CTIMER (Events.CTIMER, Units.SAMPLES, AggregationType.SUM), 16 | ITIMER (Events.ITIMER, Units.SAMPLES, AggregationType.SUM); 17 | 18 | /** 19 | * Event type id, as defined in one.profiler.Events. 20 | */ 21 | public final String id; 22 | 23 | /** 24 | * Unit option, as expected by Pyroscope's HTTP API. 25 | */ 26 | public final Units units; 27 | 28 | /** 29 | * Aggregation type option, as expected by Pyroscope's HTTP API. 30 | */ 31 | public final AggregationType aggregationType; 32 | 33 | EventType(String id, Units units, AggregationType aggregationType) { 34 | this.id = id; 35 | this.units = units; 36 | this.aggregationType = aggregationType; 37 | } 38 | 39 | public static EventType fromId(String id) throws IllegalArgumentException { 40 | Optional maybeEventType = 41 | EnumSet.allOf(EventType.class) 42 | .stream() 43 | .filter(eventType -> eventType.id.equals(id)) 44 | .findAny(); 45 | return maybeEventType.orElseThrow(IllegalArgumentException::new); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/JFRJCMDProfilerDelegate.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.javaagent.config.Config; 5 | import io.pyroscope.labels.v2.Pyroscope; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.io.UncheckedIOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.nio.file.StandardCopyOption; 17 | import java.time.Instant; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | import static java.lang.String.format; 23 | 24 | /** 25 | * This implementation of JFR profiler, uses external jcmd command to manage JFR recordings. 26 | * This only to be used with JDK 8. 27 | *

28 | * NOTE: This is an experimental feature and is subject to API changes or may be removed in future releases. 29 | */ 30 | public final class JFRJCMDProfilerDelegate implements ProfilerDelegate { 31 | private static final String RECORDING_NAME = "pyroscope"; 32 | private static final String JFR_SETTINGS_RESOURCE = "/jfr/pyroscope.jfc"; 33 | 34 | private static final String OS_NAME = "os.name"; 35 | private Config config; 36 | private File tempJFRFile; 37 | private Path jcmdBin; 38 | private Path jfrSettingsPath; 39 | 40 | public JFRJCMDProfilerDelegate(Config config) { 41 | setConfig(config); 42 | } 43 | 44 | @Override 45 | public void setConfig(final Config config) { 46 | this.config = config; 47 | jcmdBin = findJcmdBin(); 48 | jfrSettingsPath = findJfrSettingsPath(config); 49 | 50 | try { 51 | tempJFRFile = File.createTempFile("pyroscope", ".jfr"); 52 | tempJFRFile.deleteOnExit(); 53 | } catch (IOException e) { 54 | throw new IllegalStateException(e); 55 | } 56 | } 57 | 58 | /** 59 | * Start JFR profiler 60 | */ 61 | @Override 62 | public synchronized void start() { 63 | List cmdLine = new ArrayList<>(); 64 | cmdLine.add(jcmdBin.toString()); 65 | cmdLine.add(String.valueOf(CurrentPidProvider.getCurrentProcessId())); 66 | cmdLine.add("JFR.start"); 67 | cmdLine.add("name=" + RECORDING_NAME); 68 | cmdLine.add("filename=" + tempJFRFile.getAbsolutePath()); 69 | cmdLine.add("settings=" + jfrSettingsPath); 70 | executeCmd(cmdLine); 71 | } 72 | 73 | /** 74 | * Stop JFR profiler 75 | */ 76 | @Override 77 | public synchronized void stop() { 78 | List cmdLine = new ArrayList<>(); 79 | cmdLine.add(jcmdBin.toString()); 80 | cmdLine.add(String.valueOf(CurrentPidProvider.getCurrentProcessId())); 81 | cmdLine.add("JFR.stop"); 82 | cmdLine.add("name=" + RECORDING_NAME); 83 | executeCmd(cmdLine); 84 | } 85 | 86 | /** 87 | * @param started - time when profiling has been started 88 | * @param ended - time when profiling has ended 89 | * @return Profiling data and dynamic labels as {@link Snapshot} 90 | */ 91 | @Override 92 | public synchronized Snapshot dumpProfile(Instant started, Instant ended) { 93 | return dumpImpl(started, ended); 94 | } 95 | 96 | private Snapshot dumpImpl(Instant started, Instant ended) { 97 | if (config.gcBeforeDump) { 98 | System.gc(); 99 | } 100 | try { 101 | byte[] data = Files.readAllBytes(tempJFRFile.toPath()); 102 | return new Snapshot( 103 | Format.JFR, 104 | EventType.CPU, 105 | started, 106 | ended, 107 | data, 108 | Pyroscope.LabelsWrapper.dump() 109 | ); 110 | } catch (IOException e) { 111 | throw new IllegalStateException(e); 112 | } 113 | } 114 | 115 | private static Path findJcmdBin() { 116 | Path javaHome = Paths.get(System.getProperty("java.home")); 117 | String jcmd = jcmdExecutable(); 118 | Path jcmdBin = javaHome.resolve("bin").resolve(jcmd); 119 | //find jcmd binary 120 | if (!Files.isExecutable(jcmdBin)) { 121 | jcmdBin = javaHome.getParent().resolve("bin").resolve(jcmd); 122 | if (!Files.isExecutable(jcmdBin)) { 123 | throw new RuntimeException("cannot find executable jcmd in Java home"); 124 | } 125 | } 126 | return jcmdBin; 127 | } 128 | 129 | private static String jcmdExecutable() { 130 | String jcmd = "jcmd"; 131 | if (isWindowsOS()) { 132 | jcmd = "jcmd.exe"; 133 | } 134 | return jcmd; 135 | } 136 | 137 | private static Path findJfrSettingsPath(Config config) { 138 | // first try to load settings from provided configuration 139 | if (config.jfrProfilerSettings != null) { 140 | return Paths.get(config.jfrProfilerSettings); 141 | } 142 | // otherwise load default settings 143 | try (InputStream inputStream = JFRJCMDProfilerDelegate.class.getResourceAsStream(JFR_SETTINGS_RESOURCE)) { 144 | Path jfrSettingsPath = Files.createTempFile("pyroscope", ".jfc"); 145 | Files.copy(inputStream, jfrSettingsPath, StandardCopyOption.REPLACE_EXISTING); 146 | return jfrSettingsPath; 147 | } catch (IOException e) { 148 | throw new UncheckedIOException(format("unable to load %s from classpath", JFR_SETTINGS_RESOURCE), e); 149 | } 150 | } 151 | 152 | private static boolean isWindowsOS() { 153 | String osName = System.getProperty(OS_NAME); 154 | return osName.contains("Windows"); 155 | } 156 | 157 | private static void executeCmd(List cmdLine) { 158 | try { 159 | ProcessBuilder processBuilder = new ProcessBuilder(cmdLine); 160 | Process process = processBuilder.redirectErrorStream(true).start(); 161 | int exitCode = process.waitFor(); 162 | if (exitCode != 0) { 163 | String processOutput = new BufferedReader(new InputStreamReader(process.getInputStream())).lines().collect(Collectors.joining("\n")); 164 | throw new RuntimeException(format("Invalid exit code %s, process output %s", exitCode, processOutput)); 165 | } 166 | } catch (IOException | InterruptedException e) { 167 | throw new RuntimeException(format("failed to start process: %s", cmdLine), e); 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/JFRJDKProfilerDelegate.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.javaagent.config.Config; 5 | import jdk.jfr.Recording; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.UncheckedIOException; 10 | import java.nio.file.Files; 11 | import java.time.Duration; 12 | import java.time.Instant; 13 | 14 | import static io.pyroscope.labels.v2.Pyroscope.*; 15 | 16 | /** 17 | * This implementation of JFR profiler, uses JDK JFR APi to manage JFR recordings. 18 | * This only to be used with JDK 9 and above. 19 | *

20 | * NOTE: This is an experimental feature and is subject to API changes or may be removed in future releases. 21 | */ 22 | public final class JFRJDKProfilerDelegate implements ProfilerDelegate { 23 | private static final String RECORDING_NAME = "pyroscope"; 24 | 25 | private Config config; 26 | private File tempJFRFile; 27 | private Recording recording; 28 | 29 | public JFRJDKProfilerDelegate(Config config) { 30 | setConfig(config); 31 | } 32 | 33 | @Override 34 | public void setConfig(final Config config) { 35 | this.config = config; 36 | try { 37 | tempJFRFile = jfrRecordingPath(); 38 | } catch (IOException e) { 39 | throw new UncheckedIOException("cannot create JFR destination path", e); 40 | } 41 | } 42 | 43 | private static File jfrRecordingPath() throws IOException { 44 | File tempJFRFile = File.createTempFile("pyroscope", ".jfr"); 45 | tempJFRFile.deleteOnExit(); 46 | return tempJFRFile; 47 | } 48 | 49 | /** 50 | * Start JFR profiler 51 | */ 52 | @Override 53 | public synchronized void start() { 54 | try { 55 | recording = new Recording(); 56 | recording.enable("jdk.ExecutionSample").withPeriod(Duration.ofMillis(1)); 57 | recording.enable("jdk.ThreadPark").withPeriod(Duration.ofMillis(10)).withStackTrace(); 58 | recording.enable("jdk.ObjectAllocationInNewTLAB").withStackTrace(); 59 | recording.enable("jdk.ObjectAllocationOutsideTLAB").withStackTrace(); 60 | recording.enable("jdk.JavaMonitorEnter").withPeriod(Duration.ofMillis(10)).withStackTrace(); 61 | recording.setToDisk(true); 62 | recording.setDestination(tempJFRFile.toPath()); 63 | recording.start(); 64 | } catch (IOException e) { 65 | throw new UncheckedIOException("cannot start JFR recording", e); 66 | } 67 | } 68 | 69 | /** 70 | * Stop JFR profiler 71 | */ 72 | @Override 73 | public synchronized void stop() { 74 | recording.stop(); 75 | } 76 | 77 | /** 78 | * @param started - time when profiling has been started 79 | * @param ended - time when profiling has ended 80 | * @return Profiling data and dynamic labels as {@link Snapshot} 81 | */ 82 | @Override 83 | public synchronized Snapshot dumpProfile(Instant started, Instant ended) { 84 | return dumpImpl(started, ended); 85 | } 86 | 87 | private Snapshot dumpImpl(Instant started, Instant ended) { 88 | if (config.gcBeforeDump) { 89 | System.gc(); 90 | } 91 | try { 92 | byte[] data = Files.readAllBytes(tempJFRFile.toPath()); 93 | return new Snapshot( 94 | Format.JFR, 95 | EventType.CPU, 96 | started, 97 | ended, 98 | data, 99 | LabelsWrapper.dump() 100 | ); 101 | } catch (IOException e) { 102 | throw new IllegalStateException(e); 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/JFRProfilerDelegate.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.javaagent.config.Config; 4 | import io.pyroscope.javaagent.impl.DefaultLogger; 5 | 6 | import java.time.Instant; 7 | 8 | import static java.lang.String.format; 9 | 10 | /** 11 | * This is a JFR profiler delegate, which checks JVM version and registers proper delegate implementation. 12 | *

13 | * NOTE: This is an experimental feature and is subject to API changes or may be removed in future releases. 14 | */ 15 | public final class JFRProfilerDelegate implements ProfilerDelegate { 16 | 17 | private ProfilerDelegate delegate; 18 | 19 | public JFRProfilerDelegate(Config config) { 20 | String javaVersion = System.getProperty("java.version"); 21 | if (javaVersion.startsWith("1.8")) { 22 | delegate = new JFRJCMDProfilerDelegate(config); 23 | } else { 24 | delegate = new JFRJDKProfilerDelegate(config); 25 | } 26 | } 27 | 28 | @Override 29 | public void setConfig(final Config config) { 30 | delegate.setConfig(config); 31 | } 32 | 33 | /** 34 | * Start JFR profiler 35 | */ 36 | @Override 37 | public synchronized void start() { 38 | delegate.start(); 39 | } 40 | 41 | /** 42 | * Stop JFR profiler 43 | */ 44 | @Override 45 | public synchronized void stop() { 46 | delegate.stop(); 47 | } 48 | 49 | /** 50 | * @param started - time when profiling has been started 51 | * @param ended - time when profiling has ended 52 | * @return Profiling data and dynamic labels as {@link Snapshot} 53 | */ 54 | @Override 55 | public synchronized Snapshot dumpProfile(Instant started, Instant ended) { 56 | return delegate.dumpProfile(started, ended); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/OverfillQueue.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import java.util.concurrent.ArrayBlockingQueue; 4 | import java.util.concurrent.locks.Condition; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | /** 8 | *

A blocking queue with a limited capacity.

9 | * 10 | *

When the queue is attempted to put a new element and is overfilled, the oldest element is dropped 11 | * so the capacity limit is preserved.

12 | * 13 | * @param the type of elements. 14 | */ 15 | public final class OverfillQueue { 16 | private final ArrayBlockingQueue innerQueue; 17 | // Guards innerQueue. 18 | private final ReentrantLock lock = new ReentrantLock(false); 19 | private final Condition notEmpty = lock.newCondition(); 20 | 21 | public OverfillQueue(final int capacity) { 22 | if (capacity < 1) { 23 | throw new IllegalArgumentException("Capacity must be >= 1"); 24 | } 25 | this.innerQueue = new ArrayBlockingQueue<>(capacity); 26 | } 27 | 28 | /** 29 | * Inserts the specified element at the tail of this queue if it is 30 | * possible to do so without exceeding the queue's capacity. If not, 31 | * drops one element from the head of the queue. 32 | */ 33 | public void put(final E element) throws InterruptedException { 34 | lock.lockInterruptibly(); 35 | try { 36 | boolean offerSuccessful = innerQueue.offer(element); 37 | if (offerSuccessful) { 38 | notEmpty.signal(); 39 | } else { 40 | // Drop one old element to ensure the capacity for the new one. 41 | innerQueue.poll(); 42 | offerSuccessful = innerQueue.offer(element); 43 | if (offerSuccessful) { 44 | notEmpty.signal(); 45 | } else { 46 | // Doing this as a sanity check. 47 | throw new RuntimeException("innerQueue.offer was not successful"); 48 | } 49 | } 50 | } finally { 51 | lock.unlock(); 52 | } 53 | } 54 | 55 | /** 56 | * Retrieves and removes the head of this queue, waiting for the element to become available if needed. 57 | */ 58 | public E take() throws InterruptedException { 59 | lock.lockInterruptibly(); 60 | try { 61 | E result; 62 | while ((result = innerQueue.poll()) == null) { 63 | notEmpty.await(); 64 | } 65 | return result; 66 | } finally { 67 | lock.unlock(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/ProfilerDelegate.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.javaagent.config.Config; 4 | import org.jetbrains.annotations.NotNull; 5 | import io.pyroscope.javaagent.config.ProfilerType; 6 | 7 | import java.time.Instant; 8 | 9 | public interface ProfilerDelegate { 10 | /** 11 | * Creates profiler delegate instance based on configuration. 12 | * 13 | * @param config 14 | * @return 15 | */ 16 | static ProfilerDelegate create(Config config) { 17 | return config.profilerType.create(config); 18 | } 19 | 20 | void start(); 21 | 22 | void stop(); 23 | 24 | @NotNull 25 | Snapshot dumpProfile(@NotNull Instant profilingStartTime, @NotNull Instant now); 26 | 27 | void setConfig(@NotNull Config config); 28 | } 29 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/ProfilerSdk.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.PyroscopeAsyncProfiler; 4 | import io.pyroscope.javaagent.api.ProfilerScopedContext; 5 | import io.pyroscope.javaagent.api.ProfilerApi; 6 | import io.pyroscope.javaagent.config.Config; 7 | import io.pyroscope.javaagent.impl.ProfilerScopedContextWrapper; 8 | import io.pyroscope.labels.v2.LabelsSet; 9 | import io.pyroscope.labels.v2.Pyroscope; 10 | import io.pyroscope.labels.v2.ScopedContext; 11 | import one.profiler.AsyncProfiler; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Map; 15 | 16 | public class ProfilerSdk implements ProfilerApi { 17 | 18 | private final AsyncProfiler asprof; 19 | 20 | public ProfilerSdk() { 21 | this.asprof = PyroscopeAsyncProfiler.getAsyncProfiler(); 22 | } 23 | @Override 24 | public void startProfiling() { 25 | PyroscopeAgent.start(Config.build()); 26 | } 27 | 28 | @Override 29 | public boolean isProfilingStarted() { 30 | return PyroscopeAgent.isStarted(); 31 | } 32 | 33 | @Deprecated 34 | @Override 35 | @NotNull 36 | public ProfilerScopedContext createScopedContext(@NotNull Map<@NotNull String, @NotNull String> labels) { 37 | return new ProfilerScopedContextWrapper(new ScopedContext(new LabelsSet(labels))); 38 | } 39 | 40 | @Override 41 | public void setTracingContext(long spanId, long spanName) { 42 | asprof.setTracingContext(spanId, spanName); 43 | } 44 | 45 | @Override 46 | public long registerConstant(String constant) { 47 | return Pyroscope.LabelsWrapper.registerConstant(constant); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/PyroscopeAgent.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.javaagent.api.Exporter; 4 | import io.pyroscope.javaagent.api.Logger; 5 | import io.pyroscope.javaagent.api.ProfilingScheduler; 6 | import io.pyroscope.javaagent.config.Config; 7 | import io.pyroscope.javaagent.impl.*; 8 | import io.pyroscope.labels.v2.ScopedContext; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.lang.instrument.Instrumentation; 12 | 13 | import static io.pyroscope.Preconditions.checkNotNull; 14 | 15 | public class PyroscopeAgent { 16 | private static final Object sLock = new Object(); 17 | private static Options sOptions = null; 18 | 19 | public static void premain(final String agentArgs, 20 | final Instrumentation inst) { 21 | final Config config; 22 | try { 23 | config = Config.build(DefaultConfigurationProvider.INSTANCE); 24 | DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.DEBUG, "Config: %s", config); 25 | } catch (final Throwable e) { 26 | DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.ERROR, "Error starting profiler %s", e); 27 | return; 28 | } 29 | start(config); 30 | } 31 | 32 | public static void start() { 33 | start(new Config.Builder().build()); 34 | } 35 | 36 | public static void start(@NotNull Config config) { 37 | checkNotNull(config, "config"); 38 | start(new Options.Builder(config).build()); 39 | } 40 | 41 | public static void start(@NotNull Options options) { 42 | checkNotNull(options, "options"); 43 | 44 | synchronized (sLock) { 45 | Logger logger = options.logger; 46 | 47 | if (!options.config.agentEnabled) { 48 | logger.log(Logger.Level.INFO, "Pyroscope agent start disabled by configuration"); 49 | return; 50 | } 51 | 52 | if (sOptions != null) { 53 | logger.log(Logger.Level.ERROR, "Failed to start profiling - already started"); 54 | return; 55 | } 56 | sOptions = options; 57 | logger.log(Logger.Level.DEBUG, "Config: %s", options.config); 58 | try { 59 | options.scheduler.start(options.profiler); 60 | ScopedContext.ENABLED.set(true); 61 | logger.log(Logger.Level.INFO, "Profiling started"); 62 | } catch (final Throwable e) { 63 | logger.log(Logger.Level.ERROR, "Error starting profiler %s", e); 64 | sOptions = null; 65 | } 66 | } 67 | } 68 | 69 | public static void stop() { 70 | ScopedContext.ENABLED.set(false); 71 | synchronized (sLock) { 72 | if (sOptions == null) { 73 | DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.ERROR, "Error stopping profiler: not started"); 74 | return; 75 | } 76 | try { 77 | sOptions.scheduler.stop(); 78 | sOptions.exporter.stop(); 79 | sOptions.logger.log(Logger.Level.INFO, "Profiling stopped"); 80 | } catch (Throwable e) { 81 | sOptions.logger.log(Logger.Level.ERROR, "Error stopping profiler %s", e); 82 | } 83 | 84 | sOptions = null; 85 | } 86 | } 87 | 88 | public static boolean isStarted() { 89 | synchronized (sLock) { 90 | return sOptions != null; 91 | } 92 | } 93 | 94 | /** 95 | * Options allow to swap pyroscope components: 96 | * - io.pyroscope.javaagent.api.ProfilingScheduler 97 | * - org.apache.logging.log4j.Logger 98 | * - io.pyroscope.javaagent.api.Exporter for io.pyroscope.javaagent.impl.ContinuousProfilingScheduler 99 | */ 100 | public static class Options { 101 | final Config config; 102 | final ProfilingScheduler scheduler; 103 | final Logger logger; 104 | final ProfilerDelegate profiler; 105 | final Exporter exporter; 106 | 107 | private Options(@NotNull Builder b) { 108 | this.config = b.config; 109 | this.profiler = b.profiler; 110 | this.scheduler = b.scheduler; 111 | this.logger = b.logger; 112 | this.exporter = b.exporter; 113 | } 114 | 115 | public static class Builder { 116 | private final Config config; 117 | private ProfilerDelegate profiler; 118 | private Exporter exporter; 119 | private ProfilingScheduler scheduler; 120 | private Logger logger; 121 | 122 | public Builder(@NotNull Config config) { 123 | checkNotNull(config, "config"); 124 | this.config = config; 125 | } 126 | 127 | public Builder setExporter(@NotNull Exporter exporter) { 128 | checkNotNull(exporter, "exporter"); 129 | this.exporter = exporter; 130 | return this; 131 | } 132 | 133 | public Builder setScheduler(@NotNull ProfilingScheduler scheduler) { 134 | checkNotNull(scheduler, "scheduler"); 135 | this.scheduler = scheduler; 136 | return this; 137 | } 138 | 139 | public Builder setLogger(@NotNull Logger logger) { 140 | checkNotNull(logger, "logger"); 141 | this.logger = logger; 142 | return this; 143 | } 144 | 145 | public Builder setProfiler(@NotNull ProfilerDelegate profiler) { 146 | checkNotNull(profiler, "logger"); 147 | this.profiler = profiler; 148 | return this; 149 | } 150 | 151 | public @NotNull Options build() { 152 | if (logger == null) { 153 | logger = new DefaultLogger(config.logLevel, System.err); 154 | } 155 | if (scheduler == null) { 156 | if (exporter == null) { 157 | exporter = new QueuedExporter(config, new PyroscopeExporter(config, logger), logger); 158 | } 159 | if (config.samplingDuration == null) { 160 | scheduler = new ContinuousProfilingScheduler(config, exporter, logger); 161 | } else { 162 | scheduler = new SamplingProfilingScheduler(config, exporter, logger); 163 | } 164 | } 165 | if (profiler == null) { 166 | profiler = ProfilerDelegate.create(config); 167 | } 168 | return new Options(this); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/Snapshot.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.labels.pb.*; 5 | 6 | import java.time.Instant; 7 | 8 | public final class Snapshot { 9 | public final Format format; 10 | public final EventType eventType; 11 | public final Instant started; 12 | public final Instant ended; 13 | public final byte[] data; 14 | public final JfrLabels.LabelsSnapshot labels; 15 | 16 | Snapshot(Format format, final EventType eventType, final Instant started, final Instant ended,final byte[] data, JfrLabels.LabelsSnapshot labels) { 17 | this.format = format; 18 | this.eventType = eventType; 19 | this.started = started; 20 | this.ended = ended; 21 | this.data = data; 22 | this.labels = labels; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/ConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public interface ConfigurationProvider { 7 | @Nullable 8 | String get(@NotNull String key); 9 | } 10 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/Exporter.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | import io.pyroscope.javaagent.Snapshot; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public interface Exporter { 7 | /** 8 | * PyroscopeAgent expects {@link Exporter#export(Snapshot)} method to be synchronous to profiling schedule, and should return as fast as 9 | * possible.
See QueuedExporter for an asynchronous implementation example.
10 | * Here is an example of an alternative to {@link io.pyroscope.javaagent.impl.PyroscopeExporter} 11 | *
12 |      * class KafkaExporter implements Exporter {
13 |      *     final KafkaProducer<String, String> kafkaProducer;
14 |      *     private MyExporter(KafkaProducer<String, String> producer) {
15 |      *         this.kafkaProducer = producer;
16 |      *     }
17 |      *     @Override
18 |      *     public void export(Snapshot snapshot) {
19 |      *         kafkaProducer.send(new ProducerRecord<>("test.app.jfr", gson.toJson(snapshot)));
20 |      *     }
21 |      * }
22 |      * 
23 | * 24 | */ 25 | void export(@NotNull Snapshot snapshot); 26 | 27 | /** 28 | * Stop the resources that are held by the exporter like Threads and so on... 29 | */ 30 | void stop(); 31 | } 32 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/Logger.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | public interface Logger { 4 | enum Level { 5 | DEBUG(0), 6 | INFO(1), 7 | WARN(2), 8 | ERROR(3); 9 | public final int level; 10 | 11 | Level(int level) { 12 | this.level = level; 13 | } 14 | } 15 | 16 | void log(Level l, String msg, Object... args); 17 | } 18 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/ProfilerApi.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Map; 6 | 7 | public interface ProfilerApi { 8 | void startProfiling(); 9 | 10 | boolean isProfilingStarted(); 11 | 12 | @Deprecated 13 | @NotNull ProfilerScopedContext createScopedContext(@NotNull Map<@NotNull String, @NotNull String> labels); 14 | 15 | void setTracingContext(long spanId, long spanName); 16 | 17 | long registerConstant(String constant); 18 | } 19 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/ProfilerScopedContext.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.function.BiConsumer; 6 | 7 | public interface ProfilerScopedContext { 8 | void forEachLabel(@NotNull BiConsumer<@NotNull String, @NotNull String> consumer); 9 | void close(); 10 | } 11 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/api/ProfilingScheduler.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.api; 2 | 3 | import io.pyroscope.javaagent.ProfilerDelegate; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.time.Instant; 7 | 8 | /** 9 | * 10 | */ 11 | public interface ProfilingScheduler { 12 | /** 13 | * Use AsyncProfilerDelegate's to start, stop, dumpProfile 14 | * {@link ProfilerDelegate#start()} 15 | * {@link ProfilerDelegate#stop()} 16 | * {@link ProfilerDelegate#dumpProfile(Instant, Instant)} 17 | * Here is an example of naive implementation 18 | *
19 |      * public void start(ProfilerDelegate profiler) {
20 |      *      new Thread(() -> {
21 |      *          while (true) {
22 |      *              Instant startTime = Instant.now();
23 |      *              profiler.start();
24 |      *              sleep(10);
25 |      *              profiler.stop();
26 |      *              exporter.export(
27 |      *                  profiler.dumpProfile(startTime)
28 |      *              );
29 |      *              sleep(50);
30 |      *          }
31 |      *      }).start();
32 |      *  }
33 |      * 
34 | **/ 35 | void start(@NotNull ProfilerDelegate profiler); 36 | 37 | void stop(); 38 | } 39 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/config/AppName.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.config; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.*; 6 | 7 | public class AppName { 8 | final String name; 9 | final Map labels; 10 | 11 | public AppName(String name, Map labels) { 12 | this.name = name; 13 | this.labels = Collections.unmodifiableMap(new TreeMap<>(labels)); 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | if (labels.isEmpty()) { 19 | return name; 20 | } 21 | StringJoiner joinedLabels = new StringJoiner(","); 22 | for (Map.Entry e : this.labels.entrySet()) { 23 | joinedLabels.add((e.getKey().trim()) + "=" + (e.getValue().trim())); 24 | } 25 | return String.format("%s{%s}", name, joinedLabels); 26 | } 27 | 28 | public Builder newBuilder() { 29 | return new Builder(name, labels); 30 | } 31 | 32 | public static class Builder { 33 | private String name; 34 | private Map labels; 35 | 36 | public Builder(String name) { 37 | this.name = name; 38 | this.labels = new TreeMap<>(); 39 | } 40 | 41 | public Builder(String name, Map labels) { 42 | this.name = name; 43 | this.labels = new TreeMap<>(labels); 44 | } 45 | 46 | public Builder setName(String name) { 47 | this.name = name; 48 | return this; 49 | } 50 | 51 | public Builder addLabel(String k, String v) { 52 | if (isValidLabel(k) || isValidLabel(v)) { 53 | this.labels.put(k, v); 54 | } 55 | return this; 56 | } 57 | 58 | public Builder addLabels(Map labels) { 59 | for (Map.Entry it : labels.entrySet()) { 60 | addLabel(it.getKey(), it.getValue()); 61 | } 62 | return this; 63 | } 64 | 65 | public AppName build() { 66 | return new AppName(name, labels); 67 | } 68 | } 69 | 70 | public static AppName parse(String appName) { 71 | int l = appName.indexOf('{'); 72 | int r = appName.indexOf('}'); 73 | if (l != -1 && r != -1 && l < r) { 74 | String name = appName.substring(0, l); 75 | String strLabels = appName.substring(l + 1, r); 76 | Map labelsMap = parseLabels(strLabels); 77 | return new AppName(name, labelsMap); 78 | } else { 79 | return new AppName(appName, Collections.emptyMap()); 80 | } 81 | } 82 | 83 | @NotNull 84 | public static Map parseLabels(String strLabels) { 85 | String[] labels = strLabels.split(","); 86 | Map labelMap = new HashMap<>(); 87 | for (String label : labels) { 88 | String[] kv = label.split("="); 89 | if (kv.length != 2) { 90 | continue; 91 | } 92 | kv[0] = kv[0].trim(); 93 | kv[1] = kv[1].trim(); 94 | if (!isValidLabel(kv[0]) || !isValidLabel(kv[1])) { 95 | continue; 96 | } 97 | labelMap.put(kv[0], kv[1]); 98 | } 99 | return labelMap; 100 | } 101 | 102 | public static boolean isValidLabel(String s) { 103 | if (s.isEmpty()) { 104 | return false; 105 | } 106 | for (int i = 0; i < s.length(); i++) { 107 | int c = s.codePointAt(i); 108 | if (c == '{' || c == '}' || c == ',' || c == '=' || c == ' ') { 109 | return false; 110 | } 111 | } 112 | return true; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/config/IntervalParser.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.config; 2 | 3 | import java.time.Duration; 4 | import java.time.temporal.ChronoUnit; 5 | import java.time.temporal.TemporalUnit; 6 | 7 | public final class IntervalParser { 8 | public static Duration parse(final String str) throws NumberFormatException { 9 | final long amount; 10 | final TemporalUnit unit; 11 | if (str.endsWith("ms")) { 12 | unit = ChronoUnit.MILLIS; 13 | amount = Long.parseLong(str.substring(0, str.length() - 2)); 14 | } else if (str.endsWith("us")) { 15 | unit = ChronoUnit.MICROS; 16 | amount = Long.parseLong(str.substring(0, str.length() - 2)); 17 | } else if (str.endsWith("s")) { 18 | unit = ChronoUnit.SECONDS; 19 | amount = Long.parseLong(str.substring(0, str.length() - 1)); 20 | } else if (Character.isDigit(str.charAt(str.length() - 1))) { 21 | unit = ChronoUnit.NANOS; 22 | amount = Long.parseLong(str); 23 | } else { 24 | throw new NumberFormatException("Cannot parse interval " + str); 25 | } 26 | 27 | if (amount <= 0) { 28 | throw new NumberFormatException("Interval must be positive, but " + str + " given"); 29 | } 30 | 31 | return Duration.of(amount, unit); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/config/ProfilerType.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.config; 2 | 3 | import io.pyroscope.javaagent.AsyncProfilerDelegate; 4 | import io.pyroscope.javaagent.JFRProfilerDelegate; 5 | import io.pyroscope.javaagent.ProfilerDelegate; 6 | 7 | import java.util.function.Function; 8 | 9 | public enum ProfilerType { 10 | /** 11 | * JFR profiler type. 12 | *

13 | * NOTE: This is an experimental feature and is subject to API changes or may be removed in future releases. 14 | */ 15 | JFR(JFRProfilerDelegate::new), 16 | ASYNC(AsyncProfilerDelegate::new); 17 | 18 | private final Function profilerDelegateFactory; 19 | 20 | ProfilerType(Function profilerDelegateFactory) { 21 | this.profilerDelegateFactory = profilerDelegateFactory; 22 | } 23 | 24 | public ProfilerDelegate create(Config config) { 25 | return profilerDelegateFactory.apply(config); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/ContinuousProfilingScheduler.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.ProfilerDelegate; 4 | import io.pyroscope.javaagent.Snapshot; 5 | import io.pyroscope.javaagent.api.Exporter; 6 | import io.pyroscope.javaagent.api.Logger; 7 | import io.pyroscope.javaagent.api.ProfilingScheduler; 8 | import io.pyroscope.javaagent.config.Config; 9 | import kotlin.random.Random; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.time.Duration; 13 | import java.time.Instant; 14 | import java.util.concurrent.*; 15 | 16 | 17 | public class ContinuousProfilingScheduler implements ProfilingScheduler { 18 | public static final ThreadFactory THREAD_FACTORY = r -> { 19 | Thread t = Executors.defaultThreadFactory().newThread(r); 20 | t.setName("PyroscopeProfilingScheduler"); 21 | t.setDaemon(true); 22 | return t; 23 | }; 24 | private final Config config; 25 | 26 | private ScheduledExecutorService executor; 27 | private final Exporter exporter; 28 | private final Logger logger; 29 | private final Object lock = new Object(); 30 | private Instant profilingIntervalStartTime; 31 | private ScheduledFuture job; 32 | private boolean started; 33 | private ProfilerDelegate profiler; 34 | 35 | public ContinuousProfilingScheduler(@NotNull Config config, @NotNull Exporter exporter, @NotNull Logger logger) { 36 | this.config = config; 37 | this.exporter = exporter; 38 | this.logger = logger; 39 | } 40 | 41 | @Override 42 | public void start(@NotNull ProfilerDelegate profiler) { 43 | this.logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler starting"); 44 | synchronized (lock) { 45 | if (started) { 46 | throw new IllegalStateException("already started"); 47 | } 48 | Duration firstProfilingDuration; 49 | try { 50 | firstProfilingDuration = startFirst(profiler); 51 | } catch (Throwable throwable) { 52 | stopSchedulerLocked(); 53 | throw new IllegalStateException(throwable); 54 | } 55 | this.profiler = profiler; 56 | this.executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY); 57 | this.job = executor.scheduleAtFixedRate(this::schedulerTick, 58 | firstProfilingDuration.toMillis(), config.uploadInterval.toMillis(), TimeUnit.MILLISECONDS); 59 | this.started = true; 60 | logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler started"); 61 | } 62 | } 63 | 64 | @Override 65 | public void stop() { 66 | ScheduledExecutorService svc = null; 67 | try { 68 | synchronized (lock) { 69 | try { 70 | stopSchedulerLocked(); 71 | } finally { 72 | svc = this.executor; 73 | this.executor = null; 74 | } 75 | } 76 | this.logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler stopped"); 77 | } finally { 78 | // shutdown here not under lock to avoid deadlock ( the task may block to wait for lock and 79 | // we are holding the lock and waiting for task to finish) 80 | // There is still synchronization happens from the PyroscopeAgent class, 81 | // so there are no concurrent calls to start/stop. So there is no lock here 82 | awaitTermination(svc); 83 | } 84 | } 85 | 86 | private static void awaitTermination(ScheduledExecutorService svc) { 87 | try { 88 | boolean terminated = svc.awaitTermination(10, TimeUnit.SECONDS); 89 | if (!terminated) { 90 | throw new IllegalStateException("failed to terminate scheduler's executor"); 91 | } 92 | } catch (InterruptedException e) { 93 | Thread.currentThread().interrupt(); 94 | throw new IllegalStateException("failed to terminate scheduler's executor", e); 95 | } 96 | } 97 | 98 | private void stopSchedulerLocked() { 99 | if (!this.started) { 100 | return; 101 | } 102 | this.logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler stopping"); 103 | try { 104 | this.profiler.stop(); 105 | } catch (Throwable throwable) { 106 | throw new IllegalStateException(throwable); 107 | } finally { 108 | job.cancel(true); 109 | executor.shutdown(); 110 | this.started = false; 111 | } 112 | } 113 | 114 | 115 | private void schedulerTick() { 116 | 117 | synchronized (lock) { 118 | if (!started) { 119 | return; 120 | } 121 | logger.log(Logger.Level.DEBUG, "ContinuousProfilingScheduler#schedulerTick"); 122 | Snapshot snapshot; 123 | Instant now; 124 | try { 125 | profiler.stop(); 126 | now = Instant.now(); 127 | snapshot = profiler.dumpProfile(this.profilingIntervalStartTime, now); 128 | profiler.start(); 129 | } catch (Throwable throwable) { 130 | logger.log(Logger.Level.ERROR, "Error dumping profiler %s", throwable); 131 | stopSchedulerLocked(); 132 | return; 133 | } 134 | profilingIntervalStartTime = now; 135 | exporter.export(snapshot); 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Starts the first profiling interval. 142 | * profilingIntervalStartTime is set to now 143 | * Duration of the first profiling interval is a random fraction of uploadInterval not smaller than 2000ms. 144 | * 145 | * @return Duration of the first profiling interval 146 | */ 147 | private Duration startFirst(ProfilerDelegate profiler) { 148 | Instant now = Instant.now(); 149 | 150 | long uploadIntervalMillis = config.uploadInterval.toMillis(); 151 | float randomOffset = Random.Default.nextFloat(); 152 | uploadIntervalMillis = (long) ((float) uploadIntervalMillis * randomOffset); 153 | if (uploadIntervalMillis < 2000) { 154 | uploadIntervalMillis = 2000; 155 | } 156 | Duration firstProfilingDuration = Duration.ofMillis(uploadIntervalMillis); 157 | 158 | profiler.start(); 159 | profilingIntervalStartTime = now; 160 | return firstProfilingDuration; 161 | } 162 | 163 | 164 | } 165 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/DefaultConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.api.ConfigurationProvider; 4 | import io.pyroscope.javaagent.api.Logger; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Delegates configuration provision to multiple sources 17 | * - System.getProperties 18 | * - System.getenv 19 | * - pyroscope.properties configuration file 20 | * pyroscope.properties file can be overridden by PYROSCOPE_CONFIGURATION_FILE_CONFIG 21 | */ 22 | public class DefaultConfigurationProvider implements ConfigurationProvider { 23 | private static final String PYROSCOPE_CONFIGURATION_FILE_CONFIG = "PYROSCOPE_CONFIGURATION_FILE"; 24 | private static final String DEFAULT_CONFIGURATION_FILE = "pyroscope.properties"; 25 | 26 | public static final DefaultConfigurationProvider INSTANCE = new DefaultConfigurationProvider(); 27 | 28 | final List delegates = new ArrayList<>(); 29 | 30 | public DefaultConfigurationProvider() { 31 | delegates.add(new PropertiesConfigurationProvider(System.getProperties())); 32 | delegates.add(new EnvConfigurationProvider()); 33 | String configFile = getPropertiesFile(); 34 | try { 35 | delegates.add(new PropertiesConfigurationProvider( 36 | Files.newInputStream(Paths.get(configFile)) 37 | )); 38 | } catch (IOException ignored) { 39 | } 40 | try { 41 | InputStream res = this.getClass().getResourceAsStream(configFile); 42 | if (res != null) { 43 | delegates.add(new PropertiesConfigurationProvider(res)); 44 | } 45 | } catch (IOException ignored) { 46 | } 47 | if (!configFile.equals(DEFAULT_CONFIGURATION_FILE) && delegates.size() == 2) { 48 | DefaultLogger.PRECONFIG_LOGGER.log(Logger.Level.WARN, "%s configuration file was specified but was not found", configFile); 49 | } 50 | } 51 | 52 | @Override 53 | @Nullable 54 | public String get(@NotNull String key) { 55 | for (int i = 0; i < delegates.size(); i++) { 56 | String v = delegates.get(i).get(key); 57 | if (v != null) { 58 | return v; 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | private String getPropertiesFile() { 65 | String f = get(PYROSCOPE_CONFIGURATION_FILE_CONFIG); 66 | if (f == null) { 67 | return DEFAULT_CONFIGURATION_FILE; 68 | } 69 | return f; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/DefaultLogger.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.api.Logger; 4 | 5 | import java.io.PrintStream; 6 | import java.text.DateFormat; 7 | import java.text.SimpleDateFormat; 8 | 9 | public class DefaultLogger implements Logger { 10 | public static final Logger PRECONFIG_LOGGER = new DefaultLogger(Level.DEBUG, System.err); 11 | private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 12 | private final Level logLevel; 13 | private final PrintStream out; 14 | 15 | public DefaultLogger(Level logLevel, PrintStream out) { 16 | this.logLevel = logLevel; 17 | this.out = out; 18 | } 19 | 20 | @Override 21 | public void log(Level logLevel, String msg, Object... args) { 22 | if (logLevel.level < this.logLevel.level) { 23 | return; 24 | } 25 | String date; 26 | synchronized (this) { 27 | date = DATE_FORMAT.format(System.currentTimeMillis()); 28 | } 29 | String formattedMsg = (msg == null) ? "null" 30 | : (args == null || args.length == 0) ? msg : String.format(msg, args); 31 | 32 | out.printf("%s [%s] %s%n", date, logLevel, formattedMsg); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/EnvConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.api.ConfigurationProvider; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.Map; 8 | 9 | public class EnvConfigurationProvider implements ConfigurationProvider { 10 | 11 | private final Map env; 12 | 13 | public EnvConfigurationProvider() { 14 | env = System.getenv(); 15 | } 16 | 17 | @Override 18 | @Nullable 19 | public String get(@NotNull String key) { 20 | return env.get(key); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/ExponentialBackoff.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * Exponential backoff counter implementing the Full Jitter algorithm from 7 | * here. 8 | */ 9 | final class ExponentialBackoff { 10 | private final Random random; 11 | 12 | private final int base; 13 | private final int cap; 14 | 15 | private int attempt = -1; 16 | 17 | ExponentialBackoff(final int base, final int cap, final Random random) { 18 | this.base = base; 19 | this.cap = cap; 20 | this.random = random; 21 | } 22 | 23 | final int error() { 24 | attempt += 1; 25 | int multiplier = cap / base; 26 | // from https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.19 27 | // "If the promoted type of the left-hand operand is int, then only the five lowest-order bits of the right-hand operand are used as the shift distance". 28 | if (attempt < 32 && (multiplier >> attempt) > 0) { 29 | multiplier = 1 << attempt; 30 | } 31 | return random.nextInt(base * multiplier); 32 | } 33 | 34 | final void reset() { 35 | attempt = -1; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/ProfilerScopedContextWrapper.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.api.ProfilerScopedContext; 4 | import io.pyroscope.labels.v2.ScopedContext; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.function.BiConsumer; 8 | 9 | public class ProfilerScopedContextWrapper implements ProfilerScopedContext { 10 | private final ScopedContext ctx; 11 | 12 | public ProfilerScopedContextWrapper(@NotNull ScopedContext ctx) { 13 | this.ctx = ctx; 14 | } 15 | 16 | @Override 17 | public void forEachLabel(@NotNull BiConsumer<@NotNull String, @NotNull String> consumer) { 18 | ctx.forEachLabel(consumer); 19 | } 20 | 21 | @Override 22 | public void close() { 23 | ctx.close(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/PropertiesConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.api.ConfigurationProvider; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Locale; 10 | import java.util.Properties; 11 | 12 | public class PropertiesConfigurationProvider implements ConfigurationProvider { 13 | final Properties properties; 14 | 15 | public PropertiesConfigurationProvider(Properties properties) { 16 | this.properties = properties; 17 | } 18 | 19 | public PropertiesConfigurationProvider(InputStream source) throws IOException { 20 | this.properties = new Properties(); 21 | this.properties.load(source); 22 | } 23 | 24 | @Override 25 | @Nullable 26 | public String get(@NotNull String key) { 27 | String v = properties.getProperty(key); 28 | if (v == null) { 29 | String k2 = key.toLowerCase(Locale.ROOT) 30 | .replace('_', '.'); 31 | v = properties.getProperty(k2); 32 | } 33 | return v; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.EventType; 4 | import io.pyroscope.javaagent.Snapshot; 5 | import io.pyroscope.javaagent.api.Exporter; 6 | import io.pyroscope.javaagent.api.Logger; 7 | import io.pyroscope.javaagent.config.Config; 8 | import io.pyroscope.javaagent.util.zip.GzipSink; 9 | import io.pyroscope.labels.v2.Pyroscope; 10 | import okhttp3.*; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.io.IOException; 14 | import java.time.Instant; 15 | import java.util.Random; 16 | import java.util.zip.Deflater; 17 | 18 | public class PyroscopeExporter implements Exporter { 19 | 20 | private static final MediaType PROTOBUF = MediaType.parse("application/x-protobuf"); 21 | 22 | final Config config; 23 | final Logger logger; 24 | final OkHttpClient client; 25 | final String staticLabels; 26 | public PyroscopeExporter(Config config, Logger logger) { 27 | this.config = config; 28 | this.logger = logger; 29 | this.staticLabels = nameWithStaticLabels(); 30 | this.client = new OkHttpClient.Builder() 31 | .connectTimeout(config.profileExportTimeout) 32 | .readTimeout(config.profileExportTimeout) 33 | .callTimeout(config.profileExportTimeout) 34 | .build(); 35 | 36 | } 37 | 38 | @Override 39 | public void export(@NotNull Snapshot snapshot) { 40 | try { 41 | uploadSnapshot(snapshot); 42 | } catch (final InterruptedException ignored) { 43 | Thread.currentThread().interrupt(); 44 | } 45 | } 46 | 47 | @Override 48 | public void stop() { 49 | client.dispatcher().executorService().shutdown(); 50 | client.connectionPool().evictAll(); 51 | try { 52 | if (client.cache() != null) { 53 | client.cache().close(); 54 | } 55 | } 56 | catch (final IOException ignored) {} 57 | } 58 | 59 | private void uploadSnapshot(final Snapshot snapshot) throws InterruptedException { 60 | final HttpUrl url = urlForSnapshot(snapshot); 61 | final ExponentialBackoff exponentialBackoff = new ExponentialBackoff(1_000, 30_000, new Random()); 62 | boolean retry = true; 63 | int tries = 0; 64 | while (retry) { 65 | tries++; 66 | final RequestBody requestBody; 67 | byte[] labels = snapshot.labels.toByteArray(); 68 | logger.log(Logger.Level.DEBUG, "Upload attempt %d to %s. %s %s JFR: %s, labels: %s", tries, url.toString(), 69 | snapshot.started.toString(), snapshot.ended.toString(), snapshot.data.length, labels.length); 70 | MultipartBody.Builder bodyBuilder = new MultipartBody.Builder() 71 | .setType(MultipartBody.FORM); 72 | RequestBody jfrBody = RequestBody.create(snapshot.data); 73 | if (config.compressionLevelJFR != Deflater.NO_COMPRESSION) { 74 | jfrBody = GzipSink.gzip(jfrBody, config.compressionLevelJFR); 75 | } 76 | bodyBuilder.addFormDataPart("jfr", "jfr", jfrBody); 77 | if (labels.length > 0) { 78 | RequestBody labelsBody = RequestBody.create(labels, PROTOBUF); 79 | if (config.compressionLevelLabels != Deflater.NO_COMPRESSION) { 80 | labelsBody = GzipSink.gzip(labelsBody, config.compressionLevelLabels); 81 | } 82 | bodyBuilder.addFormDataPart("labels", "labels", labelsBody); 83 | } 84 | requestBody = bodyBuilder.build(); 85 | Request.Builder request = new Request.Builder() 86 | .post(requestBody) 87 | .url(url); 88 | 89 | config.httpHeaders.forEach((k, v) -> request.header(k, v)); 90 | 91 | addAuthHeader(request, url, config); 92 | 93 | 94 | try (Response response = client.newCall(request.build()).execute()) { 95 | int status = response.code(); 96 | if (status >= 400) { 97 | ResponseBody body = response.body(); 98 | final String responseBody; 99 | if (body == null) { 100 | responseBody = ""; 101 | } else { 102 | responseBody = body.string(); 103 | } 104 | logger.log(Logger.Level.ERROR, "Error uploading snapshot: %s %s", status, responseBody); 105 | retry = shouldRetry(status); 106 | } else { 107 | retry = false; 108 | } 109 | } catch (final IOException e) { 110 | logger.log(Logger.Level.ERROR, "Error uploading snapshot: %s", e.getMessage()); 111 | } 112 | if (retry) { 113 | if (config.ingestMaxTries >= 0 && tries >= config.ingestMaxTries) { 114 | logger.log(Logger.Level.ERROR, "Gave up uploading profiling snapshot after %d tries", tries); 115 | break; 116 | } 117 | final int backoff = exponentialBackoff.error(); 118 | logger.log(Logger.Level.DEBUG, "Backing off for %s ms", backoff); 119 | Thread.sleep(backoff); 120 | } 121 | } 122 | } 123 | 124 | private static boolean shouldRetry(int status) { 125 | boolean isRateLimited = (status == 429); 126 | boolean isServerError = (status >= 500 && status <= 599); 127 | 128 | return isRateLimited || isServerError; 129 | } 130 | 131 | private static void addAuthHeader(Request.Builder request, HttpUrl url, Config config) { 132 | if (config.tenantID != null && !config.tenantID.isEmpty()) { 133 | request.header("X-Scope-OrgID", config.tenantID); 134 | } 135 | if (config.basicAuthUser != null && !config.basicAuthUser.isEmpty() 136 | && config.basicAuthPassword != null && !config.basicAuthPassword.isEmpty()) { 137 | request.header("Authorization", Credentials.basic(config.basicAuthUser, config.basicAuthPassword)); 138 | return; 139 | } 140 | String u = url.username(); 141 | String p = url.password(); 142 | if (!u.isEmpty() && !p.isEmpty()) { 143 | request.header("Authorization", Credentials.basic(u, p)); 144 | return; 145 | } 146 | if (config.authToken != null && !config.authToken.isEmpty()) { 147 | request.header("Authorization", "Bearer " + config.authToken); 148 | } 149 | } 150 | 151 | private HttpUrl urlForSnapshot(final Snapshot snapshot) { 152 | Instant started = snapshot.started; 153 | Instant finished = snapshot.ended; 154 | HttpUrl.Builder builder = HttpUrl.parse(config.serverAddress) 155 | .newBuilder() 156 | .addPathSegment("ingest") 157 | .addQueryParameter("name", staticLabels) 158 | .addQueryParameter("units", snapshot.eventType.units.id) 159 | .addQueryParameter("aggregationType", snapshot.eventType.aggregationType.id) 160 | .addQueryParameter("from", Long.toString(started.getEpochSecond())) 161 | .addQueryParameter("until", Long.toString(finished.getEpochSecond())) 162 | .addQueryParameter("spyName", Config.DEFAULT_SPY_NAME); 163 | if (EventType.CPU == snapshot.eventType || EventType.ITIMER == snapshot.eventType || EventType.WALL == snapshot.eventType) { 164 | builder.addQueryParameter("sampleRate", Long.toString(config.profilingIntervalInHertz())); 165 | } 166 | builder.addQueryParameter("format", "jfr"); 167 | return builder.build(); 168 | } 169 | 170 | private String nameWithStaticLabels() { 171 | return config.timeseries.newBuilder() 172 | .addLabels(config.labels) 173 | .addLabels(Pyroscope.getStaticLabels()) 174 | .build() 175 | .toString(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/QueuedExporter.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import io.pyroscope.javaagent.OverfillQueue; 4 | import io.pyroscope.javaagent.Snapshot; 5 | import io.pyroscope.javaagent.api.Exporter; 6 | import io.pyroscope.javaagent.api.Logger; 7 | import io.pyroscope.javaagent.config.Config; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class QueuedExporter implements Exporter { 11 | final Exporter impl; 12 | final Logger logger; 13 | private final Thread thread; 14 | private final OverfillQueue queue; 15 | 16 | public QueuedExporter(Config config, Exporter impl, Logger logger) { 17 | this.impl = impl; 18 | this.logger = logger; 19 | this.thread = new Thread(this::exportLoop); 20 | this.thread.setDaemon(true); 21 | this.queue = new OverfillQueue<>(config.pushQueueCapacity); 22 | 23 | this.thread.start(); 24 | } 25 | 26 | private void exportLoop() { 27 | logger.log(Logger.Level.DEBUG, "Uploading started"); 28 | try { 29 | while (!Thread.currentThread().isInterrupted()) { 30 | final Snapshot snapshot = queue.take(); 31 | impl.export(snapshot); 32 | } 33 | } catch (final InterruptedException e) { 34 | logger.log(Logger.Level.DEBUG, "Uploading interrupted"); 35 | Thread.currentThread().interrupt(); 36 | } 37 | } 38 | 39 | @Override 40 | public void export(@NotNull Snapshot snapshot) { 41 | try { 42 | queue.put(snapshot); 43 | } catch (final InterruptedException ignored) { 44 | Thread.currentThread().interrupt(); 45 | } 46 | } 47 | 48 | @Override 49 | public void stop() { 50 | try { 51 | this.thread.interrupt(); 52 | } catch (Exception e) { 53 | logger.log(Logger.Level.ERROR, "Error stopping thread", e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/impl/SamplingProfilingScheduler.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | 4 | import io.pyroscope.javaagent.AsyncProfilerDelegate; 5 | import io.pyroscope.javaagent.EventType; 6 | import io.pyroscope.javaagent.ProfilerDelegate; 7 | import io.pyroscope.javaagent.Snapshot; 8 | import io.pyroscope.javaagent.api.Exporter; 9 | import io.pyroscope.javaagent.api.Logger; 10 | import io.pyroscope.javaagent.api.ProfilingScheduler; 11 | import io.pyroscope.javaagent.config.Config; 12 | import kotlin.random.Random; 13 | 14 | import io.pyroscope.javaagent.config.Config.Builder; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.time.Duration; 18 | import java.time.Instant; 19 | import java.util.concurrent.Executors; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.ScheduledFuture; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | /** 25 | * Schedule profiling in sampling mode. 26 | *

27 | * WARNING: still experimental, may go away or behavior may change 28 | */ 29 | public class SamplingProfilingScheduler implements ProfilingScheduler { 30 | 31 | private final Config config; 32 | private final Exporter exporter; 33 | private Logger logger; 34 | 35 | private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> { 36 | Thread t = Executors.defaultThreadFactory().newThread(r); 37 | t.setName("PyroscopeProfilingScheduler_Sampling"); 38 | t.setDaemon(true); 39 | return t; 40 | }); 41 | private ScheduledFuture job; 42 | 43 | public SamplingProfilingScheduler(@NotNull Config config, @NotNull Exporter exporter, @NotNull Logger logger) { 44 | this.config = config; 45 | this.exporter = exporter; 46 | this.logger = logger; 47 | } 48 | 49 | @Override 50 | public void start(@NotNull ProfilerDelegate profiler) { 51 | final long samplingDurationMillis = config.samplingDuration.toMillis(); 52 | final Duration uploadInterval = config.uploadInterval; 53 | 54 | final Runnable task = (null != config.samplingEventOrder) ? 55 | () -> { 56 | for (int i = 0; i < config.samplingEventOrder.size(); i++) { 57 | final EventType t = config.samplingEventOrder.get(i); 58 | final Config tmp = isolate(t, config); 59 | logger.log(Logger.Level.DEBUG, "Config for %s ordinal %d: %s", t.id, i, tmp); 60 | profiler.setConfig(tmp); 61 | dumpProfile(profiler, samplingDurationMillis, uploadInterval); 62 | } 63 | } : 64 | () -> dumpProfile(profiler, samplingDurationMillis, uploadInterval); 65 | 66 | Duration initialDelay = getInitialDelay(); 67 | job = executor.scheduleAtFixedRate( 68 | task, 69 | initialDelay.toMillis(), 70 | config.uploadInterval.toMillis(), 71 | TimeUnit.MILLISECONDS 72 | ); 73 | } 74 | 75 | @Override 76 | public void stop() { 77 | throw new RuntimeException("not implemented"); 78 | } 79 | 80 | private void dumpProfile(final ProfilerDelegate profiler, final long samplingDurationMillis, final Duration uploadInterval) { 81 | Instant profilingStartTime = Instant.now(); 82 | try { 83 | profiler.start(); 84 | } catch (Throwable e) { 85 | logger.log(Logger.Level.ERROR, "Error starting profiler %s", e); 86 | stopProfiling(); 87 | return; 88 | } 89 | try { 90 | Thread.sleep(samplingDurationMillis); 91 | } catch (InterruptedException e) { 92 | Thread.currentThread().interrupt(); 93 | } 94 | profiler.stop(); 95 | 96 | Snapshot snapshot = profiler.dumpProfile(profilingStartTime, Instant.now()); 97 | exporter.export(snapshot); 98 | } 99 | 100 | private void stopProfiling() { 101 | if (job != null) { 102 | job.cancel(true); 103 | } 104 | executor.shutdown(); 105 | } 106 | 107 | private Duration getInitialDelay() { 108 | long uploadIntervalMillis = config.uploadInterval.toMillis(); 109 | float randomOffset = Random.Default.nextFloat(); 110 | uploadIntervalMillis = (long) ((float) uploadIntervalMillis * randomOffset); 111 | if (uploadIntervalMillis < 2000) { 112 | uploadIntervalMillis = 2000; 113 | } 114 | Duration firstProfilingDuration = Duration.ofMillis(uploadIntervalMillis); 115 | return firstProfilingDuration; 116 | } 117 | 118 | private Config isolate(final EventType type, final Config config) { 119 | final Builder b = new Builder(config); 120 | b.setProfilingEvent(type); 121 | if (!EventType.ALLOC.equals(type)) 122 | b.setProfilingAlloc(""); 123 | if (!EventType.LOCK.equals(type)) 124 | b.setProfilingLock(""); 125 | return b.build(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/util/zip/GzipSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.pyroscope.javaagent.util.zip; 17 | 18 | import java.io.IOException; 19 | import java.util.zip.CRC32; 20 | import java.util.zip.Deflater; 21 | 22 | import kotlin.jvm.internal.Intrinsics; 23 | import okhttp3.MediaType; 24 | import okhttp3.RequestBody; 25 | import okio.*; 26 | import org.jetbrains.annotations.NotNull; 27 | import org.jetbrains.annotations.Nullable; 28 | 29 | 30 | @SuppressWarnings("KotlinInternalInJava") 31 | public final class GzipSink implements Sink { 32 | private final BufferedSink sink; 33 | @NotNull 34 | private final Deflater deflater; 35 | private final DeflaterSink deflaterSink; 36 | private boolean closed; 37 | private final CRC32 crc; 38 | 39 | 40 | public GzipSink(@NotNull Sink sink, int compressionLevel) { 41 | Intrinsics.checkNotNullParameter(sink, "sink"); 42 | this.sink = Okio.buffer(sink); 43 | this.deflater = new Deflater(compressionLevel, true); 44 | this.deflaterSink = new DeflaterSink((BufferedSink) this.sink, this.deflater); 45 | this.crc = new CRC32(); 46 | 47 | // Write the Gzip header directly into the buffer for the sink to avoid handling IOException. 48 | Buffer buf = this.sink.getBuffer(); 49 | buf.writeShort(0x1f8b); // Two-byte Gzip ID. 50 | buf.writeByte(8); // 8 == Deflate compression method. 51 | buf.writeByte(0); // No flags. 52 | buf.writeInt(0); // No modification time. 53 | buf.writeByte(0); // No extra flags. 54 | buf.writeByte(0); // No OS. 55 | } 56 | 57 | @Override 58 | public void write(@NotNull Buffer source, long byteCount) throws IOException { 59 | // require(byteCount >= 0L) { "byteCount < 0: $byteCount" } 60 | if (!(byteCount >= 0L)) { 61 | throw new IllegalArgumentException("byteCount < 0: " + byteCount); 62 | } 63 | if (byteCount == 0L) return; 64 | updateCrc(source, byteCount); 65 | deflaterSink.write(source, byteCount); 66 | } 67 | 68 | 69 | @Override 70 | public void close() throws IOException { 71 | throw new IllegalStateException("should not be called. use end"); 72 | } 73 | 74 | // almost same as okio.GzipSink.close but does sink.flush instead of sink.close 75 | public void end() throws IOException { 76 | if (!this.closed) { 77 | Throwable thrown = null; 78 | 79 | try { 80 | this.deflaterSink.finishDeflate$okio(); 81 | this.writeFooter(); 82 | } catch (Throwable var3) { 83 | thrown = var3; 84 | } 85 | 86 | try { 87 | this.deflater.end(); 88 | } catch (Throwable var5) { 89 | if (thrown == null) { 90 | thrown = var5; 91 | } 92 | } 93 | 94 | try { 95 | this.sink.flush(); 96 | } catch (Throwable var4) { 97 | if (thrown == null) { 98 | thrown = var4; 99 | } 100 | } 101 | 102 | this.closed = true; 103 | if (thrown != null) { 104 | Util.sneakyRethrow(thrown); 105 | } 106 | } 107 | } 108 | 109 | @Override 110 | public void flush() throws IOException { 111 | deflaterSink.flush(); 112 | } 113 | 114 | @NotNull 115 | @Override 116 | public Timeout timeout() { 117 | return sink.timeout(); 118 | } 119 | 120 | private void updateCrc(Buffer buffer, long byteCount) { 121 | Segment head = buffer.head; 122 | Intrinsics.checkNotNull(head); 123 | long remaining = byteCount; 124 | while (remaining > 0) { 125 | int segmentLength = (int) Math.min(remaining, head.limit - head.pos); 126 | this.crc.update(head.data, head.pos, segmentLength); 127 | remaining -= segmentLength; 128 | head = head.next; 129 | Intrinsics.checkNotNull(head); 130 | } 131 | } 132 | 133 | private final void writeFooter() throws IOException { 134 | this.sink.writeIntLe((int) this.crc.getValue()); 135 | this.sink.writeIntLe((int) this.deflater.getBytesRead()); 136 | } 137 | 138 | 139 | /** 140 | * origin 141 | * Returns a gzip version of the RequestBody, with compressed payload. 142 | * This is not automatic as not all servers support gzip compressed requests. 143 | *

144 | * ``` 145 | * val request = Request.Builder().url("...") 146 | * .addHeader("Content-Encoding", "gzip") 147 | * .post(uncompressedBody.gzip()) 148 | * .build() 149 | * ``` 150 | */ 151 | public static RequestBody gzip(RequestBody req, int compressionLevel) { 152 | return new RequestBody() { 153 | @Nullable 154 | @Override 155 | public MediaType contentType() { 156 | return req.contentType(); 157 | } 158 | 159 | @Override 160 | public long contentLength() throws IOException { 161 | return -1;// We don't know the compressed length in advance! 162 | } 163 | 164 | @Override 165 | public void writeTo(@NotNull BufferedSink sink) throws IOException { 166 | GzipSink gzipSink = new GzipSink(sink, compressionLevel); 167 | BufferedSink buffer = Okio.buffer(gzipSink); 168 | req.writeTo(buffer); 169 | // do not close gzipSink & buffer to avoid closing upstream sink 170 | // do flushes instead 171 | buffer.flush(); 172 | gzipSink.end(); 173 | } 174 | 175 | @Override 176 | public boolean isOneShot() { 177 | return req.isOneShot(); 178 | } 179 | }; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /agent/src/main/java/io/pyroscope/javaagent/util/zip/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.pyroscope.javaagent.util.zip; 17 | 18 | public class Util { 19 | /** 20 | * origin 21 | * Throws {@code t}, even if the declared throws clause doesn't permit it. 22 | * This is a terrible – but terribly convenient – hack that makes it easy to 23 | * catch and rethrow exceptions after cleanup. See Java Puzzlers #43. 24 | */ 25 | public static void sneakyRethrow(Throwable t) { 26 | Util. sneakyThrow2(t); 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | private static void sneakyThrow2(Throwable t) throws T { 31 | throw (T) t; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /agent/src/main/resources/jfr/pyroscope.jfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | true 9 | 1 ms 10 | 11 | 12 | 13 | true 14 | true 15 | 10 ms 16 | 17 | 18 | 19 | true 20 | true 21 | 22 | 23 | 24 | true 25 | true 26 | 27 | 28 | 29 | true 30 | true 31 | 10 ms 32 | 33 | 34 | -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/OverfillQueueTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | public class OverfillQueueTest { 8 | @Test 9 | void withoutOverfill1() throws InterruptedException { 10 | final OverfillQueue queue = new OverfillQueue<>(5); 11 | queue.put(0); 12 | queue.put(1); 13 | queue.put(2); 14 | queue.put(3); 15 | queue.put(4); 16 | 17 | assertEquals(0, queue.take()); 18 | assertEquals(1, queue.take()); 19 | assertEquals(2, queue.take()); 20 | assertEquals(3, queue.take()); 21 | assertEquals(4, queue.take()); 22 | } 23 | 24 | @Test 25 | void withoutOverfill2() throws InterruptedException { 26 | final OverfillQueue queue = new OverfillQueue<>(5); 27 | queue.put(0); 28 | queue.put(1); 29 | queue.put(2); 30 | queue.put(3); 31 | queue.put(4); 32 | 33 | queue.take(); 34 | queue.take(); 35 | queue.take(); 36 | 37 | queue.put(5); 38 | queue.put(6); 39 | queue.put(7); 40 | 41 | assertEquals(3, queue.take()); 42 | assertEquals(4, queue.take()); 43 | assertEquals(5, queue.take()); 44 | assertEquals(6, queue.take()); 45 | assertEquals(7, queue.take()); 46 | } 47 | 48 | @Test 49 | void withOverfill() throws InterruptedException { 50 | final OverfillQueue queue = new OverfillQueue<>(5); 51 | queue.put(0); 52 | queue.put(1); 53 | queue.put(2); 54 | queue.put(3); 55 | queue.put(4); 56 | queue.put(5); 57 | queue.put(6); 58 | queue.put(7); 59 | queue.put(8); 60 | queue.put(9); 61 | 62 | assertEquals(5, queue.take()); 63 | assertEquals(6, queue.take()); 64 | assertEquals(7, queue.take()); 65 | assertEquals(8, queue.take()); 66 | assertEquals(9, queue.take()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/PyroscopeAgentTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.javaagent.api.Logger; 4 | import io.pyroscope.javaagent.api.ProfilingScheduler; 5 | import io.pyroscope.javaagent.config.Config; 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.Mockito.*; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class PyroscopeAgentTest { 18 | 19 | private Config configAgentEnabled; 20 | private Config configAgentDisabled; 21 | private PyroscopeAgent.Options optionsAgentEnabled; 22 | private PyroscopeAgent.Options optionsAgentDisabled; 23 | 24 | @Mock 25 | private Logger logger; 26 | 27 | @Mock 28 | private ProfilingScheduler profilingScheduler; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | configAgentEnabled = new Config.Builder() 33 | .setAgentEnabled(true) 34 | .build(); 35 | optionsAgentEnabled = new PyroscopeAgent.Options.Builder(configAgentEnabled) 36 | .setScheduler(profilingScheduler) 37 | .setLogger(logger) 38 | .build(); 39 | 40 | configAgentDisabled = new Config.Builder() 41 | .setAgentEnabled(false) 42 | .build(); 43 | optionsAgentDisabled = new PyroscopeAgent.Options.Builder(configAgentDisabled) 44 | .setScheduler(profilingScheduler) 45 | .setLogger(logger) 46 | .build(); 47 | } 48 | 49 | @AfterEach 50 | void tearDown() { 51 | PyroscopeAgent.stop(); 52 | } 53 | 54 | @Test 55 | void startupTestWithEnabledAgent() { 56 | PyroscopeAgent.start(optionsAgentEnabled); 57 | 58 | verify(profilingScheduler, times(1)).start(any()); 59 | } 60 | 61 | @Test 62 | void startupTestWithDisabledAgent() { 63 | PyroscopeAgent.start(optionsAgentDisabled); 64 | 65 | verify(profilingScheduler, never()).start(any()); 66 | } 67 | } -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/StartStopTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.javaagent.api.Logger; 5 | import io.pyroscope.javaagent.config.Config; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | public class StartStopTest { 12 | 13 | public static final Config INVALID = new Config.Builder() 14 | .setApplicationName("demo.app{qweqwe=asdasd}") 15 | .setFormat(Format.JFR) 16 | .setProfilingAlloc("512k") 17 | .setAPExtraArguments("event=qwe") // java.lang.IllegalArgumentException: Duplicate event argument 18 | .setProfilingEvent(EventType.ITIMER) 19 | .setLogLevel(Logger.Level.DEBUG) 20 | .build(); 21 | 22 | public static final Config VALID = new Config.Builder() 23 | .setApplicationName("demo.app{qweqwe=asdasd}") 24 | .setFormat(Format.JFR) 25 | .setProfilingEvent(EventType.ITIMER) 26 | .setLogLevel(Logger.Level.DEBUG) 27 | .build(); 28 | 29 | 30 | @Test 31 | void testStartFail() { 32 | assertFalse(PyroscopeAgent.isStarted()); 33 | 34 | PyroscopeAgent.start(INVALID); 35 | assertFalse(PyroscopeAgent.isStarted()); 36 | 37 | PyroscopeAgent.start(INVALID); 38 | assertFalse(PyroscopeAgent.isStarted()); 39 | 40 | PyroscopeAgent.stop(); 41 | assertFalse(PyroscopeAgent.isStarted()); 42 | PyroscopeAgent.stop(); 43 | assertFalse(PyroscopeAgent.isStarted()); 44 | 45 | PyroscopeAgent.start(VALID); 46 | assertTrue(PyroscopeAgent.isStarted()); 47 | 48 | PyroscopeAgent.stop(); 49 | assertFalse(PyroscopeAgent.isStarted()); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/config/AppNameTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.config; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static java.util.Collections.emptyMap; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | class AppNameTest { 12 | @Test 13 | void withoutLabels() { 14 | AppName app = AppName.parse("test.app"); 15 | assertEquals(app.name, "test.app"); 16 | assertEquals(app.labels, emptyMap()); 17 | } 18 | 19 | @Test 20 | void emptyLabels() { 21 | AppName app = AppName.parse("test.app{}"); 22 | assertEquals(app.name, "test.app"); 23 | assertEquals(app.labels, emptyMap()); 24 | } 25 | 26 | @Test 27 | void singleLabel() { 28 | AppName app = AppName.parse("test.app{foo=bar}"); 29 | assertEquals(app.name, "test.app"); 30 | assertEquals(app.labels, mapOf("foo", "bar")); 31 | } 32 | 33 | @Test 34 | void twoLabels() { 35 | AppName app = AppName.parse("test.app{foo=bar,fiz=baz}"); 36 | assertEquals(app.name, "test.app"); 37 | assertEquals(app.labels, mapOf("foo", "bar", "fiz", "baz")); 38 | assertEquals("test.app{fiz=baz,foo=bar}", app.toString()); 39 | } 40 | 41 | @Test 42 | void emptyKey() { 43 | AppName app = AppName.parse("test.app{=bar , fiz=baz}"); 44 | assertEquals(app.name, "test.app"); 45 | assertEquals(app.labels, mapOf("fiz", "baz")); 46 | } 47 | 48 | @Test 49 | void emptyValue() { 50 | AppName app = AppName.parse("test.app{foo= , fiz=baz}"); 51 | assertEquals(app.name, "test.app"); 52 | assertEquals(app.labels, mapOf("fiz", "baz")); 53 | } 54 | 55 | @Test 56 | void noEqSign() { 57 | AppName app = AppName.parse("test.app{foo= , fiz=baz}"); 58 | assertEquals(app.name, "test.app"); 59 | assertEquals(app.labels, mapOf("fiz", "baz")); 60 | } 61 | 62 | private static Map mapOf(String ...ss) { 63 | HashMap res = new HashMap<>(); 64 | for (int i = 0; i < ss.length; i+=2) { 65 | res.put(ss[i], ss[i + 1]); 66 | } 67 | return res; 68 | } 69 | } -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/config/IntervalParserTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.config; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.time.Duration; 6 | import java.time.temporal.ChronoUnit; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | public class IntervalParserTest { 12 | @Test 13 | void testNanos() { 14 | NumberFormatException numberFormatException = assertThrows( 15 | NumberFormatException.class, () -> IntervalParser.parse("-1")); 16 | assertEquals("Interval must be positive, but -1 given", numberFormatException.getMessage()); 17 | 18 | numberFormatException = assertThrows(NumberFormatException.class, () -> IntervalParser.parse("0")); 19 | assertEquals("Interval must be positive, but 0 given", numberFormatException.getMessage()); 20 | 21 | assertEquals(Duration.ofNanos(10), IntervalParser.parse("10")); 22 | } 23 | 24 | @Test 25 | void testMicros() { 26 | NumberFormatException numberFormatException = assertThrows( 27 | NumberFormatException.class, () -> IntervalParser.parse("-1us")); 28 | assertEquals("Interval must be positive, but -1us given", numberFormatException.getMessage()); 29 | 30 | numberFormatException = assertThrows(NumberFormatException.class, () -> IntervalParser.parse("0us")); 31 | assertEquals("Interval must be positive, but 0us given", numberFormatException.getMessage()); 32 | 33 | assertEquals(Duration.of(10, ChronoUnit.MICROS), IntervalParser.parse("10us")); 34 | } 35 | 36 | @Test 37 | void testMillis() { 38 | NumberFormatException numberFormatException = assertThrows( 39 | NumberFormatException.class, () -> IntervalParser.parse("-1ms")); 40 | assertEquals("Interval must be positive, but -1ms given", numberFormatException.getMessage()); 41 | 42 | numberFormatException = assertThrows(NumberFormatException.class, () -> IntervalParser.parse("0ms")); 43 | assertEquals("Interval must be positive, but 0ms given", numberFormatException.getMessage()); 44 | 45 | assertEquals(Duration.ofMillis(10), IntervalParser.parse("10ms")); 46 | } 47 | 48 | @Test 49 | void testSeconds() { 50 | NumberFormatException numberFormatException = assertThrows( 51 | NumberFormatException.class, () -> IntervalParser.parse("-1s")); 52 | assertEquals("Interval must be positive, but -1s given", numberFormatException.getMessage()); 53 | 54 | numberFormatException = assertThrows(NumberFormatException.class, () -> IntervalParser.parse("0s")); 55 | assertEquals("Interval must be positive, but 0s given", numberFormatException.getMessage()); 56 | 57 | assertEquals(Duration.ofSeconds(10), IntervalParser.parse("10s")); 58 | } 59 | 60 | @Test 61 | void testUnknownUnit() { 62 | NumberFormatException numberFormatException = assertThrows( 63 | NumberFormatException.class, () -> IntervalParser.parse("10k")); 64 | assertEquals("Cannot parse interval 10k", numberFormatException.getMessage()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /agent/src/test/java/io/pyroscope/javaagent/impl/ExponentialBackoffTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.javaagent.impl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.junit.jupiter.MockitoExtension; 6 | 7 | import java.util.Random; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.mockito.ArgumentMatchers.anyInt; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | import static org.mockito.Mockito.withSettings; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class ExponentialBackoffTest { 17 | @Test 18 | void test() { 19 | final Random random = mock(Random.class, withSettings().withoutAnnotations()); 20 | when(random.nextInt(anyInt())).then(invocation -> invocation.getArgument(0)); 21 | 22 | final ExponentialBackoff exponentialBackoff = new ExponentialBackoff(1_000, 30_000, random); 23 | assertEquals(1_000, exponentialBackoff.error()); 24 | assertEquals(2_000, exponentialBackoff.error()); 25 | assertEquals(4_000, exponentialBackoff.error()); 26 | assertEquals(8_000, exponentialBackoff.error()); 27 | assertEquals(16_000, exponentialBackoff.error()); 28 | assertEquals(30_000, exponentialBackoff.error()); 29 | assertEquals(30_000, exponentialBackoff.error()); 30 | exponentialBackoff.reset(); 31 | assertEquals(1_000, exponentialBackoff.error()); 32 | assertEquals(2_000, exponentialBackoff.error()); 33 | assertEquals(4_000, exponentialBackoff.error()); 34 | assertEquals(8_000, exponentialBackoff.error()); 35 | assertEquals(16_000, exponentialBackoff.error()); 36 | for (int i = 0; i < 100; i++) { 37 | assertEquals(30_000, exponentialBackoff.error()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /alpine-test.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE_VERSION 2 | ARG JAVA_VERSION 3 | 4 | FROM alpine:3.20.3 AS builder 5 | RUN apk add openjdk8 6 | 7 | WORKDIR /app 8 | ADD gradlew build.gradle settings.gradle gradle.properties /app/ 9 | ADD gradle gradle 10 | RUN ./gradlew --no-daemon --version 11 | ADD agent agent 12 | ADD async-profiler-context async-profiler-context 13 | ADD demo/build.gradle demo/ 14 | 15 | # for testing locally produced artifacts 16 | #COPY async-profiler-3.0.0.1-linux-x64.tar.gz . 17 | #COPY async-profiler-3.0.0.1-linux-arm64.tar.gz . 18 | #COPY async-profiler-3.0.0.1-macos.zip . 19 | 20 | RUN ./gradlew --no-daemon shadowJar 21 | 22 | FROM alpine:${IMAGE_VERSION} AS runner 23 | ARG IMAGE_VERSION 24 | ARG JAVA_VERSION 25 | RUN apk add openjdk${JAVA_VERSION} 26 | 27 | WORKDIR /app 28 | ADD demo demo 29 | COPY --from=builder /app/agent/build/libs/pyroscope.jar /app/agent/build/libs/pyroscope.jar 30 | 31 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/default-jvm/jre/bin 32 | 33 | RUN javac demo/src/main/java/Fib.java 34 | 35 | ENV PYROSCOPE_LOG_LEVEL=debug 36 | ENV PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 37 | ENV PYROSCOPE_APPLICATION_NAME=alpine-${IMAGE_VERSION}-${JAVA_VERSION} 38 | ENV PYROSCOPE_UPLOAD_INTERVAL=15s 39 | 40 | CMD ["java", "-javaagent:/app/agent/build/libs/pyroscope.jar", "-cp", "demo/src/main/java/", "Fib"] 41 | -------------------------------------------------------------------------------- /async-profiler-context/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/pyroscope-io/async-profiler 2 | - bundles shared async-profiler's shared libraries into fat jar 3 | - cherry-picks Context ID functionality 4 | -------------------------------------------------------------------------------- /async-profiler-context/build.gradle: -------------------------------------------------------------------------------- 1 | //import com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation 2 | 3 | import java.security.MessageDigest 4 | 5 | plugins { 6 | id 'java-library' 7 | id 'maven-publish' 8 | id 'signing' 9 | id "de.undercouch.download" version "4.1.1" 10 | id "com.gradleup.shadow" version '8.3.1' 11 | } 12 | 13 | sourceCompatibility = JavaVersion.VERSION_1_8 14 | targetCompatibility = JavaVersion.VERSION_1_8 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | def asyncProfilerVersion = project.properties['async_profiler_version'] 21 | def pyroscopeVersion = project.properties['pyroscope_version'] 22 | dependencies { 23 | api files("$buildDir/async-profiler/async-profiler.jar") 24 | compileOnly 'org.jetbrains:annotations:24.1.0' 25 | implementation 'com.google.protobuf:protobuf-java:4.29.2' 26 | testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.2' 27 | testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.2' 28 | testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.10.0' 29 | testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.10.0' 30 | } 31 | 32 | jar { 33 | from("$buildDir/async-profiler/native") { 34 | include "**.so" 35 | include "**.so.sha1" 36 | } 37 | } 38 | 39 | test { 40 | useJUnitPlatform() 41 | maxHeapSize = "1g" 42 | jvmArgs '-XX:MaxMetaspaceSize=512m' 43 | } 44 | 45 | java { 46 | withJavadocJar() 47 | withSourcesJar() 48 | } 49 | 50 | shadowJar { 51 | exclude 'Log4j-*' 52 | exclude 'META-INF/org/apache/logging/log4j/**' 53 | exclude 'META-INF/services/**' 54 | 55 | from("$buildDir/async-profiler/native") { 56 | include "**.so" 57 | include "**.so.sha1" 58 | } 59 | 60 | archiveFileName = "async-profiler-context.jar" 61 | 62 | minimize() 63 | archiveClassifier.set('') 64 | 65 | relocate("com.google", "io.pyroscope.vendor.com.google") 66 | relocate("one.profiler", "io.pyroscope.vendor.one.profiler") 67 | dependencies { 68 | exclude "org/jetbrains/**" 69 | exclude "org/intellij/**" 70 | exclude "google/protobuf/**" 71 | } 72 | } 73 | 74 | tasks.register('asyncProfilerLib') { 75 | def useLocalArtifacts = project.findProperty('useLocalAsyncProfilerArtifacts') == 'true' 76 | 77 | def suffixes = [ 78 | ['linux-arm64', 'tar.gz'], 79 | ['linux-x64', 'tar.gz'], 80 | ['macos', 'zip'] 81 | ] 82 | 83 | def asyncProfilerDir = file("$buildDir/async-profiler") 84 | outputs.file "$asyncProfilerDir/async-profiler.jar" 85 | suffixes.forEach { suffix -> 86 | outputs.file "$asyncProfilerDir/native/libasyncProfiler-${suffix}.so" 87 | } 88 | 89 | doLast { 90 | suffixes.forEach { suffix, ext -> 91 | if (!useLocalArtifacts) { 92 | def repo = "https://github.com/grafana/async-profiler" 93 | download { 94 | src "$repo/releases/download/v${asyncProfilerVersion}/async-profiler-${asyncProfilerVersion}-${suffix}.${ext}" 95 | dest new File(asyncProfilerDir, "async-profiler-${asyncProfilerVersion}-${suffix}.${ext}") 96 | overwrite true 97 | } 98 | } else { 99 | copy { 100 | from file("../async-profiler-${asyncProfilerVersion}-${suffix}.${ext}") 101 | into asyncProfilerDir 102 | } 103 | } 104 | 105 | // Extract the native library files. 106 | copy { 107 | if (ext == "zip") { 108 | from zipTree("$asyncProfilerDir/async-profiler-${asyncProfilerVersion}-${suffix}.${ext}") 109 | } else { 110 | from tarTree(resources.gzip("$asyncProfilerDir/async-profiler-${asyncProfilerVersion}-${suffix}.${ext}")) 111 | } 112 | include '**/libasyncProfiler.*' 113 | eachFile { 114 | it.relativePath = new RelativePath(true, "native/", "libasyncProfiler-${suffix}.so") 115 | } 116 | includeEmptyDirs false 117 | into asyncProfilerDir 118 | } 119 | 120 | // Write checksums for the library files. 121 | // They are used for the library deployment. 122 | def sha1 = MessageDigest.getInstance('SHA1') 123 | sha1.update(file("$asyncProfilerDir/native/libasyncProfiler-${suffix}.so").toPath().readBytes()) 124 | def hexString = new StringBuilder() 125 | for (b in sha1.digest()) { 126 | hexString.append(String.format('%01x', b & 0xFF)) 127 | } 128 | new File("$asyncProfilerDir/native/libasyncProfiler-${suffix}.so.sha1").text = hexString 129 | } 130 | 131 | // Extract the Java library. 132 | copy { 133 | from tarTree(resources.gzip("$asyncProfilerDir/async-profiler-${asyncProfilerVersion}-linux-x64.tar.gz")) 134 | include '**/async-profiler.jar' 135 | eachFile { 136 | it.relativePath = new RelativePath(true, it.relativePath.segments.drop(2)) 137 | } 138 | includeEmptyDirs false 139 | into asyncProfilerDir 140 | } 141 | } 142 | } 143 | 144 | clean.dependsOn 'cleanAsyncProfilerLib' 145 | compileJava.dependsOn 'asyncProfilerLib' 146 | 147 | publishing { 148 | publications { 149 | shadow(MavenPublication) { publication -> 150 | project.shadow.component(publication) 151 | groupId = 'io.pyroscope' 152 | artifactId = 'async-profiler-context' 153 | version = pyroscopeVersion 154 | artifacts = [ shadowJar, javadocJar, sourcesJar ] 155 | pom { 156 | name = 'Pyroscope Java agent' 157 | description = 'async-profiler fork with contextID functionality and bundled shared libs' 158 | url = 'https://pyroscope.io' 159 | licenses { 160 | license { 161 | name = 'The Apache License, Version 2.0' 162 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 163 | } 164 | } 165 | developers { 166 | developer { 167 | id = 'pyroscope' 168 | name = 'Pyroscope' 169 | email = 'anatoly@pyroscope.io' 170 | } 171 | } 172 | scm { 173 | connection = 'scm:git:git://github.com/pyroscope-io/pyroscope-java.git' 174 | developerConnection = 'scm:git:ssh://github.com/pyroscope-io/pyroscope-java.git' 175 | url = 'https://github.com/pyroscope-io/pyroscope-java' 176 | } 177 | } 178 | } 179 | } 180 | repositories { 181 | maven { 182 | credentials { 183 | username project.hasProperty('nexusUsername') ? project.nexusUsername : '' 184 | password project.hasProperty('nexusPassword') ? project.nexusPassword : '' 185 | } 186 | url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 187 | } 188 | } 189 | } 190 | 191 | signing { 192 | sign publishing.publications.shadow 193 | } 194 | 195 | afterEvaluate { 196 | if (project.tasks.findByName('signShadowPublication')) { 197 | project.tasks.named('signShadowPublication').configure { 198 | dependsOn 'jar' 199 | dependsOn 'sourcesJar' 200 | dependsOn 'javadocJar' 201 | } 202 | } 203 | } 204 | 205 | tasks.register('generate-proto-classes') { 206 | doLast { 207 | exec { 208 | commandLine 'protoc', 209 | '-I', '.', 210 | '--java_out', 'src/main/java', 211 | 'jfr_labels.proto' 212 | } 213 | } 214 | } 215 | 216 | generateMetadataFileForShadowPublication.dependsOn 'jar' 217 | -------------------------------------------------------------------------------- /async-profiler-context/jfr_labels.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package io.pyroscope.labels.pb; 4 | 5 | message Context { 6 | map labels = 1; 7 | } 8 | message LabelsSnapshot { 9 | map contexts = 1; 10 | map strings = 2; 11 | } -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/Preconditions.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public final class Preconditions { 7 | 8 | private Preconditions() { 9 | } 10 | 11 | public static T checkNotNull(@Nullable T reference, @NotNull String paramName) { 12 | if (reference == null) { 13 | throw new NullPointerException(paramName + " cannot be null"); 14 | } 15 | return reference; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/PyroscopeAsyncProfiler.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope; 2 | 3 | import one.profiler.AsyncProfiler; 4 | 5 | import java.io.FileNotFoundException; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.charset.StandardCharsets; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.nio.file.StandardCopyOption; 13 | 14 | public class PyroscopeAsyncProfiler { 15 | static final String libraryPath; 16 | 17 | static { 18 | try { 19 | libraryPath = deployLibrary(); 20 | } catch (final IOException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | 25 | public static AsyncProfiler getAsyncProfiler() { 26 | return AsyncProfiler.getInstance(libraryPath); 27 | } 28 | 29 | /** 30 | * Extracts the profiler library file from the JAR and puts it in the temp directory. 31 | * 32 | * @return path to the extracted library 33 | */ 34 | private static String deployLibrary() throws IOException { 35 | final String fileName = libraryFileName(); 36 | final String userName = System.getProperty("user.name"); 37 | final Path targetDir = Files.createTempDirectory(userName + "-pyroscope"); 38 | 39 | try (final InputStream is = loadResource(fileName)) { 40 | final Path target = targetDir.resolve(targetLibraryFileName(fileName)).toAbsolutePath(); 41 | Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING); 42 | return target.toString(); 43 | } 44 | } 45 | 46 | /** 47 | * load resource either from jar resources for production or from local file system for testing 48 | * 49 | * @param fileName 50 | * @return 51 | * @throws FileNotFoundException 52 | */ 53 | private static InputStream loadResource(String fileName) throws IOException { 54 | InputStream res = PyroscopeAsyncProfiler.class.getResourceAsStream("/" + fileName); 55 | if (res != null) { 56 | return res; // from shadowJar 57 | } 58 | Path filePath = Paths.get("build", "async-profiler", "native", fileName); 59 | return Files.newInputStream(filePath); 60 | } 61 | 62 | /** 63 | * Creates the library file name based on the current OS and architecture name. 64 | */ 65 | private static String libraryFileName() { 66 | String arch; 67 | final String osProperty = System.getProperty("os.name"); 68 | final String archProperty = System.getProperty("os.arch"); 69 | switch (osProperty) { 70 | case "Linux": 71 | switch (archProperty) { 72 | case "amd64": 73 | arch = "x64"; 74 | break; 75 | case "aarch64": 76 | arch = "arm64"; 77 | break; 78 | 79 | default: 80 | throw new RuntimeException("Unsupported architecture " + archProperty); 81 | } 82 | 83 | return "libasyncProfiler-linux-" + arch + ".so"; 84 | 85 | case "Mac OS X": 86 | switch (archProperty) { 87 | case "x86_64": 88 | case "aarch64": 89 | return "libasyncProfiler-macos.so"; 90 | default: 91 | throw new RuntimeException("Unsupported architecture " + archProperty); 92 | } 93 | 94 | default: 95 | throw new RuntimeException("Unsupported OS " + osProperty); 96 | } 97 | } 98 | 99 | /** 100 | *

Adds the checksum to the library file name.

101 | * 102 | *

E.g. {@code libasyncProfiler-linux-x64.so} -> 103 | * {@code libasyncProfiler-linux-x64-7b43b7cc6c864dd729cc7dcdb6e3db8f5ee5b4a4.so}

104 | */ 105 | private static String targetLibraryFileName(final String libraryFileName) throws IOException { 106 | if (!libraryFileName.endsWith(".so")) { 107 | throw new IllegalArgumentException("Incorrect library file name: " + libraryFileName); 108 | } 109 | 110 | final String checksumFileName = libraryFileName + ".sha1"; 111 | String checksum; 112 | try (final InputStream is = loadResource(checksumFileName)) { 113 | byte[] buf = new byte[40]; 114 | int bufLen = is.read(buf); 115 | if (bufLen <= 0) throw new IOException("checksum read fail"); 116 | checksum = new String(buf, 0, bufLen, StandardCharsets.UTF_8); 117 | } 118 | 119 | return libraryFileName.substring(0, libraryFileName.length() - 3) + "-" + checksum + ".so"; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/labels/v2/ConstantContext.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.labels.v2; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import static io.pyroscope.Preconditions.checkNotNull; 6 | 7 | /** 8 | * ConstantContext provides a way to define a set of labels that will be permanently stored 9 | * in memory for the life of the process. 10 | * 11 | *

IMPORTANT: This class keeps references to {@link LabelsSet} instances 12 | * indefinitely (they are never garbage collected). Only use ConstantContext for labels that: 13 | *

    14 | *
  • Have a finite, predetermined set of possible values (low cardinality)
  • 15 | *
  • Are constant throughout the application lifetime
  • 16 | *
  • Do NOT contain user-controlled values such as user IDs, session IDs, or span IDs
  • 17 | *
  • Are reused frequently across different parts of your application
  • 18 | *
19 | * 20 | *

For high-cardinality or ephemeral labels, use {@link ScopedContext} instead, which properly 21 | * cleans up references after context is closed and labels are dumped. 22 | * 23 | *

All parameters must be non-null. Attempting to create a ConstantContext with null parameters 24 | * will result in a NullPointerException. 25 | * 26 | *

Example usage: 27 | *

{@code
28 |  * private static final ConstantContext ctx = ConstantContext.of(new LabelsSet(
29 |  *     "path", "/foo/bar",
30 |  *     "method", "GET",
31 |  *     "service", "svc-1"
32 |  * ));
33 |  *
34 |  * // Later in your code:
35 |  * try {
36 |  *     ctx.activate();
37 |  *     // Do work with this context active
38 |  * } finally {
39 |  *     ctx.deactivate();
40 |  * }
41 |  * }
42 | */ 43 | public class ConstantContext { 44 | 45 | /** 46 | * Creates a new ConstantContext with the given labels. 47 | * 48 | *

Warning: The provided LabelsSet will be stored permanently in memory. 49 | * Only use this for low-cardinality, constant label sets. 50 | * 51 | * @param labels The labels to associate with this context 52 | * @return A new ConstantContext instance 53 | * @throws NullPointerException if labels is null 54 | */ 55 | public static @NotNull ConstantContext of(@NotNull LabelsSet labels) { 56 | checkNotNull(labels, "Labels"); 57 | long contextId = ScopedContext.CONTEXT_COUNTER.incrementAndGet(); 58 | ScopedContext.CONSTANT_CONTEXTS.put(contextId, labels); 59 | return new ConstantContext(contextId); 60 | } 61 | 62 | private final long contextId; 63 | 64 | private ConstantContext(long contextId) { 65 | this.contextId = contextId; 66 | } 67 | 68 | /** 69 | * Activates this context for the current thread. 70 | * This sets the async-profiler's context ID to this context's ID, 71 | * which will associate profiled samples with these labels. 72 | */ 73 | public void activate() { 74 | ScopedContext.getAsyncProfiler().setContextId(contextId); 75 | } 76 | 77 | /** 78 | * Deactivates this context for the current thread. 79 | * This resets the async-profiler's context ID to 0 (no context). 80 | */ 81 | public void deactivate() { 82 | ScopedContext.getAsyncProfiler().setContextId(0); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/labels/v2/LabelsSet.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.labels.v2; 2 | 3 | import java.util.Map; 4 | import java.util.function.BiConsumer; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import static io.pyroscope.Preconditions.checkNotNull; 9 | 10 | /** 11 | * LabelsSet represents an immutable set of key-value pairs used for profiling labels. 12 | * 13 | *

Labels are used by Pyroscope to categorize and filter profiling data, allowing 14 | * for more detailed analysis of application performance across different contexts. 15 | * 16 | *

This class stores labels as a flattened array of alternating keys and values, 17 | * making it memory-efficient while still providing easy iteration over the contained 18 | * label pairs. 19 | * 20 | *

LabelsSet instances are immutable once created and should be passed to 21 | * {@link ScopedContext} or {@link ConstantContext} to associate profiling data 22 | * with these labels. 23 | * 24 | *

All label keys and values must be non-null. Attempting to create a LabelsSet 25 | * with null keys or values will result in a NullPointerException. 26 | * 27 | *

Example usage: 28 | *

{@code
 29 |  * // Creating a label set with explicit key-value pairs
 30 |  * LabelsSet labels = new LabelsSet(
 31 |  *     "service", "user-api",
 32 |  *     "method", "GET",
 33 |  *     "endpoint", "/users"
 34 |  * );
 35 |  *
 36 |  * // Creating a label set from a Map
 37 |  * Map labelMap = new HashMap<>();
 38 |  * labelMap.put("transaction", "payment");
 39 |  * labelMap.put("customer_type", "premium");
 40 |  * LabelsSet labels = new LabelsSet(labelMap);
 41 |  * }
42 | */ 43 | public final class LabelsSet { 44 | private final String[] args; 45 | 46 | /** 47 | * Creates a LabelsSet from alternating key-value pairs. 48 | * 49 | *

The arguments must be provided as alternating key-value pairs, 50 | * where even-indexed arguments (0, 2, 4, ...) are keys and 51 | * odd-indexed arguments (1, 3, 5, ...) are their corresponding values. 52 | * 53 | * @param args An array of alternating key-value strings 54 | * @throws IllegalArgumentException if the number of arguments is not even 55 | * @throws NullPointerException if any key or value is null 56 | */ 57 | public LabelsSet(@NotNull String... args) { 58 | if (args.length % 2 != 0) { 59 | throw new IllegalArgumentException("args.length % 2 != 0: " + 60 | "api.LabelsSet's constructor arguments should be key-value pairs"); 61 | } 62 | 63 | for (int i = 0; i < args.length; i++) { 64 | checkNotNull(args[i], "Label"); 65 | } 66 | 67 | this.args = new String[args.length]; 68 | System.arraycopy(args, 0, this.args, 0, args.length); 69 | } 70 | 71 | /** 72 | * Creates a LabelsSet from a Map of labels. 73 | * 74 | *

This constructor converts a Map representation of labels into the 75 | * internal array representation used by LabelsSet. 76 | * 77 | * @param args A map containing key-value pairs for labels 78 | * @throws NullPointerException if args is null or any key or value in the map is null 79 | */ 80 | public LabelsSet(@NotNull Map<@NotNull String, @NotNull String> args) { 81 | checkNotNull(args, "Labels"); 82 | this.args = new String[args.size() * 2]; 83 | int i = 0; 84 | for (Map.Entry it : args.entrySet()) { 85 | this.args[i] = checkNotNull(it.getKey(), "Key"); 86 | this.args[i + 1] = checkNotNull(it.getValue(), "Value"); 87 | i += 2; 88 | } 89 | } 90 | 91 | /** 92 | * Applies a BiConsumer function to each key-value pair in this label set. 93 | * 94 | *

This method provides a way to iterate through all labels without 95 | * exposing the internal representation. 96 | * 97 | * @param labelConsumer A function that accepts a key (String) and value (String) 98 | * @throws NullPointerException if labelConsumer is null 99 | */ 100 | public void forEachLabel(@NotNull BiConsumer<@NotNull String, @NotNull String> labelConsumer) { 101 | checkNotNull(labelConsumer, "Consumer"); 102 | for (int i = 0; i < args.length; i += 2) { 103 | labelConsumer.accept(args[i], args[i + 1]); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/labels/v2/Pyroscope.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.labels.v2; 2 | 3 | 4 | import io.pyroscope.labels.pb.*; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.*; 8 | import java.util.concurrent.Callable; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.function.BiConsumer; 11 | 12 | import static io.pyroscope.Preconditions.checkNotNull; 13 | 14 | public final class Pyroscope { 15 | /** 16 | * LabelsWrapper accumulates dynamic labels and corelates them with async-profiler's contextId 17 | * You are expected to call {@link LabelsWrapper#dump()} periodically, io.pyroscope.javaagent.Profiler 18 | * does that. If you don't use io.pyroscope.javaagent.Profiler, 19 | * you need to call {@link LabelsWrapper#dump()} yourself. 20 | */ 21 | public static class LabelsWrapper { 22 | 23 | public static T run(@NotNull LabelsSet labels, @NotNull Callable c) throws Exception { 24 | try (ScopedContext s = new ScopedContext(checkNotNull(labels, "Labels"))) { 25 | return checkNotNull(c, "Callable").call(); 26 | } 27 | } 28 | 29 | public static void run(@NotNull LabelsSet labels, @NotNull Runnable c) { 30 | try (ScopedContext s = new ScopedContext(checkNotNull(labels, "Labels"))) { 31 | checkNotNull(c, "Runnable").run(); 32 | } 33 | } 34 | 35 | /** 36 | * Emergency method to clear all ScopedContext references when memory leaks are detected. 37 | * 38 | *

WARNING: This method should NOT be used in normal operation. It is 39 | * provided as a last resort for situations where unclosed {@link ScopedContext} instances 40 | * are causing memory leaks and cannot be fixed by proper closing in the application code. 41 | * 42 | *

Calling this method will: 43 | *

    44 | *
  • Remove all references to active and unclosed ScopedContext instances
  • 45 | *
  • Result in missing labels/labelsets in profiling data
  • 46 | *
  • Potentially create inconsistent profiling results
  • 47 | *
48 | * 49 | *

Recommended usage pattern: 50 | *

    51 | *
  • Fix your code to properly close all ScopedContext instances (preferred solution)
  • 52 | *
  • If that's not possible in the short term, call this method periodically (e.g., once every N minutes) 53 | * as a temporary workaround
  • 54 | *
55 | * 56 | *

Note: This does not affect {@link ConstantContext} instances, which are designed to live 57 | * for the entire application lifetime. 58 | */ 59 | public static void clear() { 60 | ScopedContext.CONTEXTS.clear(); 61 | } 62 | 63 | public static JfrLabels.LabelsSnapshot dump() { 64 | final JfrLabels.LabelsSnapshot.Builder sb = JfrLabels.LabelsSnapshot.newBuilder(); 65 | final StringTableBuilder stb = new StringTableBuilder(); 66 | stb.indexes.putAll(CONSTANTS); 67 | final Set closedContexts = new HashSet<>(); 68 | final BiConsumer collect = (contextID, ls) -> { 69 | final JfrLabels.Context.Builder cb = JfrLabels.Context.newBuilder(); 70 | ls.forEachLabel((k, v) -> { 71 | cb.putLabels(stb.get(k), stb.get(v)); 72 | }); 73 | sb.putContexts(contextID, cb.build()); 74 | }; 75 | for (Map.Entry it : ScopedContext.CONTEXTS.entrySet()) { 76 | final Long contextID = it.getKey(); 77 | if (it.getValue().closed.get()) { 78 | closedContexts.add(contextID); 79 | } 80 | collect.accept(contextID, it.getValue().labels); 81 | } 82 | for (Map.Entry it : ScopedContext.CONSTANT_CONTEXTS.entrySet()) { 83 | final Long contextID = it.getKey(); 84 | collect.accept(contextID, it.getValue()); 85 | } 86 | stb.indexes.forEach((k, v) -> { 87 | sb.putStrings(v, k); 88 | }); 89 | for (Long cid : closedContexts) { 90 | ScopedContext.CONTEXTS.remove(cid); 91 | } 92 | return sb.build(); 93 | } 94 | 95 | static final ConcurrentHashMap CONSTANTS = new ConcurrentHashMap<>(); 96 | 97 | public static long registerConstant(@NotNull String constant) { 98 | checkNotNull(constant, "constant"); 99 | Long v = CONSTANTS.get(constant); 100 | if (v != null) { 101 | return v; 102 | } 103 | synchronized (CONSTANTS) { 104 | v = CONSTANTS.get(constant); 105 | if (v != null) { 106 | return v; 107 | } 108 | long id = CONSTANTS.size() + 1; 109 | CONSTANTS.put(constant, id); 110 | return id; 111 | } 112 | } 113 | } 114 | 115 | private static Map staticLabels = Collections.emptyMap(); 116 | 117 | /** 118 | * Sets the static labels to be included with all profiling data. 119 | * 120 | *

Static labels are constant across the entire application lifetime and are used 121 | * to identify and categorize profiling data at a global level. 122 | * 123 | *

All label keys and values must be non-null. Attempting to set static labels 124 | * with null keys or values will result in a NullPointerException. 125 | * 126 | * @param labels A map containing key-value pairs for static labels 127 | * @throws NullPointerException if labels is null or any key or value in the map is null 128 | */ 129 | public static void setStaticLabels(@NotNull Map<@NotNull String, @NotNull String> labels) { 130 | checkNotNull(labels, "Labels"); 131 | 132 | for (Map.Entry entry : labels.entrySet()) { 133 | checkNotNull(entry.getKey(), "Key"); 134 | checkNotNull(entry.getValue(), "Value"); 135 | } 136 | 137 | staticLabels = Collections.unmodifiableMap(new HashMap<>(labels)); 138 | } 139 | 140 | public static Map getStaticLabels() { 141 | return staticLabels; 142 | } 143 | 144 | static class StringTableBuilder { 145 | private final Map indexes = new HashMap<>(); 146 | 147 | public StringTableBuilder() { 148 | } 149 | 150 | public long get(@NotNull String s) { 151 | Long prev = indexes.get(s); 152 | if (prev != null) { 153 | return prev; 154 | } 155 | long index = indexes.size() + 1; 156 | indexes.put(s, index); 157 | return index; 158 | 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/labels/v2/ScopedContext.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope.labels.v2; 2 | 3 | import io.pyroscope.PyroscopeAsyncProfiler; 4 | import one.profiler.AsyncProfiler; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | import java.util.function.BiConsumer; 11 | 12 | import static io.pyroscope.Preconditions.checkNotNull; 13 | 14 | /** 15 | * ScopedContext associates profiling data with a set of labels for the current execution scope. 16 | * Unlike {@link ConstantContext}, a ScopedContext is designed for dynamic, temporary use and 17 | * will be properly garbage collected. 18 | * 19 | *

ScopedContext implements {@link AutoCloseable}, allowing it to be used in try-with-resources 20 | * blocks for automatic cleanup when the context goes out of scope. 21 | * 22 | *

When a ScopedContext is closed, it's marked as such but remains in memory until the next call 23 | * to {@link io.pyroscope.labels.v2.Pyroscope.LabelsWrapper#dump()}. After being included in a dump, 24 | * closed contexts are removed from the internal map, allowing them to be garbage collected. 25 | * 26 | *

All constructor parameters (labels, previous context) must be non-null. Attempting to create 27 | * a ScopedContext with null parameters will result in a NullPointerException. 28 | * 29 | *

Use ScopedContext for: 30 | *

    31 | *
  • High-cardinality labels (like request IDs, user IDs, etc.)
  • 32 | *
  • Temporary, request-scoped labels that change frequently
  • 33 | *
  • User-controlled or dynamically generated values
  • 34 | *
  • Any labels that would result in memory leaks if kept indefinitely
  • 35 | *
36 | * 37 | *

Example usage: 38 | *

{@code
 39 |  * // For a single block of code with labels
 40 |  * try (ScopedContext ctx = new ScopedContext(new LabelsSet("request_id", requestId))) {
 41 |  *     // Do work that will be profiled with this context
 42 |  *     processRequest();
 43 |  * } // Context automatically closed here
 44 |  *
 45 |  * // Or using the helper method in Pyroscope.LabelsWrapper:
 46 |  * Pyroscope.LabelsWrapper.run(new LabelsSet("span_id", spanId), () -> {
 47 |  *     // Operations to perform with this context
 48 |  *     executeSpan();
 49 |  * });
 50 |  * }
51 | */ 52 | public final class ScopedContext implements AutoCloseable { 53 | public static final AtomicBoolean ENABLED = new AtomicBoolean(false); 54 | static final AtomicLong CONTEXT_COUNTER = new AtomicLong(0); 55 | static final ConcurrentHashMap CONTEXTS = new ConcurrentHashMap<>(); 56 | static final ConcurrentHashMap CONSTANT_CONTEXTS = new ConcurrentHashMap<>(); 57 | 58 | private static volatile AsyncProfiler asyncProfiler; 59 | 60 | static AsyncProfiler getAsyncProfiler() { 61 | if (asyncProfiler != null) { 62 | return asyncProfiler; 63 | } 64 | asyncProfiler = PyroscopeAsyncProfiler.getAsyncProfiler(); 65 | return asyncProfiler; 66 | } 67 | 68 | final LabelsSet labels; 69 | final long contextId; 70 | final long prevContextId; 71 | final AtomicBoolean closed = new AtomicBoolean(false); 72 | 73 | /** 74 | * Creates a new ScopedContext with the given labels. 75 | * The previous context ID is set to 0 (root context). 76 | * 77 | * @param labels The labels to associate with this context 78 | * @throws NullPointerException if labels is null 79 | */ 80 | public ScopedContext(@NotNull LabelsSet labels) { 81 | this( 82 | checkNotNull(labels, "Labels"), 83 | 0 84 | ); 85 | } 86 | 87 | /** 88 | * Creates a new ScopedContext with the given labels, using the previous context's ID. 89 | * This allows for proper nesting of contexts. 90 | * 91 | * @param labels The labels to associate with this context 92 | * @param prev The previous context that this context will replace temporarily 93 | * @throws NullPointerException if labels or prev is null 94 | */ 95 | public ScopedContext(@NotNull LabelsSet labels, @NotNull ScopedContext prev) { 96 | this( 97 | checkNotNull(labels, "Labels"), 98 | checkNotNull(prev, "Context").contextId 99 | ); 100 | } 101 | 102 | /** 103 | * Internal constructor to create a ScopedContext with specific previous context ID. 104 | * 105 | * @param labels The labels to associate with this context 106 | * @param prevContextId The context ID to restore when this context is closed 107 | * @throws NullPointerException if labels is null 108 | */ 109 | ScopedContext(@NotNull LabelsSet labels, long prevContextId) { 110 | this.labels = checkNotNull(labels, "Labels"); 111 | if (ENABLED.get()) { 112 | this.contextId = CONTEXT_COUNTER.incrementAndGet(); 113 | this.prevContextId = prevContextId; 114 | CONTEXTS.put(contextId, this); 115 | getAsyncProfiler().setContextId(contextId); 116 | } else { 117 | this.contextId = 0; 118 | this.prevContextId = 0; 119 | } 120 | } 121 | 122 | /** 123 | * Closes this context, restoring the previous context. 124 | * 125 | *

The context is marked as closed, but will remain in memory until the next call to 126 | * {@link io.pyroscope.labels.v2.Pyroscope.LabelsWrapper#dump()} which will clean up 127 | * closed contexts. 128 | */ 129 | @Override 130 | public void close() { 131 | if (!closed.compareAndSet(false, true)) { 132 | return; 133 | } 134 | if (ENABLED.get()) { 135 | getAsyncProfiler().setContextId(this.prevContextId); 136 | } 137 | } 138 | 139 | /** 140 | * Applies a consumer function to each label in this context. 141 | * 142 | * @param labelConsumer A function that will be called with each key-value pair in the labels 143 | * @throws NullPointerException if labelConsumer is null 144 | */ 145 | public void forEachLabel(@NotNull BiConsumer<@NotNull String, @NotNull String> labelConsumer) { 146 | labels.forEachLabel( 147 | checkNotNull(labelConsumer, "Consumer") 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /async-profiler-context/src/main/java/io/pyroscope/labels/v2/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Package {@code io.pyroscope.labels.v2} provides a new implementation of Pyroscope's context labeling system. 3 | * 4 | *

Why v2?

5 | * 6 | *

The previous implementation (v1) used an implicit ThreadLocal reference-counted approach to manage context labels. 7 | * While convenient for simple cases, this approach had several fundamental issues: 8 | * 9 | *

    10 | *
  • Cross-thread operations could lead to assertion errors, missing labels, or infinite loops
  • 11 | *
  • Closing a ScopedContext on a different thread than where it was created caused unpredictable behavior
  • 12 | *
  • Implicit label merging made debugging difficult when labels were unexpectedly combined
  • 13 | *
  • Thread pools could inherit unexpected labels from previous operations
  • 14 | *
15 | * 16 | *

Key Differences in v2

17 | * 18 | *

The v2 implementation: 19 | *

    20 | *
  • Eliminates implicit label merging - you must explicitly create contexts with all required labels
  • 21 | *
  • Requires explicit passing of parent contexts for proper nesting
  • 22 | *
  • Manages memory better by cleaning up closed contexts after they're dumped
  • 23 | *
  • Adds {@link io.pyroscope.labels.v2.ConstantContext} for static, low-cardinality labels
  • 24 | *
25 | * 26 | *

Example: v1 vs v2 Implementation

27 | * 28 | *

In v1, implicit merging happened at the ThreadLocal level: 29 | * 30 | *

{@code
31 |  * // v1: Implicit merging through ThreadLocal
32 |  * try (ScopedContext ctx = new ScopedContext(new LabelsSet("request_id", "239"))) {
33 |  *     try (ScopedContext ctx2 = new ScopedContext(new LabelsSet("op", "doSomething"))) {
34 |  *         doSomething(); // Runs with BOTH "request_id" and "op" labels
35 |  *     }
36 |  * }
37 |  * }
38 | * 39 | *

In v2, you must explicitly include all labels or pass the parent context: 40 | * 41 | *

{@code
42 |  * // v2: Explicit passing of parent context
43 |  * try (ScopedContext ctx1 = new ScopedContext(new LabelsSet("request_id", "239"))) {
44 |  *     // Option 1: Create new context with ALL needed labels
45 |  *     try (ScopedContext ctx2 = new ScopedContext(new LabelsSet("request_id", "239", "op", "doSomething"))) {
46 |  *         doSomething();
47 |  *     }
48 |  *
49 |  *     // Option 2: Pass parent context to create proper hierarchy
50 |  *     try (ScopedContext ctx2 = new ScopedContext(new LabelsSet("op", "doSomething"), ctx1)) {
51 |  *         doSomething();
52 |  *     }
53 |  * }
54 |  * }
55 | */ 56 | package io.pyroscope.labels.v2; 57 | -------------------------------------------------------------------------------- /async-profiler-context/src/test/java/io/pyroscope/ConcurrentUsageTest.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class ConcurrentUsageTest { 11 | 12 | @Test 13 | public void runConcurrently() throws IOException { 14 | int iterations = 10; 15 | List processes = new ArrayList<>(); 16 | for (int i = 0; i < iterations; i++) { 17 | Process process = new ProcessBuilder( 18 | "java", "-cp", System.getProperty("java.class.path"), TestApplication.class.getName() 19 | ).inheritIO().start(); 20 | processes.add(process); 21 | } 22 | 23 | processes.parallelStream().forEach(p -> { 24 | int exitCode = 0; 25 | try { 26 | exitCode = p.waitFor(); 27 | } catch (InterruptedException e) { 28 | Assertions.fail("could not get process status", e); 29 | } 30 | if (exitCode != 0) { 31 | Assertions.fail("process failed with exit code: " + exitCode); 32 | } 33 | }); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /async-profiler-context/src/test/java/io/pyroscope/TestApplication.java: -------------------------------------------------------------------------------- 1 | package io.pyroscope; 2 | 3 | import one.profiler.AsyncProfiler; 4 | import one.profiler.Counter; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class TestApplication { 9 | 10 | public static void main(String[] args) { 11 | AsyncProfiler asyncProfiler = PyroscopeAsyncProfiler.getAsyncProfiler(); 12 | asyncProfiler.start("cpu", TimeUnit.SECONDS.toNanos(1)); 13 | try { 14 | Thread.sleep(1000); 15 | } catch (InterruptedException e) { 16 | System.exit(1); 17 | } 18 | asyncProfiler.stop(); 19 | 20 | System.out.println( 21 | asyncProfiler + "-" + 22 | asyncProfiler.dumpCollapsed(Counter.SAMPLES).split(";").length 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' 3 | } 4 | 5 | group = "io.pyroscope" 6 | version = project.properties['pyroscope_version'] 7 | 8 | nexusPublishing { 9 | repositories { 10 | sonatype { //only for users registered in Sonatype after 24 Feb 2021 11 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 12 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '2.1.20' 3 | } 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | 6 | sourceCompatibility = JavaVersion.VERSION_1_8 7 | targetCompatibility = JavaVersion.VERSION_1_8 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | dependencies { 13 | implementation(project(":agent")) 14 | } 15 | kotlin{ 16 | compilerOptions{ 17 | jvmTarget.set(JvmTarget.JVM_1_8) 18 | } 19 | } -------------------------------------------------------------------------------- /demo/src/main/java/App.java: -------------------------------------------------------------------------------- 1 | import io.pyroscope.http.Format; 2 | import io.pyroscope.javaagent.EventType; 3 | import io.pyroscope.javaagent.PyroscopeAgent; 4 | import io.pyroscope.javaagent.api.Logger; 5 | import io.pyroscope.javaagent.config.Config; 6 | import io.pyroscope.javaagent.config.ProfilerType; 7 | import io.pyroscope.labels.v2.LabelsSet; 8 | import io.pyroscope.labels.v2.Pyroscope; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | 15 | public class App { 16 | public static final int N_THREADS = 8; 17 | 18 | public static void main(String[] args) { 19 | PyroscopeAgent.start( 20 | new PyroscopeAgent.Options.Builder( 21 | new Config.Builder() 22 | .setApplicationName("demo.app{qweqwe=asdasd}") 23 | .setServerAddress("http://localhost:4040") 24 | .setFormat(Format.JFR) 25 | .setProfilingEvent(EventType.CTIMER) 26 | .setLogLevel(Logger.Level.DEBUG) 27 | .setProfilerType(ProfilerType.JFR) 28 | .setLabels(mapOf("user", "tolyan")) 29 | .build()) 30 | .build() 31 | ); 32 | Pyroscope.setStaticLabels(mapOf("region", "us-east-1")); 33 | 34 | appLogic(); 35 | } 36 | 37 | private static void appLogic() { 38 | ExecutorService executors = Executors.newFixedThreadPool(N_THREADS); 39 | for (int i = 0; i < N_THREADS; i++) { 40 | executors.submit(() -> { 41 | Pyroscope.LabelsWrapper.run(new LabelsSet("thread_name", Thread.currentThread().getName()), () -> { 42 | while (true) { 43 | try { 44 | fib(32L); 45 | } catch (InterruptedException e) { 46 | Thread.currentThread().interrupt(); 47 | break; 48 | } 49 | } 50 | } 51 | ); 52 | }); 53 | } 54 | } 55 | 56 | private static Map mapOf(String... args) { 57 | Map staticLabels = new HashMap<>(); 58 | for (int i = 0; i < args.length; i += 2) { 59 | staticLabels.put(args[i], args[i + 1]); 60 | } 61 | return staticLabels; 62 | } 63 | 64 | private static long fib(Long n) throws InterruptedException { 65 | if (n == 0L) { 66 | return 0L; 67 | } 68 | if (n == 1L) { 69 | return 1L; 70 | } 71 | return fib(n - 1) + fib(n - 2); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /demo/src/main/java/Fib.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/pyroscope-java/230eda066b9e20b54537a085d25da35e0001b38d/demo/src/main/java/Fib.class -------------------------------------------------------------------------------- /demo/src/main/java/Fib.java: -------------------------------------------------------------------------------- 1 | 2 | import java.util.concurrent.ExecutorService; 3 | import java.util.concurrent.Executors; 4 | 5 | public class Fib { 6 | public static final int N_THREADS; 7 | 8 | static { 9 | int n; 10 | try { 11 | n = Integer.parseInt(System.getenv("N_THREADS")); 12 | } catch (NumberFormatException e) { 13 | n = 1; 14 | } 15 | N_THREADS = n; 16 | } 17 | 18 | public static void main(String[] args) { 19 | appLogic(); 20 | } 21 | 22 | private static void appLogic() { 23 | ExecutorService executors = Executors.newFixedThreadPool(N_THREADS); 24 | for (int i = 0; i < N_THREADS; i++) { 25 | executors.submit(() -> { 26 | while (true) { 27 | try { 28 | fib(32L); 29 | } catch (InterruptedException e) { 30 | Thread.currentThread().interrupt(); 31 | break; 32 | } 33 | } 34 | }); 35 | } 36 | } 37 | 38 | private static long fib(Long n) throws InterruptedException { 39 | if (n == 0L) { 40 | return 0L; 41 | } 42 | if (n == 1L) { 43 | return 1L; 44 | } 45 | return fib(n - 1) + fib(n - 2); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /demo/src/main/java/StartStopApp.java: -------------------------------------------------------------------------------- 1 | import io.pyroscope.http.Format; 2 | import io.pyroscope.javaagent.EventType; 3 | import io.pyroscope.javaagent.PyroscopeAgent; 4 | import io.pyroscope.javaagent.api.Logger; 5 | import io.pyroscope.javaagent.config.Config; 6 | import io.pyroscope.labels.v2.LabelsSet; 7 | import io.pyroscope.labels.v2.Pyroscope; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | 14 | public class StartStopApp { 15 | public static final int N_THREADS = 2; 16 | public static final Config CONFIG = new Config.Builder() 17 | .setApplicationName("demo.app{qweqwe=asdasd}") 18 | .setServerAddress("http://localhost:4040") 19 | .setFormat(Format.JFR) 20 | .setProfilingEvent(EventType.ITIMER) 21 | .setLogLevel(Logger.Level.DEBUG) 22 | .setLabels(mapOf("user", "tolyan")) 23 | .build(); 24 | public static final PyroscopeAgent.Options OPTIONS = new PyroscopeAgent.Options.Builder(CONFIG) 25 | .build(); 26 | 27 | public static final int RUN_TIME = 30000; 28 | public static final int SLEEP_TIME = 30000; 29 | 30 | public static void main(String[] args) { 31 | 32 | 33 | appLogic(); 34 | 35 | while (true) { 36 | Pyroscope.setStaticLabels(mapOf("region", "us-east-1")); 37 | PyroscopeAgent.start(OPTIONS); 38 | 39 | System.out.println(">>> running for " + RUN_TIME); 40 | try { 41 | Thread.sleep(RUN_TIME); 42 | } catch (InterruptedException e) { 43 | Thread.currentThread().interrupt(); 44 | break; 45 | } 46 | 47 | PyroscopeAgent.stop(); 48 | 49 | System.out.println(">>> sleeping for " + RUN_TIME); 50 | try { 51 | Thread.sleep(SLEEP_TIME); 52 | } catch (InterruptedException e) { 53 | Thread.currentThread().interrupt(); 54 | break; 55 | } 56 | } 57 | } 58 | 59 | private static void appLogic() { 60 | ExecutorService executors = Executors.newFixedThreadPool(N_THREADS); 61 | for (int i = 0; i < N_THREADS; i++) { 62 | executors.submit(() -> { 63 | Pyroscope.LabelsWrapper.run(new LabelsSet("thread_name", Thread.currentThread().getName()), () -> { 64 | while (true) { 65 | try { 66 | fib(32L); 67 | } catch (InterruptedException e) { 68 | Thread.currentThread().interrupt(); 69 | break; 70 | } 71 | } 72 | } 73 | ); 74 | }); 75 | } 76 | } 77 | 78 | private static Map mapOf(String... args) { 79 | Map staticLabels = new HashMap<>(); 80 | for (int i = 0; i < args.length; i += 2) { 81 | staticLabels.put(args[i], args[i + 1]); 82 | } 83 | return staticLabels; 84 | } 85 | 86 | private static long fib(Long n) throws InterruptedException { 87 | if (n == 0L) { 88 | return 0L; 89 | } 90 | if (n == 1L) { 91 | return 1L; 92 | } 93 | return fib(n - 1) + fib(n - 2); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/TracingContext.kt: -------------------------------------------------------------------------------- 1 | import io.pyroscope.PyroscopeAsyncProfiler 2 | import io.pyroscope.javaagent.EventType 3 | import io.pyroscope.javaagent.PyroscopeAgent 4 | import io.pyroscope.javaagent.Snapshot 5 | import io.pyroscope.javaagent.api.Exporter 6 | import io.pyroscope.javaagent.api.Logger 7 | import io.pyroscope.javaagent.config.Config 8 | import io.pyroscope.javaagent.impl.DefaultLogger 9 | import io.pyroscope.javaagent.impl.PyroscopeExporter 10 | import io.pyroscope.javaagent.impl.QueuedExporter 11 | import io.pyroscope.labels.v2.LabelsSet 12 | import io.pyroscope.labels.v2.Pyroscope 13 | import io.pyroscope.labels.v2.ScopedContext 14 | import java.io.File 15 | import java.util.concurrent.atomic.AtomicLong 16 | 17 | fun fib(n: Int): Int { 18 | if (n <= 1) { 19 | return 1 20 | } 21 | return fib(n - 1) + fib(n - 2) 22 | } 23 | 24 | fun inf() { 25 | while (true) { 26 | 27 | } 28 | } 29 | 30 | fun main() { 31 | val constFib = Pyroscope.LabelsWrapper.registerConstant("Fibonachi") 32 | val constInf = Pyroscope.LabelsWrapper.registerConstant("Infinite") 33 | println(constFib) 34 | println(constInf) 35 | 36 | val config = Config.Builder() 37 | .setApplicationName("spanContextApp") 38 | .setProfilingEvent(EventType.CTIMER) 39 | .build() 40 | val logger = DefaultLogger(Logger.Level.DEBUG, System.out) 41 | val exporter = QueuedExporter(config, PyroscopeExporter(config, logger), logger) 42 | 43 | val opt = PyroscopeAgent.Options.Builder(config) 44 | .setLogger(logger) 45 | .setExporter(Dumper(exporter)) 46 | .build() 47 | PyroscopeAgent.start(opt) 48 | 49 | val t1 = Thread { 50 | PyroscopeAsyncProfiler.getAsyncProfiler() 51 | .setTracingContext(239, constFib); 52 | ScopedContext(LabelsSet("dyn2", "v2")).use { 53 | while (true) { 54 | fib(13) 55 | } 56 | } 57 | } 58 | val t2 = Thread { 59 | PyroscopeAsyncProfiler.getAsyncProfiler() 60 | .setTracingContext(4242, constInf); 61 | 62 | ScopedContext(LabelsSet("dyn1", "v1")).use { 63 | inf() 64 | } 65 | } 66 | t1.start() 67 | t2.start() 68 | 69 | } 70 | 71 | class Dumper( 72 | val next: Exporter, 73 | ) : Exporter { 74 | private val counter: AtomicLong = AtomicLong() 75 | override fun export(p0: Snapshot) { 76 | val no = counter.incrementAndGet() 77 | File("./profile.${no}.bin").writeBytes(p0.data) 78 | File("./profile.${no}.labels.bin").writeBytes(p0.labels.toByteArray()) 79 | next.export(p0) 80 | } 81 | 82 | override fun stop() { 83 | next.stop() 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /docker-compose-itest.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | alpine-3.16-8: 3 | build: 4 | platforms: 5 | - linux/amd64 6 | context: . 7 | dockerfile: alpine-test.Dockerfile 8 | args: 9 | IMAGE_VERSION: 3.16 10 | JAVA_VERSION: 8 11 | alpine-3.16-11: 12 | build: 13 | platforms: 14 | - linux/amd64 15 | context: . 16 | dockerfile: alpine-test.Dockerfile 17 | args: 18 | IMAGE_VERSION: 3.16 19 | JAVA_VERSION: 11 20 | alpine-3.16-17: 21 | build: 22 | platforms: 23 | - linux/amd64 24 | context: . 25 | dockerfile: alpine-test.Dockerfile 26 | args: 27 | IMAGE_VERSION: 3.16 28 | JAVA_VERSION: 17 29 | alpine-3.17-8: 30 | build: 31 | platforms: 32 | - linux/amd64 33 | context: . 34 | dockerfile: alpine-test.Dockerfile 35 | args: 36 | IMAGE_VERSION: 3.17 37 | JAVA_VERSION: 8 38 | alpine-3.17-11: 39 | build: 40 | platforms: 41 | - linux/amd64 42 | context: . 43 | dockerfile: alpine-test.Dockerfile 44 | args: 45 | IMAGE_VERSION: 3.17 46 | JAVA_VERSION: 11 47 | alpine-3.17-17: 48 | build: 49 | platforms: 50 | - linux/amd64 51 | context: . 52 | dockerfile: alpine-test.Dockerfile 53 | args: 54 | IMAGE_VERSION: 3.17 55 | JAVA_VERSION: 17 56 | alpine-3.18-8: 57 | build: 58 | platforms: 59 | - linux/amd64 60 | context: . 61 | dockerfile: alpine-test.Dockerfile 62 | args: 63 | IMAGE_VERSION: 3.18 64 | JAVA_VERSION: 8 65 | alpine-3.18-11: 66 | build: 67 | platforms: 68 | - linux/amd64 69 | context: . 70 | dockerfile: alpine-test.Dockerfile 71 | args: 72 | IMAGE_VERSION: 3.18 73 | JAVA_VERSION: 11 74 | alpine-3.18-17: 75 | build: 76 | platforms: 77 | - linux/amd64 78 | context: . 79 | dockerfile: alpine-test.Dockerfile 80 | args: 81 | IMAGE_VERSION: 3.18 82 | JAVA_VERSION: 17 83 | alpine-3.19-8: 84 | build: 85 | platforms: 86 | - linux/amd64 87 | context: . 88 | dockerfile: alpine-test.Dockerfile 89 | args: 90 | IMAGE_VERSION: 3.19 91 | JAVA_VERSION: 8 92 | alpine-3.19-11: 93 | build: 94 | platforms: 95 | - linux/amd64 96 | context: . 97 | dockerfile: alpine-test.Dockerfile 98 | args: 99 | IMAGE_VERSION: 3.19 100 | JAVA_VERSION: 11 101 | alpine-3.19-17: 102 | build: 103 | platforms: 104 | - linux/amd64 105 | context: . 106 | dockerfile: alpine-test.Dockerfile 107 | args: 108 | IMAGE_VERSION: 3.19 109 | JAVA_VERSION: 17 110 | 111 | ubuntu-18.04-8: 112 | build: 113 | platforms: 114 | - linux/amd64 115 | context: . 116 | dockerfile: ubuntu-test.Dockerfile 117 | args: 118 | IMAGE_VERSION: 18.04 119 | JAVA_VERSION: 8 120 | ubuntu-18.04-11: 121 | build: 122 | platforms: 123 | - linux/amd64 124 | context: . 125 | dockerfile: ubuntu-test.Dockerfile 126 | args: 127 | IMAGE_VERSION: 18.04 128 | JAVA_VERSION: 11 129 | ubuntu-18.04-17: 130 | build: 131 | platforms: 132 | - linux/amd64 133 | context: . 134 | dockerfile: ubuntu-test.Dockerfile 135 | args: 136 | IMAGE_VERSION: 18.04 137 | JAVA_VERSION: 17 138 | ubuntu-20.04-8: 139 | build: 140 | platforms: 141 | - linux/amd64 142 | context: . 143 | dockerfile: ubuntu-test.Dockerfile 144 | args: 145 | IMAGE_VERSION: 20.04 146 | JAVA_VERSION: 8 147 | ubuntu-20.04-11: 148 | build: 149 | platforms: 150 | - linux/amd64 151 | context: . 152 | dockerfile: ubuntu-test.Dockerfile 153 | args: 154 | IMAGE_VERSION: 20.04 155 | JAVA_VERSION: 11 156 | ubuntu-20.04-17: 157 | build: 158 | platforms: 159 | - linux/amd64 160 | context: . 161 | dockerfile: ubuntu-test.Dockerfile 162 | args: 163 | IMAGE_VERSION: 20.04 164 | JAVA_VERSION: 17 165 | ubuntu-20.04-21: 166 | build: 167 | platforms: 168 | - linux/amd64 169 | context: . 170 | dockerfile: ubuntu-test.Dockerfile 171 | args: 172 | IMAGE_VERSION: 20.04 173 | JAVA_VERSION: 21 174 | ubuntu-22.04-8: 175 | build: 176 | platforms: 177 | - linux/amd64 178 | context: . 179 | dockerfile: ubuntu-test.Dockerfile 180 | args: 181 | IMAGE_VERSION: 22.04 182 | JAVA_VERSION: 8 183 | ubuntu-22.04-11: 184 | build: 185 | platforms: 186 | - linux/amd64 187 | context: . 188 | dockerfile: ubuntu-test.Dockerfile 189 | args: 190 | IMAGE_VERSION: 22.04 191 | JAVA_VERSION: 11 192 | ubuntu-22.04-17: 193 | build: 194 | platforms: 195 | - linux/amd64 196 | context: . 197 | dockerfile: ubuntu-test.Dockerfile 198 | args: 199 | IMAGE_VERSION: 22.04 200 | JAVA_VERSION: 17 201 | ubuntu-22.04-21: 202 | build: 203 | platforms: 204 | - linux/amd64 205 | context: . 206 | dockerfile: ubuntu-test.Dockerfile 207 | args: 208 | IMAGE_VERSION: 22.04 209 | JAVA_VERSION: 21 210 | pyroscope: 211 | image: 'grafana/pyroscope:latest' 212 | ports: 213 | - 4040:4040 214 | 215 | 216 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11.0.11-jdk 2 | 3 | ADD https://download.jboss.org/optaplanner/release/8.6.0.Final/optaweb-employee-rostering-distribution-8.6.0.Final.zip / 4 | 5 | RUN apt-get update && apt-get install -y unzip \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN unzip optaweb-employee-rostering-distribution-8.6.0.Final.zip && \ 9 | rm optaweb-employee-rostering-distribution-8.6.0.Final.zip 10 | 11 | ADD pyroscope.jar / 12 | 13 | CMD ["java", "-javaagent:/pyroscope.jar", "-jar", "/optaweb-employee-rostering-distribution-8.6.0.Final/bin/optaweb-employee-rostering-standalone-8.6.0.Final/quarkus-run.jar"] 14 | -------------------------------------------------------------------------------- /examples/docker-compose-base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.9" 3 | services: 4 | pyroscope: 5 | image: "pyroscope/pyroscope:latest" 6 | ports: 7 | - "4040:4040" 8 | command: 9 | - "server" 10 | app: 11 | build: . 12 | environment: 13 | - PYROSCOPE_APPLICATION_NAME=optaplanner 14 | - PYROSCOPE_PROFILING_INTERVAL=10ms 15 | - PYROSCOPE_PROFILER_EVENT=itimer 16 | - PYROSCOPE_UPLOAD_INTERVAL=1s 17 | - PYROSCOPE_LOG_LEVEL=debug 18 | - PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 19 | - PYROSCOPE_AUTH_TOKEN=abc123 20 | ports: 21 | - "8080:8080" 22 | -------------------------------------------------------------------------------- /examples/docker-compose-expt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.9" 3 | services: 4 | pyroscope: 5 | image: "pyroscope/pyroscope:latest" 6 | ports: 7 | - "4040:4040" 8 | command: 9 | - "server" 10 | app: 11 | build: . 12 | environment: 13 | - PYROSCOPE_APPLICATION_NAME=optaplanner 14 | - PYROSCOPE_PROFILING_INTERVAL=10ms 15 | - PYROSCOPE_PROFILER_EVENT=itimer 16 | - PYROSCOPE_PROFILER_ALLOC=4k 17 | - PYROSCOPE_UPLOAD_INTERVAL=8s 18 | - PYROSCOPE_SAMPLING_DURATION=2s 19 | - PYROSCOPE_FORMAT=jfr 20 | - PYROSCOPE_SAMPLING_EVENT_ORDER=itimer,alloc 21 | - PYROSCOPE_LOG_LEVEL=debug 22 | - PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 23 | - PYROSCOPE_AUTH_TOKEN=abc123 24 | ports: 25 | - "8080:8080" 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | pyroscope_version=2.1.2 2 | async_profiler_version=4.0.0.0 3 | useLocalAsyncProfilerArtifacts=false 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/pyroscope-java/230eda066b9e20b54537a085d25da35e0001b38d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /itest/query/go.mod: -------------------------------------------------------------------------------- 1 | module java-test-querier 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | connectrpc.com/connect v1.18.0 9 | github.com/grafana/pyroscope/api v1.2.0 10 | ) 11 | 12 | require ( 13 | github.com/gorilla/mux v1.8.0 // indirect 14 | github.com/planetscale/vtprotobuf v0.6.0 // indirect 15 | golang.org/x/net v0.38.0 // indirect 16 | golang.org/x/sys v0.31.0 // indirect 17 | golang.org/x/text v0.23.0 // indirect 18 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 19 | google.golang.org/grpc v1.65.0 // indirect 20 | google.golang.org/protobuf v1.34.2 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /itest/query/go.sum: -------------------------------------------------------------------------------- 1 | connectrpc.com/connect v1.18.0 h1:7ZHAkx8fTaRO4YIyvV00XiS8bx4XjWp0grk9oh0PIQ0= 2 | connectrpc.com/connect v1.18.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 6 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | github.com/grafana/pyroscope/api v1.2.0 h1:SfHDZcEZ4Vbj/Jj3bTOSpm4IDB33wLA2xBYxROhiL4U= 8 | github.com/grafana/pyroscope/api v1.2.0/go.mod h1:CCWrMnwvTB5O+VBZfT+jO2RAvgm0GxdG2//kAWuMDhA= 9 | github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA= 10 | github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 11 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 12 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 13 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 14 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 15 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 16 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 17 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= 18 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= 19 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 20 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 21 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 22 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 23 | -------------------------------------------------------------------------------- /itest/query/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "slices" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "connectrpc.com/connect" 14 | 15 | profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 16 | querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 17 | 18 | "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1/querierv1connect" 19 | ) 20 | 21 | func main() { 22 | type result struct { 23 | target string 24 | err error 25 | } 26 | targets := os.Args[1:] 27 | if len(targets) == 0 { 28 | targets = []string{ 29 | "alpine-3.16-8", 30 | "alpine-3.16-11", 31 | "alpine-3.16-17", 32 | "alpine-3.17-8", 33 | "alpine-3.17-11", 34 | "alpine-3.17-17", 35 | "alpine-3.18-8", 36 | "alpine-3.18-11", 37 | "alpine-3.18-17", 38 | "alpine-3.19-8", 39 | "alpine-3.19-11", 40 | "alpine-3.19-17", 41 | "ubuntu-18.04-8", 42 | "ubuntu-18.04-11", 43 | "ubuntu-18.04-17", 44 | "ubuntu-20.04-8", 45 | "ubuntu-20.04-11", 46 | "ubuntu-20.04-17", 47 | "ubuntu-20.04-21", 48 | "ubuntu-22.04-8", 49 | "ubuntu-22.04-11", 50 | "ubuntu-22.04-17", 51 | "ubuntu-22.04-21", 52 | } 53 | } 54 | url := "http://localhost:4040" 55 | qc := querierv1connect.NewQuerierServiceClient( 56 | http.DefaultClient, 57 | url, 58 | ) 59 | wg := sync.WaitGroup{} 60 | ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Minute)) 61 | results := make(chan result, len(targets)) 62 | for _, target := range targets { 63 | wg.Add(1) 64 | go func(target string) { 65 | defer wg.Done() 66 | err := testTarget(ctx, qc, target) 67 | results <- result{target: target, err: err} 68 | }(target) 69 | } 70 | wg.Wait() 71 | close(results) 72 | 73 | failed := false 74 | for r := range results { 75 | if r.err != nil { 76 | fmt.Printf("[%s] %s\n", r.target, r.err.Error()) 77 | failed = true 78 | } else { 79 | fmt.Printf("[%s] OK\n", r.target) 80 | } 81 | } 82 | if failed { 83 | os.Exit(1) 84 | } 85 | } 86 | 87 | func testTarget(ctx context.Context, qc querierv1connect.QuerierServiceClient, target string) error { 88 | //needle := "Fib$$Lambda_.run;Fib.lambda$appLogic$0;Fib.fib;Fib.fib;Fib.fib;Fib.fib;" // us this one when https://github.com/grafana/jfr-parser/pull/28/files lands pyroscope docker tag 89 | needle := "run;Fib.lambda$appLogic$0;Fib.fib;Fib.fib;Fib.fib;Fib.fib;" 90 | ticker := time.NewTicker(time.Second * 5) 91 | n := 0 92 | for { 93 | select { 94 | case <-ctx.Done(): 95 | return fmt.Errorf("timed out waiting for target %s. tried %d times", target, n) 96 | case <-ticker.C: 97 | n += 1 98 | to := time.Now() 99 | from := to.Add(-time.Minute * 1) 100 | 101 | resp, err := qc.SelectMergeProfile(context.Background(), connect.NewRequest(&querierv1.SelectMergeProfileRequest{ 102 | ProfileTypeID: "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 103 | Start: from.UnixMilli(), 104 | End: to.UnixMilli(), 105 | LabelSelector: fmt.Sprintf("{service_name=\"%s\"}", target), 106 | })) 107 | if err != nil { 108 | fmt.Printf("[%s] %d %s\n", target, n, err.Error()) 109 | continue 110 | } 111 | 112 | ss := stackCollapseProto(resp.Msg, false) 113 | if !strings.Contains(ss, needle) { 114 | fmt.Printf("[%s] %d not found yet\n%s\n", target, n, ss) 115 | continue 116 | } 117 | fmt.Printf("[%s] %d OK\n", target, n) 118 | return nil 119 | } 120 | } 121 | 122 | } 123 | 124 | func stackCollapseProto(p *profilev1.Profile, lineNumbers bool) string { 125 | allZeros := func(a []int64) bool { 126 | for _, v := range a { 127 | if v != 0 { 128 | return false 129 | } 130 | } 131 | return true 132 | } 133 | addValues := func(a, b []int64) { 134 | for i := range a { 135 | a[i] += b[i] 136 | } 137 | } 138 | 139 | type stack struct { 140 | funcs string 141 | value []int64 142 | } 143 | locMap := make(map[int64]*profilev1.Location) 144 | funcMap := make(map[int64]*profilev1.Function) 145 | for _, l := range p.Location { 146 | locMap[int64(l.Id)] = l 147 | } 148 | for _, f := range p.Function { 149 | funcMap[int64(f.Id)] = f 150 | } 151 | 152 | var ret []stack 153 | for _, s := range p.Sample { 154 | var funcs []string 155 | for i := range s.LocationId { 156 | locID := s.LocationId[len(s.LocationId)-1-i] 157 | loc := locMap[int64(locID)] 158 | for _, line := range loc.Line { 159 | f := funcMap[int64(line.FunctionId)] 160 | fname := p.StringTable[f.Name] 161 | if lineNumbers { 162 | fname = fmt.Sprintf("%s:%d", fname, line.Line) 163 | } 164 | funcs = append(funcs, fname) 165 | } 166 | } 167 | 168 | vv := make([]int64, len(s.Value)) 169 | copy(vv, s.Value) 170 | ret = append(ret, stack{ 171 | funcs: strings.Join(funcs, ";"), 172 | value: vv, 173 | }) 174 | } 175 | slices.SortFunc(ret, func(i, j stack) int { 176 | return strings.Compare(i.funcs, j.funcs) 177 | }) 178 | var unique []stack 179 | for _, s := range ret { 180 | if allZeros(s.value) { 181 | continue 182 | } 183 | if len(unique) == 0 { 184 | unique = append(unique, s) 185 | continue 186 | } 187 | 188 | if unique[len(unique)-1].funcs == s.funcs { 189 | addValues(unique[len(unique)-1].value, s.value) 190 | continue 191 | } 192 | unique = append(unique, s) 193 | 194 | } 195 | 196 | res := make([]string, 0, len(unique)) 197 | for _, s := range unique { 198 | res = append(res, fmt.Sprintf("%s %v", s.funcs, s.value)) 199 | } 200 | return strings.Join(res, "\n") 201 | } 202 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'pyroscope-java' 2 | include('agent') 3 | include('async-profiler-context') 4 | include('demo') 5 | -------------------------------------------------------------------------------- /ubuntu-test.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE_VERSION 2 | ARG JAVA_VERSION 3 | 4 | FROM ubuntu:18.04 AS builder 5 | RUN apt-get update && apt-get install -y openjdk-8-jdk-headless 6 | 7 | WORKDIR /app 8 | ADD gradlew build.gradle settings.gradle gradle.properties /app/ 9 | ADD gradle gradle 10 | RUN ./gradlew --no-daemon --version 11 | ADD agent agent 12 | ADD async-profiler-context async-profiler-context 13 | ADD demo/build.gradle demo/ 14 | 15 | # for testing locally produced artifacts 16 | #COPY async-profiler-3.0.0.1-linux-x64.tar.gz . 17 | #COPY async-profiler-3.0.0.1-linux-arm64.tar.gz . 18 | #COPY async-profiler-3.0.0.1-macos.zip . 19 | 20 | RUN ./gradlew --no-daemon shadowJar 21 | 22 | FROM ubuntu:${IMAGE_VERSION} AS runner 23 | ARG IMAGE_VERSION 24 | ARG JAVA_VERSION 25 | RUN apt-get update && apt-get install -y openjdk-${JAVA_VERSION}-jdk-headless 26 | 27 | WORKDIR /app 28 | ADD demo demo 29 | COPY --from=builder /app/agent/build/libs/pyroscope.jar /app/agent/build/libs/pyroscope.jar 30 | 31 | RUN javac demo/src/main/java/Fib.java 32 | 33 | ENV PYROSCOPE_LOG_LEVEL=debug 34 | ENV PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 35 | ENV PYROSCOPE_UPLOAD_INTERVAL=5s 36 | ENV PYROSCOPE_APPLICATION_NAME=ubuntu-${IMAGE_VERSION}-${JAVA_VERSION} 37 | 38 | CMD ["java", "-javaagent:/app/agent/build/libs/pyroscope.jar", "-cp", "demo/src/main/java/", "Fib"] 39 | --------------------------------------------------------------------------------