diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..258116649 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,125 @@ +# GitHub Copilot Instructions for BTrace + +## About BTrace +BTrace is a safe, dynamic tracing tool for the Java platform. It dynamically instruments running Java applications to inject tracing code at runtime using bytecode instrumentation. + +## Project Structure +- **Gradle multi-module project** with modules named `btrace-*` +- **Core modules**: `btrace-core`, `btrace-agent`, `btrace-runtime`, `btrace-client`, `btrace-instr` +- **Build artifacts**: `btrace-dist` for distributions +- **Tests**: `integration-tests/` for integration tests, `src/test/java` in modules for unit tests +- **Documentation**: `docs/` directory + +## Architecture Overview +- **btrace-agent**: Attachable Java agent with class transformer, manages script lifecycle +- **btrace-compiler**: Verifies and compiles BTrace scripts to bytecode +- **btrace-instr**: ASM-based instrumentation and weaving utilities +- **btrace-runtime**: APIs for scripts (printing, timers, data collection) +- **btrace-client**: CLI/attach tooling for sending scripts to target JVM +- **services**: SPI for pluggable exporters (e.g., statsd) + +## Development Guidelines + +### Language & Versions +- **Language**: Java +- **Source/Target**: Java 8 +- **Build toolchain**: JDK 11 +- **Test framework**: JUnit Jupiter (JUnit 5) + +### Code Style +- **Format**: Google Java Format enforced via Spotless +- **Packages**: All under `io.btrace.*` +- **Naming**: Module names follow `btrace-` pattern +- **Imports**: Order enforced; remove unused imports +- **Comments**: Only add if they match existing style or explain complex logic + +### Building & Testing +```bash +# Full build with unit tests +./gradlew build + +# Build distribution only +./gradlew :btrace-dist:build + +# Run unit tests +./gradlew test + +# Run integration tests (requires dist build first) +./gradlew -Pintegration test + +# Format code +./gradlew spotlessApply + +# Check formatting +./gradlew spotlessCheck +``` + +### Important Environment Variables +- `JAVA_HOME`: Required for builds +- `TEST_JAVA_HOME`: Required for integration tests (typically JDK 11) +- `BTRACE_TEST_DEBUG=true`: Enable verbose integration test output +- `BTRACE_HOME`: Optional, points to exploded dist + +### Testing Best Practices +- Unit tests: `src/test/java` with `*Test` suffix +- Integration tests: `integration-tests/src/test/java` +- BTrace scripts: `integration-tests/src/test/btrace` +- Always run relevant tests after making changes +- Update golden files when changing instrumentor: `./gradlew test -PupdateTestData` + +### Commit & PR Guidelines +- **Commit style**: Conventional Commits (e.g., `feat(core): add probe`, `fix(instr): handle null arg`) +- **Clear descriptions**: Link related issues +- **Tests required**: Update/add tests; ensure CI passes +- **Formatting**: Must pass `spotlessCheck` +- **No unrelated changes**: Keep changes focused and minimal + +## Troubleshooting + +### Build Issues +- **Attach disabled**: Remove `-XX:+DisableAttachMechanism` from target JVM +- **Permission errors**: Attach requires same OS user as target JVM +- **Toolchain issues**: Verify `JAVA_HOME` and `TEST_JAVA_HOME` point to valid JDKs + +### Restricted Environments +```bash +# Use workspace-local Gradle cache +GRADLE_USER_HOME=$(pwd)/.gradle-user + +# Force IPv4 to avoid network interface issues +JAVA_TOOL_OPTIONS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false" +``` + +## Code Generation Tips +- **Prefer simplicity**: Simple, performant solutions over complex designs +- **Use existing patterns**: Follow patterns from similar code in the repository +- **Minimal changes**: Make the smallest possible changes to achieve the goal +- **Reuse libraries**: Use ASM for bytecode, JCTools for concurrency, existing BTrace APIs +- **No temporary files in repo**: Use `/tmp` for scratch work +- **Security**: Never commit secrets; avoid introducing vulnerabilities + +## Example BTrace Script Pattern +```java +package example; +import static io.btrace.core.BTraceUtils.*; +import io.btrace.core.annotations.*; + +@BTrace +public class ExampleTrace { + @OnMethod(clazz="com.example.Target", method="methodName") + public static void onMethod(@ProbeMethodName String method) { + println("Called: " + method); + } +} +``` + +## Key Dependencies +- **ASM**: Bytecode manipulation +- **JCTools**: High-performance concurrent data structures +- **hppcrt**: Optimized collections +- **JUnit Jupiter**: Testing framework + +## Additional Resources +- Full guidelines: See `AGENTS.md` in repository root +- Tutorial: `docs/BTraceTutorial.md` +- Binary releases: https://github.com/btraceio/btrace/releases diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..f952e80fc --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,339 @@ +# BTrace GitHub Actions Workflows + +## Overview + +This directory contains GitHub Actions workflows for continuous integration and testing of the BTrace project, with special focus on the new Binary Protocol v2 implementation. + +## Workflows + +### 1. `continuous.yml` - Main CI/CD Pipeline + +**Purpose:** Main continuous integration pipeline for all BTrace components. + +**Triggers:** +- Push to `develop` branch +- Pull requests to `develop` +- Manual workflow dispatch + +**Jobs:** +- **build:** Compiles the project and runs all tests + - Java 11 with Temurin distribution + - Parallel build with caching + - **V2 Protocol Tests:** Runs dedicated v2 protocol test suite + - Uploads dist build artifacts +- **test:** Runs integration tests on multiple Java versions + - Matrix: Java 8, 11, 17, 21, 25 (EA) + - Uses SDKMAN for multiple JDK management + - Downloads build artifacts from previous job + - Runs integration tests with `-Pintegration` flag +- **publish:** Publishes artifacts to Maven Central + - Only on `develop` branch + - Requires GPG signing credentials +- **cleanup:** Removes temporary artifacts + +**Enhancements for V2 Protocol:** +- Added explicit V2 protocol test execution in build job +- Tests all v2 packages: `v2.*`, `Protocol*`, `WireProtocol*` + +### 2. `v2-protocol-tests.yml` - V2 Protocol Test Suite + +**Purpose:** Comprehensive testing suite specifically for Binary Protocol v2. + +**Triggers:** +- Push to `develop`, `master`, or `jb/comm_v2` branches +- Pull requests to `develop` +- Changes to protocol-related files +- Manual workflow dispatch +- Commit messages containing `[benchmark]` + +**Jobs:** + +#### **unit-tests** +- Runs all v2 protocol unit tests on Java 11, 17, 21 +- Tests binary serialization/deserialization +- Validates all 17 command types +- Upload test reports as artifacts + +#### **protocol-negotiation-tests** +- Tests protocol version detection +- Validates V1/V2 negotiation +- Tests configuration management +- Verifies magic byte detection + +#### **edge-case-tests** +- Runs 35 edge case scenarios +- Tests boundary conditions +- Validates large message handling +- Tests compression functionality +- Unicode and special character handling + +#### **jmh-benchmarks** (Manual/Opt-in) +- Runs JMH performance benchmarks +- Triggered by workflow dispatch or `[benchmark]` in commit message +- Quick benchmarks: warmup=1, iterations=2, fork=1 +- Focuses on serialization performance +- Uploads JMH results for 30 days + +#### **protocol-compatibility** +- Tests all 4 compatibility scenarios: + - V2 client ↔ V2 agent (optimal) + - V1 client ↔ V1 agent (legacy) + - V2 client ↔ V1 agent (fallback) + - V1 client ↔ V2 agent (detection) +- Matrix strategy for comprehensive coverage +- Validates backward compatibility + +#### **test-summary** +- Aggregates test results from all jobs +- Generates GitHub Step Summary +- Reports total/passed/failed counts +- Fails if any tests failed + +#### **code-coverage** +- Generates JaCoCo coverage reports +- Focuses on `io.btrace.core.comm` package +- Uploads coverage artifacts for 30 days +- Creates coverage summary in step output + +### 3. `codeql-analysis.yml` - Security Analysis + +**Purpose:** CodeQL security scanning for vulnerability detection. + +**Triggers:** Push/PR to default branch + +### 4. `stale.yml` - Issue Management + +**Purpose:** Automatically marks stale issues and PRs. + +**Schedule:** Daily at midnight + +### 5. `release.yml` - Release Management + +**Purpose:** Handles the complete release process with a manual checkpoint for Maven Central. + +**Trigger:** Manual via `scripts/release.sh` or workflow_dispatch + +**Key Features:** +- Stages artifacts to Maven Central (does NOT auto-release) +- Waits up to 30 minutes for manual release via Central Portal +- Creates GitHub release only after Maven artifacts are available +- Updates SDKMan and manages milestones + +**Manual Checkpoint:** After staging, you must release via [Central Portal](https://central.sonatype.com/publishing/deployments). This allows reviewing artifacts before they become permanent. + +## V2 Protocol Test Coverage + +The workflows ensure comprehensive testing of the v2 protocol implementation: + +### Unit Tests (113 total) +- ✅ Binary protocol serialization (26 tests) +- ✅ Edge cases and boundaries (35 tests) +- ✅ Performance comparison (2 tests) +- ✅ Protocol negotiation (16 tests) +- ✅ Configuration management (18 tests) +- ✅ WireProtocol abstraction (16 tests) + +### Test Categories +1. **Command Serialization** + - All 17 BTrace command types + - Round-trip serialization/deserialization + - Compression testing + +2. **Protocol Negotiation** + - V1/V2 auto-detection + - Magic byte validation + - Configuration-based selection + - Stream handling (pushback & mark/reset) + +3. **Edge Cases** + - Null/empty values + - Large messages (10MB) + - Unicode and emojis + - Malformed data + - Numeric boundaries + +4. **Performance** + - JMH benchmarks (180 configurations) + - V1 vs V2 comparison + - Compression effectiveness + +5. **Compatibility** + - V1 ↔ V1 (legacy) + - V2 ↔ V2 (optimal) + - V1 ↔ V2 (cross-version) + - V2 ↔ V1 (fallback) + +## Artifacts + +### Retained Artifacts (7 days) +- Test reports (per Java version) +- Negotiation test results +- Edge case test results +- Compatibility test matrices + +### Long-term Artifacts (30 days) +- JMH benchmark results +- Code coverage reports + +### Build Artifacts (1 day) +- Distribution builds +- Test trace data + +## Configuration + +### Environment Variables + +**Build Job:** +- Standard Gradle environment +- Parallel execution enabled +- Build cache enabled + +**Test Job:** +- `TEST_JAVA_HOME`: Set per matrix Java version +- SDKMAN for multiple JDK management + +**Publish Job:** +- `GPG_SIGNING_KEY`: GPG key for artifact signing +- `GPG_SIGNING_PWD`: GPG key password +- `BTRACE_SONATYPE_USER`: Sonatype credentials +- `BTRACE_SONATYPE_PWD`: Sonatype credentials + +### Gradle Properties for V2 Testing + +```bash +# Run only v2 tests +./gradlew :btrace-core:test --tests "io.btrace.core.comm.v2.*" + +# Run protocol negotiation tests +./gradlew :btrace-core:test --tests "*Protocol*" + +# Run specific JMH benchmarks +./gradlew :btrace-core:jmh -PjmhInclude=".*MessageCommand.*" + +# Generate coverage report +./gradlew :btrace-core:test jacocoTestReport +``` + +## JMH Benchmark Workflow + +### Trigger Benchmark Run + +**Option 1: Workflow Dispatch** +```bash +# Via GitHub UI: Actions → V2 Protocol Tests → Run workflow +``` + +**Option 2: Commit Message** +```bash +git commit -m "Optimize binary protocol [benchmark]" +``` + +### Benchmark Configuration + +**Quick Benchmarks (CI):** +- Warmup: 1 iteration +- Measurement: 2 iterations +- Forks: 1 +- Focus: Serialization methods only + +**Full Benchmarks (Local):** +- Warmup: 3 iterations +- Measurement: 5 iterations +- Forks: 2 +- Coverage: All 180 configurations + +## Test Failure Handling + +### Automatic Retry +- Tests use `--rerun-tasks` to ensure fresh execution +- No test result caching to catch flaky tests + +### Artifact Upload +- All test reports uploaded on failure (`if: always()`) +- Artifacts retained for 7 days for analysis + +### Summary Generation +- Test summary job aggregates all results +- Reports failures clearly in GitHub UI +- Step summary provides quick overview + +## Code Coverage + +### JaCoCo Configuration + +**Focus Area:** +- Package: `io.btrace.core.comm.**` +- Includes v2 protocol, negotiation, and abstraction + +**Reports Generated:** +- XML (for CI tools) +- HTML (for human review) +- Available in artifacts for 30 days + +**Coverage Goals:** +- Unit test coverage: >90% +- Edge case coverage: >80% +- Integration coverage: >70% + +## Performance Monitoring + +### JMH Results +- Benchmark results uploaded as artifacts +- Compare across runs to detect regressions +- Focus on serialization/deserialization speed +- Monitor wire size changes + +### Expected Metrics +- Serialization: 3-6x faster than V1 +- Wire size: 2-5x smaller than V1 +- Compression: 10-100x size reduction (large messages) + +## Maintenance + +### Cache Management +- Gradle cache keyed by build files hash +- Automatic cache eviction after 7 days +- Cache size monitored in test job + +### Artifact Cleanup +- Temporary artifacts cleaned after publish +- Test reports retained for 7 days +- Performance results retained for 30 days + +## Future Enhancements + +### Planned Additions +1. **Integration Tests:** + - Full client-agent communication tests + - Mixed protocol version scenarios + - Reconnection testing + +2. **Stress Tests:** + - High concurrency scenarios + - Large message throughput + - Memory leak detection + +3. **Performance Regression Detection:** + - Automated benchmark comparison + - Alert on >10% performance degradation + - Historical trend analysis + +4. **Security Scanning:** + - Dependency vulnerability checks + - OWASP security analysis + - Protocol fuzzing tests + +## References + +- [BTrace v2 Protocol Architecture](../../docs/architecture/Version2ProtocolArchitecture.md) +- [Phase 3 Integration Guide](../../docs/architecture/phase3-integration-guide.md) +- [V2 Implementation Summary](../../docs/architecture/v2-implementation-summary.md) +- [JMH Benchmarks Guide](../../btrace-core/JMH_BENCHMARKS.md) + +## Support + +For workflow issues or questions: +1. Check GitHub Actions logs +2. Review artifact contents +3. Check Gradle build logs +4. Open issue with workflow run link diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..06d7998e8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,64 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '23 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + - name: Set up Java 24 + uses: actions/setup-java@v5 + with: + java-version: 24 + distribution: temurin + + - name: Build BTrace + run: | + ./gradlew -x test build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/continuous.yml b/.github/workflows/continuous.yml index 2ebbf54bc..6a5c83868 100644 --- a/.github/workflows/continuous.yml +++ b/.github/workflows/continuous.yml @@ -5,7 +5,7 @@ name: BTrace CI/CD on: push: - branches: [ develop, master ] + branches: [ develop ] pull_request: branches: [ develop ] workflow_dispatch: @@ -15,60 +15,201 @@ defaults: shell: bash jobs: - all: + build: + runs-on: ubuntu-latest + steps: + - name: Set up Java 24 + uses: actions/setup-java@v5 + with: + java-version: 24 + distribution: temurin + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: temurin + - name: Checkout + uses: actions/checkout@v6 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + - name: Generate cache key + id: cache-key + run: | + key="${{ runner.os }}-gradle-1-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" + echo "key=${key}" >> $GITHUB_ENV + echo "cache-key=${key}" >> $GITHUB_OUTPUT + - name: Cache Gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ env.key }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build + run: ./gradlew --no-daemon --parallel --build-cache build + - name: Run V2 Protocol Tests + run: | + ./gradlew :btrace-core:test --tests "io.btrace.core.comm.v2.*" \ + --tests "io.btrace.core.comm.Protocol*" \ + --tests "io.btrace.core.comm.WireProtocol*" + - name: Upload dist build data + if: always() + uses: actions/upload-artifact@v7 + with: + name: dist-build + retention-days: 1 + path: | + btrace-dist/build + btrace-agent/build/classes/traces + - name: Upload test trace data + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-trace + retention-days: 1 + path: | + btrace-agent/build/classes/traces + - name: Archive test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-reports + path: | + **/reports/**/* + outputs: + cache-key: ${{ steps.cache-key.outputs.cache-key }} + + test: + needs: build runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + java: [ 8.0.492-tem, 11.0.31-tem, 17.0.19-tem, 21.0.11-tem, 25.0.3-tem, 27.ea.20-open ] + env: + TEST_JAVA_HOME: "/home/runner/.sdkman/candidates/java/${{ matrix.java }}" steps: - - name: Set up Java 8 - uses: actions/setup-java@v1 + - name: Checkout + uses: actions/checkout@v6 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + - name: Prepare OS + run: | + sudo apt-get update + sudo apt-get install -y curl zip unzip + - name: Prepare JDK ${{ matrix.java_version }} + run: | + curl -s "https://get.sdkman.io" | bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + echo 'n' | sdk install java ${{ matrix.java }} + which java + echo 'y' | sdk install java 11.0.31-tem + - name: Cache Gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ needs.build.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Download build data + uses: actions/download-artifact@v8 + with: + name: dist-build + path: btrace-dist/build + - name: Download test trace data + uses: actions/download-artifact@v8 + with: + name: test-trace + path: btrace-agent/build/classes/traces + - name: Set up Java 24 + uses: actions/setup-java@v5 + with: + java-version: 24 + distribution: temurin + - name: Build btrace-agent jar + run: | + ./gradlew --no-daemon --parallel --build-cache :btrace-agent:jar + - name: Run tests + run: | + set +x + ./gradlew --no-daemon --parallel --build-cache -Pintegration -PCI :integration-tests:test + - name: Check Gradle cache size + run: du -sh ~/.gradle/caches + - name: Integration test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: integration-test-reports-${{ matrix.java }} + path: | + integration-tests/build/reports/**/* + - name: Archive binary artifacts + if: success() && matrix.java == '11' + uses: actions/upload-artifact@v7 + with: + name: btrace-dist + path: | + btrace-dist/build/distributions/**/btrace-*-bin*.tar.gz + + publish: + if: github.ref == 'refs/heads/develop' + needs: + - test + - build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 with: - java-version: 8 - - name: Store JAVA_8_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_8_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 9 - uses: actions/setup-java@v1 + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 with: - java-version: 9 - - name: Store JAVA_9_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_9_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 16 - uses: actions/setup-java@v1 + java-version: 11 + distribution: temurin + - name: Cache Gradle + uses: actions/cache@v5 with: - java-version: 16 - - name: Store JAVA_16_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_16_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 11 - uses: actions/setup-java@v1 + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ needs.build.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Download build data + uses: actions/download-artifact@v8 with: - java-version: 11 - - name: Store JAVA_11_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_11_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Checkout - uses: actions/checkout@v2 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build - run: JAVA_HOME="$JAVA_11_HOME" ./gradlew build -x test - - name: Run Tests - run: JAVA_HOME="$JAVA_11_HOME" ./run_tests.sh + name: dist-build - name: Deploy Maven - if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' - run: ./gradlew :btrace-dist:publish + run: ./gradlew -x test :btrace-dist:publish env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} GPG_SIGNING_PWD: ${{ secrets.GPG_SIGNING_PWD }} - BTRACE_SONATYPE_USER: ${{ secrets.BTRACE_SONATYPE_USER }} - BTRACE_SONATYPE_PWD: ${{ secrets.BTRACE_SONATYPE_PWD }} - - name: Archive binary artifacts - uses: actions/upload-artifact@v2 - with: - name: binary-dist - path: | - btrace-dist/build/distributions/**/btrace-*-bin*.tar.gz - - name: Archive reports - if: always() - uses: actions/upload-artifact@v2 - with: - name: reports - path: | - btrace-instr/build/reports/**/* + SONATYPE_USER: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + cleanup: + runs-on: ubuntu-latest + needs: publish + steps: + - name: Cleanup temporary artifacts + uses: geekyeggo/delete-artifact@v6 + with: + name: dist-build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12e4a0205..3d860c1df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,62 +1,1003 @@ -on: - push: - tags: - - 'v*' +# BTrace Release Workflow +# +# This workflow handles the complete release process: +# 1. Validates inputs and prerequisites +# 2. Runs full test suite (unit + integration tests) +# 3. Creates release branch and tag +# 4. Stages artifacts to Maven Central (requires manual release) +# 5. Waits for Maven Central availability (up to 30 minutes) +# 6. Creates GitHub release with artifacts +# 7. Updates SDKMan +# 8. Updates version numbers for next development cycle +# 9. Manages milestones +# +# IMPORTANT: Maven artifacts are STAGED, not released automatically. +# After staging, go to https://central.sonatype.com/publishing/deployments +# to review and release. The workflow will wait up to 30 minutes. +# +# Triggered by scripts/release.sh or manually via workflow_dispatch + +name: Release -name: Create Release +on: + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + type: choice + options: + - major + - minor + - patch + release_version: + description: 'Release version (e.g., 2.3.0)' + required: true + type: string + commit_sha: + description: 'Source commit SHA' + required: true + type: string + release_branch: + description: 'Release branch (e.g., release/2.3._)' + required: true + type: string + next_snapshot: + description: 'Next snapshot version (e.g., 2.4.0-SNAPSHOT)' + required: true + type: string + dry_run: + description: 'Dry run (skip publishing and tagging)' + required: false + type: boolean + default: false defaults: run: shell: bash +env: + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_TAG: v${{ inputs.release_version }} + RC_TAG: v${{ inputs.release_version }}_RC + jobs: - build: - name: Create Release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Branch name - id: branch_name - run: | - echo ::set-output name=SOURCE_NAME::${GITHUB_REF#refs/*/} - echo ::set-output name=SOURCE_BRANCH::${GITHUB_REF#refs/heads/} - echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/} - - name: Set up Java 8 - uses: actions/setup-java@v1 - with: - java-version: 8 - - name: Store JAVA_8_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_8_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 9 - uses: actions/setup-java@v1 - with: - java-version: 9 - - name: Store JAVA_9_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_9_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 16 - uses: actions/setup-java@v1 - with: - java-version: 16 - - name: Store JAVA_16_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_16_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Set up Java 11 - uses: actions/setup-java@v1 + # ============================================================ + # Job 1: Validate inputs and prerequisites + # ============================================================ + validate: + name: Validate Release + runs-on: ubuntu-latest + outputs: + branch_exists: ${{ steps.check-branch.outputs.exists }} + create_branch: ${{ steps.check-branch.outputs.create }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.commit_sha }} + fetch-depth: 0 + + - name: Validate inputs + run: | + echo "Validating release inputs..." + echo " Release Type: ${{ inputs.release_type }}" + echo " Release Version: ${{ inputs.release_version }}" + echo " Commit SHA: ${{ inputs.commit_sha }}" + echo " Release Branch: ${{ inputs.release_branch }}" + echo " Next Snapshot: ${{ inputs.next_snapshot }}" + echo " Dry Run: ${{ inputs.dry_run }}" + + # Validate version format (X.Y.Z) + if [[ ! "${{ inputs.release_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid version format. Expected X.Y.Z" + exit 1 + fi + + # Validate release type + if [[ ! "${{ inputs.release_type }}" =~ ^(major|minor|patch)$ ]]; then + echo "::error::Invalid release type. Expected major, minor, or patch" + exit 1 + fi + + # Validate commit exists + if ! git cat-file -e "${{ inputs.commit_sha }}^{commit}" 2>/dev/null; then + echo "::error::Commit ${{ inputs.commit_sha }} does not exist" + exit 1 + fi + + echo "All inputs validated successfully" + + - name: Check if tag exists + run: | + if git rev-parse "v${{ inputs.release_version }}" >/dev/null 2>&1; then + echo "::error::Tag v${{ inputs.release_version }} already exists" + exit 1 + fi + if git ls-remote --tags origin "refs/tags/v${{ inputs.release_version }}" | grep -q .; then + echo "::error::Tag v${{ inputs.release_version }} already exists on remote" + exit 1 + fi + # Check for leftover RC tag + if git rev-parse "${RC_TAG}" >/dev/null 2>&1; then + echo "::error::RC tag ${RC_TAG} already exists locally. Clean up with: git tag -d ${RC_TAG}" + exit 1 + fi + if git ls-remote --tags origin "refs/tags/${RC_TAG}" | grep -q .; then + echo "::error::RC tag ${RC_TAG} already exists on remote. Clean up with: git push origin :refs/tags/${RC_TAG}" + exit 1 + fi + echo "Tag does not exist - OK" + + - name: Check if release branch exists + id: check-branch + run: | + BRANCH="${{ inputs.release_branch }}" + if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH}"; then + echo "Release branch ${BRANCH} already exists" + echo "exists=true" >> $GITHUB_OUTPUT + echo "create=false" >> $GITHUB_OUTPUT + else + echo "Release branch ${BRANCH} will be created" + echo "exists=false" >> $GITHUB_OUTPUT + echo "create=true" >> $GITHUB_OUTPUT + fi + + # ============================================================ + # Job 2: Build and run unit tests + # ============================================================ + build-and-test: + name: Build and Test + needs: validate + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.commit_sha }} + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Generate cache key + id: cache-key + run: | + key="${{ runner.os }}-gradle-release-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" + echo "key=${key}" >> $GITHUB_OUTPUT + + - name: Cache Gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build + run: ./gradlew --no-daemon --parallel --build-cache build + + - name: Upload dist build data + uses: actions/upload-artifact@v7 + with: + name: dist-build + retention-days: 1 + path: | + btrace-dist/build + btrace-agent/build/classes/traces + + - name: Upload test trace data + uses: actions/upload-artifact@v7 + with: + name: test-trace + retention-days: 1 + path: | + btrace-agent/build/classes/traces + + - name: Archive test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: unit-test-reports + path: | + **/reports/**/* + + # ============================================================ + # Job 3: Integration tests on multiple JDK versions + # ============================================================ + integration-tests: + name: Integration Tests (JDK ${{ matrix.java }}) + needs: build-and-test + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + java: [8.0.492-tem, 11.0.31-tem, 17.0.19-tem, 21.0.11-tem] + env: + TEST_JAVA_HOME: "/home/runner/.sdkman/candidates/java/${{ matrix.java }}" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.commit_sha }} + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Prepare OS + run: | + sudo apt-get update + sudo apt-get install -y curl zip unzip + + - name: Prepare JDK ${{ matrix.java }} + run: | + curl -s "https://get.sdkman.io" | bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + echo 'n' | sdk install java ${{ matrix.java }} + which java + echo 'y' | sdk install java 11.0.31-tem + + - name: Cache Gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ needs.build-and-test.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Download build data + uses: actions/download-artifact@v8 + with: + name: dist-build + path: btrace-dist/build + + - name: Download test trace data + uses: actions/download-artifact@v8 + with: + name: test-trace + path: btrace-agent/build/classes/traces + + - name: Build btrace-agent jar + run: | + ./gradlew --no-daemon --parallel --build-cache :btrace-agent:jar + + - name: Run integration tests + run: | + ./gradlew --no-daemon --parallel --build-cache -Pintegration :integration-tests:test + + - name: Integration test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: integration-test-reports-${{ matrix.java }} + path: | + integration-tests/build/reports/**/* + + # ============================================================ + # Job 4: Prepare release (create branch, update version, tag) + # ============================================================ + prepare-release: + name: Prepare Release + needs: [validate, integration-tests] + runs-on: ubuntu-latest + outputs: + release_sha: ${{ steps.commit.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.commit_sha }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create or checkout release branch + run: | + BRANCH="${{ inputs.release_branch }}" + if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH}"; then + echo "Checking out existing branch ${BRANCH}" + git checkout -b "${BRANCH}" "origin/${BRANCH}" + else + echo "Creating new branch ${BRANCH}" + git checkout -b "${BRANCH}" + fi + + - name: Update version in build.gradle + run: | + VERSION="${{ inputs.release_version }}" + sed -i "s/version = '.*'/version = '${VERSION}'/" build.gradle + echo "Updated version to ${VERSION}" + grep "version = " build.gradle + + - name: Commit version change + id: commit + run: | + git add build.gradle + git commit -m "Release ${RELEASE_TAG}" + SHA=$(git rev-parse HEAD) + echo "sha=${SHA}" >> $GITHUB_OUTPUT + echo "Release commit: ${SHA}" + + - name: Create RC tag + if: ${{ inputs.dry_run != true }} + run: | + git tag -a "${RC_TAG}" -m "Release candidate ${RELEASE_TAG}" + echo "Created RC tag ${RC_TAG}" + + - name: Push branch and RC tag + if: ${{ inputs.dry_run != true }} + run: | + git push origin "${{ inputs.release_branch }}" + git push origin "${RC_TAG}" + echo "Pushed branch and RC tag" + + - name: Dry run summary + if: ${{ inputs.dry_run == true }} + run: | + echo "::warning::DRY RUN - Branch and tag NOT pushed" + echo "Would have pushed:" + echo " - Branch: ${{ inputs.release_branch }}" + echo " - RC Tag: ${RC_TAG}" + + # ============================================================ + # Job 5: Stage to Maven Central (does NOT release) + # ============================================================ + stage-maven: + name: Stage to Maven Central + needs: [prepare-release, build-and-test] + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout RC tag + uses: actions/checkout@v6 + with: + ref: ${{ env.RC_TAG }} + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Cache Gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ needs.build-and-test.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Stage to Maven Central + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PWD: ${{ secrets.GPG_SIGNING_PWD }} + SONATYPE_USER: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: | + echo "Staging artifacts to Central Portal..." + echo "After this job completes, go to https://central.sonatype.com/publishing/deployments" + echo "to review and release the staged artifacts." + ./gradlew :btrace-dist:publishAllPublicationsToSonatypeRepository --no-daemon + + - name: Staging complete + run: | + echo "==============================================" + echo "ARTIFACTS STAGED - MANUAL RELEASE REQUIRED" + echo "==============================================" + echo "" + echo "Staged artifacts:" + echo " - io.btrace:btrace-agent:${{ inputs.release_version }}" + echo " - io.btrace:btrace-client:${{ inputs.release_version }}" + echo " - io.btrace:btrace-boot:${{ inputs.release_version }}" + echo "" + echo "Next steps:" + echo " 1. Go to https://central.sonatype.com/publishing/deployments" + echo " 2. Review the staged repository" + echo " 3. Click 'Publish' to release to Maven Central" + echo "" + echo "The workflow will wait up to 30 minutes for artifacts to appear." + echo "If you don't want to proceed, let the workflow timeout or cancel it." + + # ============================================================ + # Job 5b: Wait for Maven Central availability + # ============================================================ + wait-for-maven: + name: Wait for Maven Central + needs: stage-maven + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - name: Wait for artifacts on Maven Central + run: | + VERSION="${{ inputs.release_version }}" + ARTIFACT_URL="https://repo1.maven.org/maven2/io/btrace/btrace-client/${VERSION}/btrace-client-${VERSION}.pom" + + echo "Waiting for Maven Central to have: ${ARTIFACT_URL}" + echo "This requires manual release from Central Portal." + echo "" + echo "Go to: https://central.sonatype.com/publishing/deployments" + echo "" + + MAX_ATTEMPTS=60 # 30 minutes with 30s intervals + ATTEMPT=0 + + while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + ATTEMPT=$((ATTEMPT + 1)) + echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: Checking Maven Central..." + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${ARTIFACT_URL}") + + if [ "$HTTP_STATUS" = "200" ]; then + echo "" + echo "SUCCESS: Artifacts are available on Maven Central!" + echo "Proceeding with GitHub release and SDKMan announcement..." + exit 0 + fi + + echo " Status: ${HTTP_STATUS} - Not available yet. Waiting 30s..." + sleep 30 + done + + echo "" + echo "TIMEOUT: Artifacts did not appear on Maven Central within 30 minutes." + echo "The release was NOT finalized." + echo "" + echo "To complete the release manually:" + echo " 1. Release artifacts via Central Portal" + echo " 2. Create GitHub release manually" + echo " 3. Run: ./gradlew :btrace-dist:sdkMinorRelease" + exit 1 + + # ============================================================ + # Job 5c: Update JBang catalog + # ============================================================ + update-jbang-catalog: + name: Update JBang Catalog + needs: [prepare-release, wait-for-maven] + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout jbang-catalog + continue-on-error: true + uses: actions/checkout@v6 + with: + repository: btraceio/jbang-catalog + token: ${{ secrets.JBANG_CATALOG_PAT || secrets.GITHUB_TOKEN }} + path: catalog + + - name: Update catalog files + continue-on-error: true + run: | + VERSION="${{ inputs.release_version }}" + + if [ ! -d catalog ]; then + echo "⚠️ Catalog checkout failed - skipping update" + echo "Add JBANG_CATALOG_PAT secret to enable automatic catalog updates" + exit 0 + fi + + cd catalog + + # Update btrace.java dependency + sed -i.bak "s|//DEPS io.btrace:btrace-client:.*|//DEPS io.btrace:btrace-client:${VERSION}|g" btrace.java + + # Update btrace-latest.java dependency + sed -i.bak "s|//DEPS io.btrace:btrace-client:.*|//DEPS io.btrace:btrace-client:${VERSION}|g" btrace-latest.java + + # Clean up backup files + rm -f *.bak + + # Show changes + git diff + + - name: Commit and push catalog updates + continue-on-error: true + run: | + VERSION="${{ inputs.release_version }}" + + if [ ! -d catalog ]; then + echo "⚠️ Catalog checkout failed - skipping update" + echo "Add JBANG_CATALOG_PAT secret to enable automatic catalog updates" + exit 0 + fi + + cd catalog + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add btrace.java btrace-latest.java + if git commit -m "Update btrace to version ${VERSION}"; then + git push || echo "⚠️ Push failed - update catalog manually at https://github.com/btraceio/jbang-catalog" + fi + + # ============================================================ + # Job 5d: Finalize release tag (RC -> final) + # ============================================================ + finalize-tag: + name: Finalize Release Tag + needs: [prepare-release, wait-for-maven] + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout at release SHA + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare-release.outputs.release_sha }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch tags + run: git fetch origin --tags + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create final tag + run: | + git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}" + echo "Created final tag ${RELEASE_TAG}" + + - name: Push final tag + run: | + git push origin "${RELEASE_TAG}" + echo "Pushed final tag ${RELEASE_TAG}" + + - name: Delete RC tag + run: | + git tag -d "${RC_TAG}" || true + git push origin ":refs/tags/${RC_TAG}" || true + echo "Deleted RC tag ${RC_TAG}" + + # ============================================================ + # Job 6: Build distribution packages + # ============================================================ + build-distributions: + name: Build Distributions + needs: prepare-release + runs-on: ubuntu-latest + steps: + - name: Checkout release commit + uses: actions/checkout@v6 + with: + ref: ${{ inputs.dry_run == true && inputs.commit_sha || env.RC_TAG }} + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 with: java-version: 11 - - name: Store JAVA_11_HOME - run: JAVA_PATH=$(which java) && echo "JAVA_11_HOME=${JAVA_PATH/\/bin\/java/\/}" >> $GITHUB_ENV - - name: Build artifacts - run: JAVA_HOME="$JAVA_11_HOME" ./gradlew :btrace-dist:build - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v1 + distribution: temurin + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Update version for dry run + if: ${{ inputs.dry_run == true }} + run: | + VERSION="${{ inputs.release_version }}" + sed -i "s/project.version = '.*'/project.version = '${VERSION}'/" common.gradle + + - name: Build distribution packages + run: | + ./gradlew :btrace-dist:build --no-daemon + + - name: List artifacts + run: | + echo "Distribution artifacts:" + ls -la btrace-dist/build/distributions/ + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v7 + with: + name: release-distributions + retention-days: 7 + path: | + btrace-dist/build/distributions/btrace-v${{ inputs.release_version }}-bin.tar.gz + btrace-dist/build/distributions/btrace-v${{ inputs.release_version }}-bin.zip + btrace-dist/build/distributions/btrace-v${{ inputs.release_version }}-sdkman-bin.zip + btrace-dist/build/distributions/*.deb + btrace-dist/build/distributions/*.rpm + + # ============================================================ + # Job 7: Create GitHub Release + # ============================================================ + create-github-release: + name: Create GitHub Release + needs: [build-distributions, finalize-tag] + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + + - name: Download distribution artifacts + uses: actions/download-artifact@v8 + with: + name: release-distributions + path: distributions + + - name: Download Maven artifact + continue-on-error: true + run: | + VERSION="${{ inputs.release_version }}" + BASE_URL="https://repo1.maven.org/maven2/io/btrace/btrace/${VERSION}" + mkdir -p maven-artifacts + MAX_ATTEMPTS=40 + for i in $(seq 1 $MAX_ATTEMPTS); do + if curl -fSL "${BASE_URL}/btrace-${VERSION}.jar" \ + -o maven-artifacts/btrace-${VERSION}.jar; then + echo "Downloaded btrace-${VERSION}.jar" + ls -la maven-artifacts/ + exit 0 + fi + echo "Attempt ${i}/${MAX_ATTEMPTS} failed, retrying in 30s..." + sleep 30 + done + echo "::warning::Failed to download Maven JAR after ${MAX_ATTEMPTS} attempts (~20 minutes). GitHub release will proceed without it." + + - name: Check for no-release-notes label + id: check-label + run: | + # Check if this release should skip auto-generated notes + SKIP_NOTES="false" + # This would check PRs merged since last release for the label + # For now, always generate release notes + echo "skip_notes=${SKIP_NOTES}" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - draft: true + tag_name: ${{ env.RELEASE_TAG }} + name: BTrace ${{ inputs.release_version }} + draft: false prerelease: false + generate_release_notes: true files: | - btrace-dist/build/distributions/btrace-${{ steps.branch_name.outputs.SOURCE_TAG }}-bin.tar.gz - btrace-dist/build/distributions/btrace-${{ steps.branch_name.outputs.SOURCE_TAG }}-bin.zip - btrace-dist/build/distributions/btrace-${{ steps.branch_name.outputs.SOURCE_TAG }}-sdkman-bin.zip \ No newline at end of file + distributions/btrace-v${{ inputs.release_version }}-bin.tar.gz + distributions/btrace-v${{ inputs.release_version }}-bin.zip + distributions/btrace-v${{ inputs.release_version }}-sdkman-bin.zip + distributions/*.deb + distributions/*.rpm + maven-artifacts/* + + # ============================================================ + # Job 8: Update SDKMan + # ============================================================ + update-sdkman: + name: Update SDKMan + needs: create-github-release + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout release tag + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Announce to SDKMan + env: + SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} + SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} + run: | + ./gradlew :btrace-dist:sdkMajorRelease --no-daemon + + # ============================================================ + # Job 9: Update develop branch (major/minor only) + # ============================================================ + update-develop: + name: Update Develop Branch + needs: prepare-release + runs-on: ubuntu-latest + if: ${{ inputs.release_type != 'patch' && inputs.dry_run != true }} + steps: + - name: Checkout develop + uses: actions/checkout@v6 + with: + ref: develop + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update version in develop + run: | + NEXT_VERSION="${{ inputs.next_snapshot }}" + sed -i "s/project.version = '.*'/project.version = '${NEXT_VERSION}'/" common.gradle + echo "Updated develop to ${NEXT_VERSION}" + grep "project.version" common.gradle + + - name: Commit and push + run: | + git add common.gradle + git commit -m "Bump version to ${{ inputs.next_snapshot }} after ${RELEASE_TAG} release" + git push origin develop + + # ============================================================ + # Job 10: Update release branch to next patch snapshot + # ============================================================ + update-release-branch: + name: Update Release Branch + needs: [prepare-release, create-github-release] + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout release branch + uses: actions/checkout@v6 + with: + ref: ${{ inputs.release_branch }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Calculate next patch snapshot + id: next-version + run: | + CURRENT="${{ inputs.release_version }}" + MAJOR=$(echo $CURRENT | cut -d. -f1) + MINOR=$(echo $CURRENT | cut -d. -f2) + PATCH=$(echo $CURRENT | cut -d. -f3) + NEXT_PATCH=$((PATCH + 1)) + NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-SNAPSHOT" + echo "version=${NEXT_VERSION}" >> $GITHUB_OUTPUT + echo "Next patch version: ${NEXT_VERSION}" + + - name: Update version in release branch + run: | + NEXT_VERSION="${{ steps.next-version.outputs.version }}" + sed -i "s/project.version = '.*'/project.version = '${NEXT_VERSION}'/" common.gradle + echo "Updated release branch to ${NEXT_VERSION}" + grep "project.version" common.gradle + + - name: Commit and push + run: | + git add common.gradle + git commit -m "Bump version to ${{ steps.next-version.outputs.version }} for next patch release" + git push origin ${{ inputs.release_branch }} + + # ============================================================ + # Job 11: Manage milestones + # ============================================================ + update-milestones: + name: Update Milestones + needs: create-github-release + runs-on: ubuntu-latest + if: ${{ inputs.dry_run != true }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Create and close milestone + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + MILESTONE_TITLE="${{ inputs.release_version }}" + + # Check if milestone exists + MILESTONE=$(gh api repos/${{ github.repository }}/milestones --jq ".[] | select(.title == \"${MILESTONE_TITLE}\")") + + if [[ -z "${MILESTONE}" ]]; then + echo "Creating milestone ${MILESTONE_TITLE}..." + gh api repos/${{ github.repository }}/milestones \ + -X POST \ + -f title="${MILESTONE_TITLE}" \ + -f state="closed" \ + -f description="Released as ${RELEASE_TAG}" + else + MILESTONE_NUMBER=$(echo "${MILESTONE}" | jq -r '.number') + echo "Closing existing milestone ${MILESTONE_TITLE} (#${MILESTONE_NUMBER})..." + gh api repos/${{ github.repository }}/milestones/${MILESTONE_NUMBER} \ + -X PATCH \ + -f state="closed" + fi + + - name: Associate merged PRs with milestone + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get the milestone number + MILESTONE_TITLE="${{ inputs.release_version }}" + MILESTONE_NUMBER=$(gh api repos/${{ github.repository }}/milestones --jq ".[] | select(.title == \"${MILESTONE_TITLE}\") | .number") + + if [[ -z "${MILESTONE_NUMBER}" ]]; then + echo "Warning: Could not find milestone ${MILESTONE_TITLE}" + exit 0 + fi + + # Find the previous release tag + PREV_TAG=$(git describe --tags --abbrev=0 "${RELEASE_TAG}^" 2>/dev/null || echo "") + + if [[ -z "${PREV_TAG}" ]]; then + echo "No previous tag found, skipping PR association" + exit 0 + fi + + echo "Finding PRs merged between ${PREV_TAG} and ${RELEASE_TAG}..." + + # Get commits between tags + COMMITS=$(git log "${PREV_TAG}..${RELEASE_TAG}" --pretty=format:"%H" 2>/dev/null || echo "") + + for COMMIT in ${COMMITS}; do + # Find PR associated with this commit + PR_NUMBER=$(gh api repos/${{ github.repository }}/commits/${COMMIT}/pulls --jq '.[0].number' 2>/dev/null || echo "") + + if [[ -n "${PR_NUMBER}" && "${PR_NUMBER}" != "null" ]]; then + echo "Associating PR #${PR_NUMBER} with milestone ${MILESTONE_TITLE}..." + gh api repos/${{ github.repository }}/issues/${PR_NUMBER} \ + -X PATCH \ + -f milestone="${MILESTONE_NUMBER}" 2>/dev/null || true + fi + done + + # ============================================================ + # Job 12: Release summary + # ============================================================ + summary: + name: Release Summary + needs: [create-github-release, update-sdkman, update-milestones, update-jbang-catalog, finalize-tag] + runs-on: ubuntu-latest + if: always() + steps: + - name: Generate summary + run: | + echo "# Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Release Details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | ${{ inputs.release_version }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | ${RELEASE_TAG} |" >> $GITHUB_STEP_SUMMARY + echo "| Release Type | ${{ inputs.release_type }} |" >> $GITHUB_STEP_SUMMARY + echo "| Source Commit | ${{ inputs.commit_sha }} |" >> $GITHUB_STEP_SUMMARY + echo "| Release Branch | ${{ inputs.release_branch }} |" >> $GITHUB_STEP_SUMMARY + echo "| Dry Run | ${{ inputs.dry_run }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.dry_run }}" != "true" ]]; then + echo "## Links" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${RELEASE_TAG})" >> $GITHUB_STEP_SUMMARY + echo "- [Maven Central](https://central.sonatype.com/search?q=io.btrace&version=${{ inputs.release_version }})" >> $GITHUB_STEP_SUMMARY + echo "- [SDKMan](https://sdkman.io/sdks#btrace)" >> $GITHUB_STEP_SUMMARY + echo "- JBang: \`jbang btrace@${{ inputs.release_version }}\` (uses Maven Central)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Maven Coordinates" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```xml' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo ' io.btrace' >> $GITHUB_STEP_SUMMARY + echo ' btrace-client' >> $GITHUB_STEP_SUMMARY + echo " ${{ inputs.release_version }}" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "## Dry Run Mode" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "> **Note:** This was a dry run. No artifacts were published or tags created." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Job Status" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Validate | ${{ needs.validate.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Build & Test | ${{ needs.build-and-test.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Integration Tests | ${{ needs.integration-tests.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Prepare Release | ${{ needs.prepare-release.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Stage Maven | ${{ needs.stage-maven.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Wait for Maven | ${{ needs.wait-for-maven.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Finalize Tag | ${{ needs.finalize-tag.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| GitHub Release | ${{ needs.create-github-release.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| SDKMan | ${{ needs.update-sdkman.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| JBang Catalog | ${{ needs.update-jbang-catalog.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Milestones | ${{ needs.update-milestones.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + + # ============================================================ + # Job 13: Cleanup + # ============================================================ + cleanup: + name: Cleanup Artifacts + needs: [summary] + runs-on: ubuntu-latest + if: always() + steps: + - name: Cleanup temporary artifacts + uses: geekyeggo/delete-artifact@v6 + with: + name: | + dist-build + test-trace diff --git a/.github/workflows/sdkman-set-default.yml b/.github/workflows/sdkman-set-default.yml new file mode 100644 index 000000000..5d6d5f3e6 --- /dev/null +++ b/.github/workflows/sdkman-set-default.yml @@ -0,0 +1,53 @@ +# .github/workflows/sdkman-set-default.yml +name: Set SDKMan Default Version + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to set as SDKMan default (e.g., 2.2.6)' + required: true + type: string + +defaults: + run: + shell: bash + +jobs: + set-default: + name: Set SDKMan Default to ${{ inputs.version }} + runs-on: ubuntu-latest + steps: + - name: Checkout tag + uses: actions/checkout@v6 + with: + ref: v${{ inputs.version }} + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up Java + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Set SDKMan default version + env: + SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} + SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} + run: | + echo "Setting SDKMan default for btrace to ${{ inputs.version }}..." + ./gradlew :btrace-dist:sdkDefaultVersion --no-daemon + echo "Done. 'sdk install btrace' will now install ${{ inputs.version }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b7234b125..b371707e8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Stale issue message' diff --git a/.github/workflows/update-jdk-versions.yml b/.github/workflows/update-jdk-versions.yml new file mode 100644 index 000000000..f41e84aa7 --- /dev/null +++ b/.github/workflows/update-jdk-versions.yml @@ -0,0 +1,52 @@ +name: Update JDK Test Versions + +on: + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-versions: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: develop + token: ${{ secrets.PAT_WORKFLOW }} + + - name: Check for version updates + id: update + run: | + chmod +x scripts/update-jdk-versions.sh + if changes=$(scripts/update-jdk-versions.sh); then + echo "changed=true" >> "$GITHUB_OUTPUT" + { + echo "body<> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request + if: steps.update.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.PAT_WORKFLOW }} + branch: automation/update-jdk-versions + base: develop + title: 'chore: update JDK test versions' + draft: true + labels: automation + commit-message: 'chore: update JDK test versions' + body: | + Automated JDK version update detected by SDKMan API. + + ### Changes + ${{ steps.update.outputs.body }} diff --git a/.github/workflows/v2-protocol-tests.yml b/.github/workflows/v2-protocol-tests.yml new file mode 100644 index 000000000..c1640b79f --- /dev/null +++ b/.github/workflows/v2-protocol-tests.yml @@ -0,0 +1,383 @@ +# Workflow for testing BTrace Binary Protocol v2 +# This workflow runs comprehensive tests for the v2 protocol implementation +# including unit tests, JMH benchmarks, and protocol negotiation tests + +name: V2 Protocol Tests + +on: + # Run on PRs when labeled with 'test:v2-protocol' + pull_request: + types: [ labeled ] + # Run weekly on Sunday at 2 AM UTC + schedule: + - cron: '0 2 * * 0' + # Allow manual trigger + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + unit-tests: + name: V2 Protocol Unit Tests + runs-on: ubuntu-latest + # Only run if triggered by schedule, manual dispatch, or PR with 'test:v2-protocol' label + if: | + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test:v2-protocol')) + strategy: + matrix: + java: [ 11, 17, 21 ] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-${{ matrix.java }} + + - name: Set up JDK ${{ matrix.java }} + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: ${{ matrix.java }} + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Cache Gradle packages + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Run V2 Protocol Tests + run: | + ./gradlew :btrace-core:test --tests "io.btrace.core.comm.v2.*" \ + --tests "io.btrace.core.comm.Protocol*" \ + --tests "io.btrace.core.comm.WireProtocol*" \ + --rerun-tasks + + - name: Upload Test Reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-reports-java-${{ matrix.java }} + retention-days: 7 + path: | + btrace-core/build/reports/tests/** + btrace-core/build/test-results/** + + protocol-negotiation-tests: + name: Protocol Negotiation Tests + runs-on: ubuntu-latest + if: | + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test:v2-protocol')) + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up JDK 11 + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run Protocol Negotiation Tests + run: | + ./gradlew :btrace-core:test --tests "org.openjdk.btrace.core.comm.ProtocolNegotiatorTest" \ + --tests "org.openjdk.btrace.core.comm.ProtocolConfigTest" \ + --tests "org.openjdk.btrace.core.comm.WireProtocolTest" \ + --rerun-tasks + + - name: Verify Protocol Version Detection + run: | + echo "Testing V1 protocol detection..." + ./gradlew :btrace-core:test --tests "*ProtocolNegotiatorTest.testNegotiateV1*" + echo "Testing V2 protocol detection..." + ./gradlew :btrace-core:test --tests "*ProtocolNegotiatorTest.testNegotiateV2*" + + - name: Upload Negotiation Test Reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: negotiation-test-reports + retention-days: 7 + path: | + btrace-core/build/reports/tests/** + btrace-core/build/test-results/** + + edge-case-tests: + name: Edge Case and Boundary Tests + runs-on: ubuntu-latest + if: | + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test:v2-protocol')) + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up JDK 11 + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run Edge Case Tests + run: | + ./gradlew :btrace-core:test --tests "org.openjdk.btrace.core.comm.v2.BinaryProtocolEdgeCasesTest" \ + --rerun-tasks + + - name: Test Large Messages + run: | + ./gradlew :btrace-core:test --tests "*testVeryLargeMessage" \ + --tests "*testLargeBytecodeArray" \ + --tests "*testMapWith1000Entries" + + - name: Test Compression + run: | + ./gradlew :btrace-core:test --tests "*testCompressionThreshold" \ + --tests "*testHighlyCompressibleMessage" \ + --tests "*testCompressionJustAboveThreshold" + + - name: Upload Edge Case Reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: edge-case-reports + retention-days: 7 + path: | + btrace-core/build/reports/tests/** + + jmh-benchmarks: + name: JMH Performance Benchmarks + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[benchmark]') + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up JDK 11 + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run Quick JMH Benchmarks (warmup=1, iterations=2, fork=1) + run: | + ./gradlew :btrace-core:jmh \ + -PjmhInclude=".*Serialize.*" \ + -Pjmh.warmupIterations=1 \ + -Pjmh.iterations=2 \ + -Pjmh.fork=1 + + - name: Upload JMH Results + if: always() + uses: actions/upload-artifact@v7 + with: + name: jmh-results + retention-days: 30 + path: | + btrace-core/build/reports/jmh/** + btrace-core/build/jmh-results/** + + protocol-compatibility: + name: Protocol Compatibility Matrix + runs-on: ubuntu-latest + if: | + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test:v2-protocol')) + strategy: + matrix: + scenario: + - name: "V2-to-V2" + client: "v2" + agent: "v2" + - name: "V1-to-V1" + client: "v1" + agent: "v1" + - name: "V2-to-V1" + client: "v2" + agent: "v1" + - name: "V1-to-V2" + client: "v1" + agent: "v2" + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up JDK 11 + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Test ${{ matrix.scenario.name }} Compatibility + run: | + echo "Testing compatibility: Client=${{ matrix.scenario.client }} Agent=${{ matrix.scenario.agent }}" + ./gradlew :btrace-core:test --tests "org.openjdk.btrace.core.comm.WireProtocolTest" \ + --rerun-tasks \ + -Dbtrace.comm.protocol=${{ matrix.scenario.client }} + + - name: Upload Compatibility Reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: compatibility-${{ matrix.scenario.name }} + retention-days: 7 + path: | + btrace-core/build/reports/tests/** + + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [unit-tests, protocol-negotiation-tests, edge-case-tests, protocol-compatibility] + if: always() + steps: + - name: Download All Test Reports + uses: actions/download-artifact@v8 + with: + path: test-reports + + - name: Generate Test Summary + run: | + echo "# V2 Protocol Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count test results + total_tests=0 + passed_tests=0 + failed_tests=0 + + for report in test-reports/*/test-results/test/*.xml; do + if [ -f "$report" ]; then + tests=$(grep -oP 'tests="\K[0-9]+' "$report" || echo "0") + failures=$(grep -oP 'failures="\K[0-9]+' "$report" || echo "0") + total_tests=$((total_tests + tests)) + failed_tests=$((failed_tests + failures)) + fi + done + + passed_tests=$((total_tests - failed_tests)) + + echo "- **Total Tests:** $total_tests" >> $GITHUB_STEP_SUMMARY + echo "- **Passed:** ✅ $passed_tests" >> $GITHUB_STEP_SUMMARY + echo "- **Failed:** ❌ $failed_tests" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ $failed_tests -eq 0 ]; then + echo "✅ All V2 protocol tests passed!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "❌ Some tests failed. Please review the reports." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + code-coverage: + name: Code Coverage + runs-on: ubuntu-latest + if: | + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test:v2-protocol')) + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache Java binaries + id: cache-java + uses: actions/cache@v5 + with: + path: ${{ runner.tool_cache }}/Java_* + key: java-${{ runner.os }}-temurin-11 + + - name: Set up JDK 11 + if: steps.cache-java.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run Tests with Coverage + run: | + ./gradlew :btrace-core:test jacocoTestReport + + - name: Upload Coverage Reports + uses: actions/upload-artifact@v7 + with: + name: coverage-reports + retention-days: 30 + path: | + btrace-core/build/reports/jacoco/** + btrace-core/build/jacoco/** + + - name: Generate Coverage Summary + run: | + if [ -f "btrace-core/build/reports/jacoco/test/html/index.html" ]; then + echo "## Code Coverage" >> $GITHUB_STEP_SUMMARY + echo "Coverage report generated successfully" >> $GITHUB_STEP_SUMMARY + echo "See artifacts for detailed coverage data" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/validate-renovate-config.yml b/.github/workflows/validate-renovate-config.yml new file mode 100644 index 000000000..6c4b6b4cd --- /dev/null +++ b/.github/workflows/validate-renovate-config.yml @@ -0,0 +1,23 @@ +name: Validate Renovate Config + +on: + push: + branches: [ develop ] + paths: + - renovate.json + pull_request: + branches: [ develop ] + paths: + - renovate.json + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Validate renovate.json + run: npx --yes --package renovate@39 -- renovate-config-validator diff --git a/.gitignore b/.gitignore index bdbe80366..2d733333f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,14 +8,17 @@ *.war *.ear +# Un-ignore specific files +!btrace-agent/src/test/resources/packed/test-pack.jar + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* build dist -docs make/netbeans/nbproject/private TEST-* +!btrace-agent/src/test/resources/packed/test-pack.jar make/junit* make/private.properties btrace-benchmark/target @@ -25,6 +28,7 @@ junit* /btrace-statsd/target/ *.iml .gradle +.gradle-user .idea !/lib/btrace-asm-*.jar !/lib/btrace-jctools-core-*.jar @@ -39,3 +43,5 @@ gradle.properties gradle-wrapper.properties /.java.versions +compile.log +.worktrees/ diff --git a/.muse/advisor.md b/.muse/advisor.md new file mode 100644 index 000000000..7f7b5e54a --- /dev/null +++ b/.muse/advisor.md @@ -0,0 +1,144 @@ +--- +name: btrace-advisor +role: repo_advisor +scope: [btraceio/btrace] +confidence: high +concertato_only: true +watched_paths: + - AGENTS.md + - CONTRIBUTING.md + - README.md + - build.gradle + - settings.gradle + - .github/workflows/continuous.yml + - .github/workflows/v2-protocol-tests.yml + - .github/workflows/release.yml + - .github/workflows/codeql-analysis.yml +--- + +# BTrace Repo Advisor + +Operational facts for `btraceio/btrace`. No reviewer opinions — only repo-specific truths for build, test, CI, and layout. + +## Branch Model + +- `develop` is the sole integration branch — ALL pull requests target `develop`. +- `master` does not exist on the remote. +- Release tags are cut from `develop` after a stabilization window. +- Branch from `develop` for features/fixes (see `CONTRIBUTING.md`). + +## Build System + +- Build tool: Gradle with the bundled wrapper (`./gradlew`). Pinned Gradle version downloaded on first run. +- Root toolchain: JDK 11 compiles the build; `btrace-core` and `btrace-runtime` sources target Java 8. +- Included build: `btrace-gradle-plugin` is consumed via `pluginManagement { includeBuild(...) }`. +- Subprojects: auto-discovered by root `build.gradle`/`settings.gradle` matching `btrace-*` directories that contain `build.gradle`. Skipped: `btrace-gradle-plugin` (handled above), and legacy `btrace-services`, `btrace-services-api`, `btrace-statsd`. +- Dependency versions live in `settings.gradle` `dependencyResolutionManagement.versionCatalogs.libs`: ASM 9.9.1, JUnit 5.11.4, SLF4J 1.7.36, JCTools 4.0.6, JMH 1.37, testcontainers 2.0.4. +- Do **not** consume Gradle task output directly — write logs to a file, filter with `grep`, then read the filtered file (AGENTS.md directive). + +### Commands + +```bash +./gradlew build # compile + unit tests for all modules +./gradlew :btrace-dist:build # build distribution (ZIP/TGZ/RPM/DEB + exploded layout) +./gradlew :btrace-dist:btraceJar # just rebuild the masked jar +./gradlew ::build -x test # faster module build +./gradlew :btrace-instr:test # instrumentation-only tests +./gradlew :btrace-instr:test -PupdateTestData # regenerate instrumentor goldens +./gradlew :integration-tests:test -Pintegration # requires built dist first +./gradlew spotlessCheck | spotlessApply # Google Java Format (Spotless) +./gradlew jacocoTestReport # coverage (CI uploads to Codecov) +``` + +### Environment variables + +- `JAVA_HOME` — build JDK (typically 21 locally; CI build row is 11). +- `TEST_JAVA_HOME` — JDK used to **run** tests; CI exports this per matrix row. For integration tests you usually want `TEST_JAVA_HOME=$JAVA_11_HOME`. Locally, setting it is the way to exercise Java 8 / 17 / 21 / 25 / 26-ea runtime paths. +- `BTRACE_TEST_DEBUG=true` — verbose integration-test output. +- `BTRACE_HOME` — optional; used when running an exploded dist (e.g. `btrace-dist/build/resources/main/v/`). +- `BTRACE_PERMS` / `-Dbtrace.permissions=` — privileged-extensions policy for integration tests (see `CONTRIBUTING.md`). +- `GRADLE_USER_HOME=$(pwd)/.gradle-user` — recommended in restricted/CI environments to avoid permission issues with the shared cache. +- `JAVA_TOOL_OPTIONS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"` — force IPv4 when wildcard-IP detection fails in Gradle. +- Sonatype publishing: `SONATYPE_USER` / `SONATYPE_USERNAME`, `SONATYPE_PASSWORD`, `GPG_SIGNING_KEY`, `GPG_SIGNING_PWD` (release workflow only). + +## Test Infrastructure + +- Framework: **JUnit 5 (Jupiter)** for both unit and integration tests. +- Unit tests: under each module's `src/test/java`, naming convention `*Test`. +- Integration tests: `integration-tests/src/test/java` (scripts under `integration-tests/src/test/btrace`) — spawn real JVMs, exercise agent + extensions, require Docker (`DOCKER_HOST=unix:///var/run/docker.sock`) and `btrace-dist/build/resources/main/v/libs/btrace.jar` on disk. +- Golden instrumentor fixtures: `btrace-instr/src/test/resources/instrumentorTestData/dynamic/` — regenerate with `-PupdateTestData` after any intentional bytecode change; commit the updated goldens. +- V2 protocol tests: `btrace-core` test class globs `io.btrace.core.comm.v2.*`, `Protocol*`, `WireProtocol*`. Dedicated workflow runs on label `test:v2-protocol`, weekly schedule, or manual dispatch. +- Running a single test: `./gradlew :btrace-instr:test --tests "io.btrace.runtime.ExtensionIndyShimIndexTest.resolvesNoopShimFromIndex"`. +- Per-module aliases from `CONTRIBUTING.md`: `:btrace-runtime:test`, `:btrace-extension:test`, `:btrace-compiler:test`, `:btrace-instr:test`. +- CI JDK matrix (build: JDK 11): tests run on `8.0.482-tem, 11.0.30-tem, 17.0.18-tem, 21.0.10-tem, 25.0.2-tem, 26.ea.35-open` — changes must compile and pass across all of these. + +## Distribution — Masked JAR + +- Single artifact: `btrace.jar` with bootstrap classes as `.class` and mode-scoped classes as `.classdata` under `META-INF/btrace/{agent,client,shared}/`. +- Loader: `io.btrace.boot.Loader` is `Main-Class`, `Premain-Class`, and `Agent-Class`. `MaskedClassLoader` resolves `.classdata`. +- Every new class **must** be categorized: agent-only, client-only, or shared. Categorization is wired into `btrace-dist/build.gradle`'s `prepareAgentClassdata` / `prepareClientClassdata` / `prepareSharedClassdata` include patterns. +- Rebuild masked jar after any restructuring: `./gradlew clean :btrace-dist:btraceJar`. +- Inspect: `unzip -l btrace.jar | grep -E '(\.class|\.classdata)'`; `unzip -p btrace.jar META-INF/MANIFEST.MF`. +- Build-order dependency: `allClassesShadow` must complete before `prepare*Classdata` tasks run; those must complete before `btraceJar` assembles. + +## Known Sharp Edges + +- **Javadoc API version trap**: `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/auxiliary/` is compiled at Java 8 source/target. Javadoc linking to JDK 15+ API (e.g. hidden-class lookups) breaks `:btrace-runtime:javadoc` with `reference not found`. Keep javadoc references on pre-9 API or use prose. +- **Bootstrap classloader constraints**: `btrace-boot.jar` lives on the bootstrap classpath. Any class referenced from an `INVOKEDYNAMIC` bootstrap method (e.g. `IndyDispatcher`) must be bootstrap-loadable. Pulling in application-classloader-only types breaks probe dispatch. +- **Hidden-class vs `defineClass` paths**: probe script classes are defined in the bootstrap CL via `Unsafe.defineClass` with `mustBeBootstrap=true` when `isTransforming()`. JDK 15+ has a separate hidden-class codepath — multi-version runtime (`src/main/java9/`, `src/main/java11/`, `src/main/java15/`) keeps these isolated; compile each at its release level. +- **Verifier contract**: probe handler methods are always `public static void`. `MethodHandles.publicLookup().findStatic(...)` is sufficient — do not switch to `privateLookupIn` "just to be safe," it breaks bootstrap loader expectations. +- **FQN rule**: never inline fully qualified names in source. Always import and use simple type names (AGENTS.md hard rule). +- **Masked-JAR class invisibility**: `ClassNotFoundException` on `.classdata` means the class is either missing from the correct mode section or was relocated but not registered. `shared/` is required whenever agent ↔ client serialize the same type (comm protocol, annotations, ASM core). +- **`BTraceRuntimeImpl_8` diverges** from 9/11 impls on the `defineClass` path — Java 8 CI row is not redundant; always verify against it via `TEST_JAVA_HOME=$JAVA_8_HOME` locally before merging runtime changes. +- **Spotless import order**: `java`, `javax`, `io.btrace`, everything else, static `io.btrace`, remaining static. Reformat with `./gradlew spotlessApply` if `spotlessCheck` fails. +- **License header**: Spotless injects `/* (C) $YEAR */` on every Java file; do not hand-write headers. + +## Known Recurring CI Failure Modes + +- **`:initializeSonatypeStagingRepository FAILED — Failed to find staging profile for package group`** on the `publish` job of BTrace CI/CD when `develop` merges. Tied to the OSSRH → Central Portal migration (`nexusPublishing` in root `build.gradle` uses `https://ossrh-staging-api.central.sonatype.com/`). Publish is guarded by `hasSonatypeCredentials` + non-snapshot version; failures typically reflect missing/rotated Central Portal user-token credentials or an unregistered `io.github.*` package group on the new portal. +- **`btrace-runtime:javadoc FAILED — reference not found`** — see Javadoc API version trap above. Caught by the build job (not the test matrix). +- **CodeQL `configuration error`** — CodeQL workflow trips on feature branches with unusual Java toolchain setups; typically a Java setup/classpath mismatch in `actions/codeql-action@v*`, not a real finding. +- **Integration test branch-scoped failures** (e.g. `ManifestLibsTests > Dynamic attach with manifest-libs enabled`) — matrix-wide failure usually means the feature branch changed something in extension-loading or masked-jar layout that works for default attach but not dynamic attach. Not a flaky test — debug the change, don't retry. + +## PR and CI Rules + +- **Commit style**: Conventional Commits (`feat(core):`, `fix(instr):`, `refactor(runtime):`, `test:`, `chore:`). Scope matches module short name. +- **OCA required**: PRs are only accepted from signers of the Oracle Contributor Agreement. +- **PR checklist** (from AGENTS.md + CONTRIBUTING.md): + 1. Target branch is `develop` (never `master`). + 2. `./gradlew spotlessCheck` clean. + 3. Unit tests updated/added. + 4. `-PupdateTestData` goldens regenerated **and committed** if instrumentation changed. + 5. Integration tests pass locally if agent/dist behavior changed. + 6. For behavior changes, include before/after notes or logs. + 7. No unrelated changes in the diff. +- **CI workflows**: + - `BTrace CI/CD` (`continuous.yml`) — build + matrix test + (on `develop`) publish + cleanup. Triggered on push/PR against `develop` and `workflow_dispatch`. + - `V2 Protocol Tests` (`v2-protocol-tests.yml`) — opt-in via PR label `test:v2-protocol`, weekly on Sundays 02:00 UTC, or manual. Includes unit, negotiation, edge-case, JMH (label `[benchmark]` or manual), and V1↔V2 compatibility matrix. + - `CodeQL` (`codeql-analysis.yml`) — static analysis on push/PR. + - `release.yml` — full release pipeline (manual dispatch). + - `update-jdk-versions.yml` — periodic JDK matrix refresh. + - `stale.yml` — issue/PR staleness sweeper. +- **Optional labels** with CI meaning: `test:v2-protocol` (run the V2 suite), `[benchmark]` in commit message (run JMH quick benchmarks). + +## File Layout + +| Path | Purpose | +|---|---| +| `btrace-core` | Annotations, wire protocol (v1 + v2), shared types — Java 8 compatible. | +| `btrace-compiler` | Script compilation + safety verification (`Verifier`). | +| `btrace-instr` | ASM-based bytecode instrumentation; probe factory; `HandlerRepositoryImpl`; golden-file tests. | +| `btrace-runtime` | Multi-version runtime impls (base, `java9/`, `java11/`, `java15/`); `IndyDispatcher`; `BTraceRuntimeImpl_*`. Must stay loadable from the bootstrap classloader. | +| `btrace-agent` | Java agent entry point; `RemoteClient`, `FileClient`; JFR hooks. | +| `btrace-boot` | Bootstrap classes (visible to JVM), including `Loader`, `MaskedClassLoader`, `MaskedJarUtils`. | +| `btrace-client` | CLI client — attach, send script, stream output. | +| `btrace-compiler` | Scripts → bytecode. | +| `btrace-dist` | Distribution packaging (masked jar, RPM/DEB/ZIP/TGZ). Hosts `prepareAgentClassdata` / `prepareClientClassdata` / `prepareSharedClassdata` / `btraceJar`. | +| `btrace-extension` / `btrace-extensions/*` | Extension API + bundled implementations (`btrace-metrics`, `btrace-statsd`, `btrace-utils`) and examples. Explicit includes in `settings.gradle`. | +| `btrace-gradle-plugin` | In-repo Gradle plugin for extension authorship — consumed via `includeBuild` in `pluginManagement`, not as a subproject. | +| `integration-tests` | Docker-based end-to-end tests. | +| `benchmarks/agent-benchmark`, `benchmarks/runtime-benchmarks` | JMH benchmarks. | +| `docs/` | User docs (tutorials, oneliner guide, extension dev guide). | +| `btrace-instr/src/test/resources/instrumentorTestData/dynamic/` | Instrumentor golden files — regenerate via `-PupdateTestData`. | +| `.github/workflows/` | CI config; `README.md` in the same directory documents the workflows. | +| `.muse/advisor.md` | This file. | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..69ff2e777 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,258 @@ +# Repository Guidelines + +## Project Structure & Modules +- Root uses Gradle with multiple modules named `btrace-*`. +- Core code lives in module directories (for example, `btrace-core`, `btrace-agent`, `btrace-runtime`, `btrace-client`, `btrace-instr`). +- Distributions are built from `btrace-dist`. +- Integration tests live in `integration-tests`; benchmarks in `benchmarks/*`; docs in `docs/`. + +## Architecture Overview +- btrace-agent: Attachable Java agent that installs a class transformer and manages script lifecycle (load/unload), output routing, and optional JFR hooks. +- btrace-compiler: Verifies and compiles BTrace scripts to bytecode. +- btrace-instr: ASM-based instrumentation and weaving utilities used by the agent/compiler. +- btrace-runtime: APIs exposed to scripts; provides safe helpers for printing, timers, and data collection. +- btrace-client: CLI/attach tooling that sends compiled scripts to the target JVM and streams results. +- extensions: API + implementations packaged as BTrace extensions (for example, statsd and metrics under `btrace-extensions/*`). +- Flow: client attaches → compiles/sends script → agent loads and instruments target classes → runtime emits events → client displays/exports. + +### High-Level Flow +``` + +--------------+ attach/send +-------------+ transform +------------------+ + | btrace-client| -----------------> | btrace-agent| --------------> | instrumented JVM | + +--------------+ +-------------+ +------------------+ + ^ | ^ | + | events/logs/stdout | | load/unload scripts | + | <------------------------------------+ +-------------------------------+ + | + +--------- optional exporters via services (eg. statsd) --------------------> +``` + +### Modules (at a glance) +``` + btrace-client -> btrace-agent -> btrace-instr + | | + v v + btrace-runtime extensions (e.g., statsd, utils, metrics) + + btrace-compiler (validates/compiles scripts) + btrace-dist (packages binaries) +``` + +## Distribution Architecture: Masked JAR + +BTrace uses a **single masked JAR** (`btrace.jar`) as its distribution artifact. This JAR contains: + +### Structure +``` +btrace.jar +├── META-INF/ +│ ├── MANIFEST.MF (Main-Class, Premain-Class, Agent-Class → io.btrace.boot.Loader) +│ ├── btrace/ +│ │ ├── agent/*.classdata (agent classes - loaded in agent mode) +│ │ ├── client/*.classdata (client classes - loaded in client mode) +│ │ └── shared/*.classdata (shared classes - loaded in both modes) +├── org/openjdk/btrace/boot/ (bootstrap classes - visible to JVM) +└── org/openjdk/btrace/core/ (core/runtime classes from bootstrap module) +``` + +### Class Loading Strategy + +1. **Bootstrap Classes** (`.class` files in root): + - Loaded by bootstrap classloader + - Visible to JVM and all code + - Includes: Loader, MaskedClassLoader, MaskedJarUtils + - These classes initialize the masked jar system + +2. **Agent Classes** (`.classdata` in `META-INF/btrace/agent/`): + - Loaded via MaskedClassLoader in agent mode + - Isolated from application classes + - Includes: btrace-agent, btrace-instr, btrace-runtime, relocated jctools + +3. **Client Classes** (`.classdata` in `META-INF/btrace/client/`): + - Loaded via MaskedClassLoader in client mode + - Includes: btrace-client, btrace-compiler, lanterna UI + +4. **Shared Classes** (`.classdata` in `META-INF/btrace/shared/`): + - Loaded in both agent and client modes + - Includes: communication protocol, annotations, ASM core + - Critical for agent-client communication + +### Why Masked JAR? + +- **Single Source of Truth**: One JAR for all use cases (agent, client, standalone) +- **Bootstrap Isolation**: Agent/client classes hidden from JVM, preventing conflicts +- **No Embedded JARs**: Eliminates nested JAR extraction overhead +- **Simplified Build**: Removed redundant uber jar - masked jar handles everything + +### Build Process + +The masked JAR is built in `btrace-dist/build.gradle`: +1. `allClassesShadow` - Creates intermediate shadow jar with all dependencies and relocations +2. `prepareAgentClassdata` - Extracts agent classes, renames `.class` → `.classdata` +3. `prepareClientClassdata` - Extracts client classes, renames `.class` → `.classdata` +4. `prepareSharedClassdata` - Extracts shared classes, renames `.class` → `.classdata` +5. `btraceJar` - Combines bootstrap classes (as `.class`) + masked classes (as `.classdata`) + +### Debugging Tips + +- **ClassNotFoundException in agent mode**: Check if class is in `META-INF/btrace/agent/` (or shared if needed) +- **ClassNotFoundException in client mode**: Check if class is in `META-INF/btrace/client/` (or shared if needed) +- **NoClassDefFoundError between modes**: Class may need to be in shared section +- **Inspect masked JAR**: `unzip -l btrace.jar | grep -E "(\.class|\.classdata)"` +- **Check manifest**: `unzip -p btrace.jar META-INF/MANIFEST.MF` + +## Launch Modes +``` +Launch-time (agent mode): + java -javaagent:$BTRACE_HOME/libs/btrace.jar=script=MyTrace.java -jar app.jar + |-> Loader.premain() -> loads agent classes from .classdata -> installs transformer + +Attach-time (client mode): + btrace MyTrace.java + |-> Loader as Main-Class -> loads client classes from .classdata -> attaches to target JVM + |-> Target JVM: Loader.agentmain() -> loads agent classes from .classdata -> instruments + +Standalone (client mode): + java -jar btrace.jar + |-> Same as attach-time, Loader delegates to client +``` + +## Troubleshooting +- Attach disabled: if JVM was started with `-XX:+DisableAttachMechanism`, remove it or relaunch without it. +- Permission errors: attach requires same OS user as target JVM; on Linux/macOS avoid sudo mixing; check container/JDK permissions. +- Toolchains: ensure `JAVA_HOME` and optional `TEST_JAVA_HOME` point to valid JDKs; for integration tests, build `btrace-dist` first so client/libs exist. + +### Masked JAR Troubleshooting +- **ClassNotFoundException with .classdata**: MaskedClassLoader can't find class in masked sections. Check: + 1. Is the class in the correct section? (agent/client/shared) + 2. Was the class relocated? Check package name matches relocated path + 3. Did the build complete successfully? Rebuild with `./gradlew clean :btrace-dist:btraceJar` +- **Shared classes**: If a class is used by BOTH agent and client (e.g., comm protocol, annotations), it MUST be in the shared section +- **Bootstrap vs Masked**: Bootstrap classes (.class) are visible everywhere; masked classes (.classdata) are isolated per-mode +- **Build order matters**: `allClassesShadow` must complete before prepare*Classdata tasks run + +## Example Script +```java +package helloworld; +import static io.btrace.core.BTraceUtils.*; +import io.btrace.core.annotations.*; +import io.btrace.core.types.AnyType; + +@BTrace +public class MyTrace { + @OnMethod(clazz="extra.HelloWorld", method="/.*/") + public static void onAny(@ProbeMethodName String pmn) { + println("entered: " + pmn); + } +} +``` +Run with: `btrace MyTrace.java` (see docs/BTraceTutorial.md for steps). + +```java +// Args capture +package helloworld; +import static io.btrace.core.BTraceUtils.*; +import io.btrace.core.annotations.*; +import io.btrace.core.types.AnyType; + +@BTrace +public class ArgsTrace { + @OnMethod(clazz="extra.HelloWorld", method="/call.*/") + public static void onCall(@ProbeMethodName String pmn, AnyType[] args) { + println("args for " + pmn); + printArray(args); + } +} +``` + +```java +// Return value and duration +package helloworld; +import static io.btrace.core.BTraceUtils.*; +import io.btrace.core.annotations.*; +import io.btrace.core.types.AnyType; + +@BTrace +public class ReturnTrace { + @OnMethod(clazz="extra.HelloWorld", method="callC", location=@Location(Kind.RETURN)) + public static void onReturn(@Duration long dur, @Return AnyType ret) { + println("callC ret=" + str(ret) + ", dur(ns)=" + dur); + } +} +``` + +## Build, Test, and Development +! Do not consume the gradle task logs directly. ! +! Write the output to a file, running through grep to include only relevant information and then read the log file. ! + +- Full build: `./gradlew build` — compiles all modules and runs unit tests. +- Distribution: `./gradlew :btrace-dist:build` — creates ZIP/TGZ/RPM/DEB and an exploded layout under `btrace-dist/build/resources/main`. +- Unit tests: `./gradlew test` — JUnit 5, runs per-module tests. +- Integration tests: first build dist, then `./gradlew -Pintegration test`. + - Requires `JAVA_HOME` and typically `TEST_JAVA_HOME` (e.g., JDK 11). Example: `TEST_JAVA_HOME=$JAVA_11_HOME ./gradlew -Pintegration test`. +- Formatting: `./gradlew spotlessApply` (check with `spotlessCheck`). +- Coverage: `./gradlew jacocoTestReport` (CI publishes to Codecov). + +## Coding Style & Naming +- Language: Java. Source/target set to 8; toolchains compile with JDK 11. +- Format: Google Java Format via Spotless. Import order enforced; unused imports removed. +- Packages under `io.btrace.*`. +- Module names follow `btrace-` (e.g., `btrace-extensions:btrace-utils`). + +## Testing Guidelines +- Framework: JUnit Jupiter (JUnit 5). +- Unit tests reside under `src/test/java`; name classes with `*Test`. +- Integration tests in `integration-tests/src/test/java`; BTrace scripts under `integration-tests/src/test/btrace`. +- For integration runs, ensure `btrace-dist/build/resources/main/v/libs/btrace.jar` exists (created by the dist build). +- The masked JAR is used for all integration tests - both agent and client modes use the same artifact. + +## Commit & Pull Request Guidelines +- Commit style: Conventional Commits (e.g., `feat(core): add probe`, `fix(instr): handle null arg`). +- PRs must be from signers of the Oracle Contributor Agreement (OCA) — see README. +- PR checklist: + - Clear description and rationale; link related issues. + - Tests updated/added; CI green across unit and integration suites. + - Formatting passes (`spotlessCheck`); no unrelated changes. + - For behavior changes, include before/after notes or relevant logs. + +## Tips & Environment +- Useful env vars: `JAVA_HOME`, `TEST_JAVA_HOME`, `BTRACE_TEST_DEBUG=true` (verbose integration tests), optional `BTRACE_HOME` when using the exploded dist. +- Example exploded dist path: `btrace-dist/build/resources/main/v2.2.6/`. + +### Restricted/CI Environments +- Prefer a workspace-local Gradle cache to avoid permission issues: set `GRADLE_USER_HOME=$(pwd)/.gradle-user`. +- If network interfaces are restricted, force IPv4 to avoid wildcard IP detection errors: set `JAVA_TOOL_OPTIONS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"`. +- Example: `GRADLE_USER_HOME=$(pwd)/.gradle-user JAVA_TOOL_OPTIONS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false" ./gradlew :btrace-dist:buildZip -x test` + +## Common Patterns & Lessons Learned + +### Adding New Classes +1. **Determine the section**: Is the class used by agent, client, or both? + - Agent only → prepareAgentClassdata include pattern + - Client only → prepareClientClassdata include pattern + - Both → prepareSharedClassdata include pattern +2. **Update build.gradle**: Add include pattern in the appropriate task +3. **Rebuild and test**: `./gradlew clean :btrace-dist:btraceJar && ./gradlew -Pintegration test` + +### Dependency Relocation +- All third-party dependencies are relocated to `io.btrace.libs.*` +- Relocations happen in `allClassesShadow` task using Shadow plugin +- Common relocations: ASM, SLF4J, JCTools +- After relocation, classes are extracted and masked in prepare*Classdata tasks + +### Build Simplification Wins +- **Before**: Separate agent.jar, client.jar, boot.jar, uber.jar (4 artifacts) +- **After**: Single btrace.jar with masked sections (1 artifact) +- **Result**: Simpler build, smaller distribution, easier maintenance + +### ClassLoader Isolation +- Bootstrap classes can see everything (including masked sections via MaskedClassLoader) +- Application classes cannot see masked sections (isolation prevents conflicts) +- Masked classes in agent mode cannot see masked classes in client mode (intentional isolation) +- Shared section solves cross-mode visibility when needed (e.g., command serialization) + +## Hard rules +- Never commit changes unless they are fully tested or you are explicitly asked to commit +- Do not use FQNs directly! Always import types and use simple type names in the code! +- When adding classes to masked jar, always consider: agent-only, client-only, or shared? +- Rebuild the distribution after any changes to masked jar structure: `./gradlew clean :btrace-dist:btraceJar` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c23a75ccf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,293 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +BTrace is a safe, dynamic tracing tool for the Java platform that instruments running Java applications to inject tracing code ("bytecode tracing"). It's similar to DTrace for OpenSolaris but designed specifically for Java. + +## Build Commands + +### Primary Build + +```bash +# Build entire project with distribution packages +./gradlew :btrace-dist:build + +# Output locations: +# - Distributions: btrace-dist/build/distributions/ (*.tar.gz, *.zip, *.rpm, *.deb) +# - Exploded binary (BTRACE_HOME): btrace-dist/build/resources/main/ +``` + +### Testing + +```bash +# Run all tests +./gradlew test + +# Run tests for specific module +./gradlew :btrace-instr:test +./gradlew :btrace-compiler:test + +# Run specific test class +./gradlew :btrace-instr:test --tests "*CompilerTest" + +# Run specific test method +./gradlew :btrace-instr:test --tests "io.btrace.compiler.CompilerTest.testSimple" + +# Integration tests (requires dist built first) +./gradlew :btrace-dist:build +./gradlew :integration-tests:test -Pintegration +``` + +### Golden Files (Test Data) + +Some instrumentation tests use golden files to verify bytecode correctness. When you intentionally change instrumented bytecode: + +```bash +# Regenerate all golden files +./gradlew test -PupdateTestData + +# Then commit the updated files in btrace-instr/src/test/resources/instrumentorTestData/ +``` + +### Code Formatting + +```bash +# Check formatting (runs during build) +./gradlew spotlessCheck + +# Auto-format all code (Google Java Format) +./gradlew spotlessApply +``` + +### Module-Specific Build + +```bash +# Build single module +./gradlew ::build + +# Build without tests (faster) +./gradlew ::build -x test +``` + +## Architecture Overview + +BTrace follows a clear separation between compile-time safety verification and runtime instrumentation: + +### Core Flow: Script → Compilation → Instrumentation → Execution + +``` +BTrace Script (.java) + ↓ +[btrace-compiler] Compile & verify safety + ↓ +Compiled Script (.class) + ↓ +[btrace-agent] Load into target JVM + ↓ +[btrace-instr] Transform target classes + ↓ +[btrace-runtime] Execute probe code +``` + +### Module Responsibilities + +**btrace-compiler** - Script compilation and safety verification +- Entry: `Compiler.java` - JSR 199 compilation pipeline +- Safety: `Verifier.java` - Enforces BTrace restrictions (no loops, no allocations, no exceptions, no field assignment) +- Post-processing: `Postprocessor.java` - Bytecode transformations after compilation +- Location: `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/` + +**btrace-agent** - Java agent for attaching to target JVMs +- Entry: `Main.java` - Implements `premain()` and `agentmain()` for instrumentation +- Client management: `Client.java`, `RemoteClient.java`, `FileClient.java` +- Starts server on port 2020 by default to accept client connections +- Location: `btrace-agent/src/main/java/org/openjdk/btrace/agent/` + +**btrace-instr** - Bytecode instrumentation using ASM +- Transformer: `BTraceTransformer.java` - ClassFileTransformer implementation +- Probe factory: `BTraceProbeFactory.java` - Creates BTraceProbe instances +- Instrumentor: `Instrumentor.java` - Main ASM-based bytecode injection logic +- 15 specialized instrumentors for different probe types (method entry/exit, field access, allocation, etc.) +- Location: `btrace-instr/src/main/java/org/openjdk/btrace/instr/` + +**btrace-runtime** - Runtime services for BTrace scripts +- Factory: `BTraceRuntimes.java` - Loads version-specific implementations (JDK 8, 9, 11, 15+) +- Base: `BTraceRuntimeImplBase.java` - Core runtime services (print, profiling, JMX, etc.) +- Multi-version support: Separate implementations in `src/main/java9/`, `src/main/java11/`, `src/main/java15/` +- Location: `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/` + +**btrace-client** - Command-line client tool +- Main: `Main.java` - CLI for connecting to agent and submitting scripts +- Client: `Client.java` - Manages socket connection to agent +- Location: `btrace-client/src/main/java/org/openjdk/btrace/client/` + +**btrace-core** - Core abstractions and communication +- Wire protocols: `WireProtocol.java` - V1 (Java serialization) and V2 (binary, 3-6x faster) +- Commands: 20+ command types in `comm/` package for agent-client communication +- Annotations: `@BTrace`, `@OnMethod`, `@OnTimer`, etc. in `annotations/` package +- Location: `btrace-core/src/main/java/org/openjdk/btrace/core/` + +### Key Architectural Patterns + +**Safety-First Design**: Scripts are verified at compile-time to prevent: +- Loops and recursion (no infinite loops in instrumented code) +- Object allocation (minimize GC pressure on target app) +- Exception throwing (don't crash target app) +- Field assignment (maintain immutability) + +**Instrumentation Pipeline**: When a class loads: +1. `BTraceTransformer.transform()` checks registered probes +2. For matching classes, `Instrumentor` creates ASM visitor chain +3. Each specialized instrumentor handles specific probe types (entry, exit, call, field access, etc.) +4. Modified bytecode with injected probe calls is returned + +**Multi-Version Runtime**: The runtime has version-specific implementations to handle JDK differences: +- Base in `src/main/java/` (Java 8) +- JDK 9+ features in `src/main/java9/` +- JDK 11+ features in `src/main/java11/` +- JDK 15+ features in `src/main/java15/` + +**Communication**: Agent and client communicate via pluggable WireProtocol: +- V1: Java Object Serialization (backward compatible) +- V2: Custom binary protocol (higher performance, 2-5x smaller payloads) +- Auto-negotiation ensures compatibility + +## Code Conventions + +**Source Compatibility**: Java 8 for main code, compiled with Java 11 toolchain +- Set in `common.gradle`: `sourceCompatibility = 8`, `targetCompatibility = 8` +- Uses Java 11 compiler for better optimization while maintaining Java 8 compatibility + +**Formatting**: Google Java Format via Spotless +- Import order: `java`, `javax`, `io.btrace`, `*`, then static imports +- Always run `./gradlew spotlessApply` before committing + +**Testing**: +- Unit tests in `src/test/java/` named `*Test.java` +- BTrace instrumentation scripts in `src/test/btrace/` (compiled with btracec) +- Golden files in `src/test/resources/instrumentorTestData/` for bytecode verification +- Integration tests require full distribution build first + +## Important Implementation Details + +**BTrace Script Restrictions** (enforced by `Verifier.java`): +- No `new` keyword (object allocation) +- No loops (`for`, `while`, `do-while`) +- No `throw` statements +- No field assignment (can read fields, but not write) +- Only whitelisted method calls allowed (BTraceUtils.*, etc.) +- Scripts must be annotated with `@BTrace` + +**Instrumentation Probe Types** (`@OnMethod` locations): +- `Kind.ENTRY` - Method entry +- `Kind.RETURN` - Method return (normal) +- `Kind.ERROR` - Method return (exception) +- `Kind.CALL` - Before method call +- `Kind.LINE` - Specific line number +- `Kind.FIELD_GET` / `FIELD_SET` - Field access +- `Kind.ARRAY_GET` / `ARRAY_SET` - Array access +- `Kind.NEW` - Object/array allocation +- Plus more (see `Location.java`) + +**ClassFileTransformer Thread Safety**: `BTraceTransformer` uses `ReentrantReadWriteLock` because: +- Multiple threads can load classes concurrently +- Probe registration (write) vs. class transformation (read) must be coordinated + +**Performance Considerations**: +- Filter-based class matching to avoid unnecessary transforms +- Class metadata caching (`ClassCache.java`) +- Binary protocol V2 for reduced serialization overhead +- Async command queue with configurable backoff + +## Module Dependencies + +``` +btrace-core (annotations, wire protocol, commands) + ↓ +btrace-compiler, btrace-instr, btrace-runtime + ↓ +btrace-agent (orchestrates everything) + ↓ +btrace-client, btrace-dist +``` + +## Working with BTrace Scripts + +BTrace scripts are Java files with special annotations. They're compiled separately from regular Java code: + +**Compilation**: Use `btracec` tool or Gradle tasks +```bash +# In tests +./gradlew :btrace-instr:compileTestProbes +./gradlew :integration-tests:compileTestProbes + +# In benchmarks +./gradlew :benchmarks:agent-benchmark:btracec +``` + +**Example Script Structure**: +```java +@BTrace +public class MyTrace { + @OnMethod(clazz="java.io.FileInputStream", method="") + public static void onFileOpen(@ProbeClassName String className, String fileName) { + BTraceUtils.println("File opened: " + fileName); + } +} +``` + +## Debugging Tips + +**Enable Debug Output**: BTrace has built-in debug support via `DebugSupport.java` +- Set system properties or use agent debug flags +- Check `Main.java` for available agent arguments + +**Test Isolation**: Each test should be independent +- Tests run with `cleanTest` dependency (always run, never cached) +- Integration tests require fresh distribution build +- BTrace agent defaults to port 2020; `ManifestLibsTests` uses port 2022 to avoid contention +- Port is configured via `btrace.port` system property, passed to spawned BTrace processes via `-Dbtrace.port` and `-p` CLI flag + +**Bytecode Verification**: If instrumentation tests fail: +1. Check if bytecode generation changed +2. Regenerate golden files with `-PupdateTestData` +3. Verify the changes are intentional +4. Commit updated golden files + +**ClassLoader Issues**: BTrace uses multiple classloaders: +- Bootstrap classpath for agent core (bootstrap section of masked `btrace.jar`) +- System classpath for client tools +- Target application classloaders remain isolated +- See `Main.java` for bootstrap setup + +## Documentation Organization + +The repository separates user-facing documentation from internal agent/planning documents. + +**User-facing docs** → `docs/` +- End-user guides, tutorials, FAQ, quick reference, troubleshooting +- Architecture reference docs (`docs/architecture/`) for contributors and advanced users +- Developer ops docs (`docs/releasing.md`, `docs/examples/`, `docs/samples/`) +- `docs/README.md` is the documentation index — keep links here up to date + +**Internal/agent docs** → `internal/` +- `internal/plans/` — session plans, implementation plans, next-steps notes +- `internal/specs/` — design specs derived from issues or external sources +- `internal/libretti/` — muse/libretto agent requirement files from GitHub issues +- `internal/superpowers/plans/` — superpowers agent implementation plans +- `internal/superpowers/specs/` — superpowers agent design specs + +**Rules for agents — MANDATORY:** +- NEVER store planning documents, session notes, implementation specs, or libretto files under `docs/` +- NEVER create or write to a `doc/` directory (singular) — it does not exist; use `internal/` instead +- NEVER create `docs/plans/`, `docs/superpowers/`, or any non-user-facing subdirectory under `docs/` +- Store all agent-generated plans in `internal/plans/` or `internal/superpowers/plans/` +- Store all agent-generated design/requirement specs in `internal/specs/` or `internal/superpowers/specs/` +- Store all libretto/muse requirement files in `internal/libretti/` +- Do not link internal documents from `docs/README.md` or other user-facing docs + +## Contributing Notes + +From Readme.md: Pull requests can only be accepted from signers of the [Oracle Contributor Agreement](https://oca.opensource.oracle.com/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..75e33ffba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +# Contributing to BTrace + +Thanks for your interest in contributing! This guide covers local development, running tests, Gradle tips, and common troubleshooting. + +## Local Development + +- JDK: Use a reasonably recent JDK (11+ recommended). The project targets a broad range but tests run comfortably on 11/17. +- Wrapper: Use the bundled `./gradlew` wrapper. It will download the pinned Gradle version if needed. +- Local Gradle cache (optional but recommended): + - macOS/Linux: `export GRADLE_USER_HOME="$PWD/.gradle-home"` + - Windows (PowerShell): `$env:GRADLE_USER_HOME = "$PWD/.gradle-home"` + +## Running Tests + +- All unit tests (skip integration tests): + ```sh + ./gradlew --no-daemon test -x integration-tests:test + ``` + +- Per-module tests: + - Runtime: `./gradlew :btrace-runtime:test` + - Extension: `./gradlew :btrace-extension:test` + - Compiler: `./gradlew :btrace-compiler:test` + - Instr: `./gradlew :btrace-instr:test` + +- Update instrumentor goldens when bytecode output changes: + ```sh + ./gradlew test -PupdateTestData + ``` + +- Integration tests (spawn JVMs, exercise agent and extensions): + ```sh + ./gradlew --no-daemon integration-tests:test + ``` + - If tests fail due to denied privileged extensions, pass a policy file to the tested JVMs: + - Create `permissions.properties`: + ```properties + allowPrivileged=true + allowExtensions=btrace-metrics,btrace-utils + ``` + - Export path: `export BTRACE_PERMS=$PWD/permissions.properties` + - Run Gradle with: `-Dbtrace.permissions=$BTRACE_PERMS` + +## Gradle Tips + +- Prefer IPv4 if your environment has unusual local IP settings (helps Gradle select a wildcard address): + ```sh + export GRADLE_OPTS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false" + ``` +- Enable Gradle debug output for flakiness: add `--info` or `--debug`. +- Run a single test class/method: + ```sh + ./gradlew :btrace-extension:test --tests io.btrace.extension.ExtensionBridgeImplPolicyTest + ./gradlew :btrace-runtime:test --tests "*ExtensionIndyShimIndexTest.resolvesNoopShimFromIndex" + ``` + +## Troubleshooting + +- Gradle wrapper needs to download Gradle: ensure network is allowed once; subsequent runs use the local cache under `.gradle-home`. +- Error: `Could not determine a usable wildcard IP for this machine`: + - Set the IPv4 flags shown above or ensure local networking is available. +- Permission errors when Gradle writes outside the workspace: + - Use a local Gradle cache via `GRADLE_USER_HOME` as shown above. +- Integration tests failing with permissions denied: + - Provide a policy file and pass it via `-Dbtrace.permissions=/path/to/permissions.properties`. + +## Code Style & Scope + +- Keep changes focused and minimal; align with existing code style. +- Update docs when changing user-visible behavior. +- Prefer clear separation of concerns and small helpers over inlined, complex logic. +- Avoid introducing new dependencies without discussion. + +## Submitting a PR + +1. Fork the repo and branch from `develop` (unless otherwise agreed). +2. Make your changes and run tests locally. +3. If instrumentor behavior changed, update goldens (`-PupdateTestData`) and include them in your commit. +4. Submit a PR with a concise description of the change, rationale, and any follow-ups. + +Happy tracing! + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..6ed38b80d --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +BTrace +Copyright 2008, 2024, Jaroslav Bachorik + +This product was originally developed at Sun Microsystems. +The project has been independently maintained since 2010. diff --git a/README.md b/README.md index f22683faa..ba1a48b7b 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,417 @@ -[![Dev build](https://github.com/btraceio/btrace/workflows/BTrace%20CI%2FCD/badge.svg?branch=develop)](https://github.com/btraceio/btrace/actions?query=workflow%3A%22BTrace+CI%2FCD%22+branch%3Adevelop) [![Download](https://img.shields.io/github/v/release/btraceio/btrace?sort=semver)](https://github.com/btraceio/btrace/releases/latest) [![codecov.io](https://codecov.io/github/btraceio/btrace/coverage.svg?branch=develop)](https://codecov.io/github/btraceio/btrace?branch=develop) [![huhu](https://img.shields.io/badge/Slack-join%20chat-brightgreen")](http://btrace.slack.com/) [![Join the chat at https://gitter.im/jbachorik/btrace](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/btraceio/btrace?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Project Stats](https://www.openhub.net/p/btrace/widgets/project_thin_badge.gif)](https://www.openhub.net/p/btrace) +# BTrace -# btrace +**Safe, dynamic tracing for Java applications** -A safe, dynamic tracing tool for the Java platform +[![CI](https://github.com/btraceio/btrace/workflows/BTrace%20CI%2FCD/badge.svg?branch=develop)](https://github.com/btraceio/btrace/actions) +[![Release](https://img.shields.io/github/v/release/btraceio/btrace?sort=semver)](https://github.com/btraceio/btrace/releases/latest) +[![codecov](https://codecov.io/github/btraceio/btrace/coverage.svg?branch=develop)](https://codecov.io/github/btraceio/btrace?branch=develop) -## Version -2.2.0 +BTrace dynamically instruments running Java applications to inject tracing code at runtime. No restarts. No recompilation. Production-safe. -## Quick Summary -BTrace is a safe, dynamic tracing tool for the Java platform. +> **Quick links:** [Quick Reference](docs/QuickReference.md) · [Step-by-Step Tutorial](docs/GettingStarted.md) -BTrace can be used to dynamically trace a running Java program (similar to DTrace for OpenSolaris applications and OS). BTrace dynamically instruments the classes of the target application to inject tracing code ("bytecode tracing"). +--- -## Credits -* Based on [ASM](http://asm.ow2.org/) -* Powered by [JCTools](https://github.com/JCTools/JCTools) -* Powered by [hppcrt](https://github.com/vsonnier/hppcrt) -* Optimized with [JProfiler Java Profiler](http://www.ej-technologies.com/products/jprofiler/overview.html) -* Build env helper using [SDKMAN!](https://sdkman.io/) +## Why BTrace? -## Building BTrace +- **Zero downtime** - Attach to running JVMs without restart +- **Production safe** - Verified scripts can't crash your application +- **Flexible probes** - Method entry/exit, timings, field access, allocations +- **Low overhead** - Bytecode injection with minimal performance impact -### Setup -You will need the following applications installed +--- -* [Git](http://git-scm.com/downloads) -* __JDKs__ - JDK 8, Java 9 and Java 11 are required to be available -* (optionally, the default launcher is the bundled `gradlew` wrapper) [Gradle](http://gradle.org) +## Get Started in 30 Seconds -In order to ease the pre-build config the `config_build.sh` script is provided. You should run it first as `source config_build.sh` to automatically download all required JDKs and set up the corresponding `JAVA_*_HOME` env variables. +```sh +# Install via JBang (easiest) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Add the BTrace JBang catalog (one time) +jbang catalog add --name btraceio https://raw.githubusercontent.com/btraceio/jbang-catalog/main/jbang-catalog.json + +# Trace slow methods in your running app +jbang btrace@btraceio -n 'com.myapp.*::* @return if duration>100ms { print method, duration }' $(pgrep -f myapp) +``` -### Build +--- -#### Java -Your __JAVA_HOME__ must point to JDK 11 (eg. __JAVA_11_HOME__) +## Trace Anything -#### Gradle +**Method timing:** ```sh -cd -./gradlew build -./gradlew buildDistributions +btrace -n 'java.sql.Statement::execute* @return { print method, duration }' +``` + +**Exception tracking:** +```sh +btrace -n 'java.lang.Exception:: @return { print self, stack(5) }' +``` + +**Custom probes:** +```java +@BTrace public class Trace { + @OnMethod(clazz = "com.example.OrderService", method = "checkout") + public static void onCheckout(@Self Object self, @Duration long ns) { + println(strcat("checkout: ", str(ns/1_000_000) + "ms")); + } +} ``` -The binary dist packages can be found in `/btrace-dist/build/distributions` as the *.tar.gz, *.zip, *.rpm and *.deb files. -The exploded binary folder which can be used right away is located at `/btrace-dist/build/resources/main` which serves as the __BTRACE_HOME__ location. + +See the [Oneliner Guide](docs/OnelinerGuide.md) for complete syntax. + +--- + +## Install + +```sh +# JBang (recommended - zero installation) +jbang catalog add --name btraceio https://raw.githubusercontent.com/btraceio/jbang-catalog/main/jbang-catalog.json +jbang btrace@btraceio script.java + +# SDKMan +sdk install btrace + +# Manual download +curl -LO https://github.com/btraceio/btrace/releases/latest/download/btrace-bin.tar.gz +``` + +See [Installation Guide](docs/GettingStarted.md#installation) for Docker, package managers, and more options. + +--- + +## Documentation + +| Resource | Description | +|----------|-------------| +| [Quick Reference](docs/QuickReference.md) | Cheat sheet for experienced users | +| [Getting Started](docs/GettingStarted.md) | Step-by-step first trace tutorial | +| [Full Tutorial](docs/BTraceTutorial.md) | Complete walkthrough of all features | +| [Oneliners](docs/OnelinerGuide.md) | DTrace-style quick probes | +| [Extensions](docs/BTraceExtensionDevelopmentGuide.md) | StatsD, custom integrations | +| [Documentation Hub](docs/README.md) | All docs and guides | + +--- + +## Building from Source + +```sh +git clone https://github.com/btraceio/btrace.git +cd btrace +./gradlew :btrace-dist:build +``` + +See [CLAUDE.md](CLAUDE.md) for development setup and architecture. + +--- + +## Community & Contributing + +**Get help:** [Slack](http://btrace.slack.com/) · [GitHub Issues](https://github.com/btraceio/btrace/issues) + +Tips: +- Prefer IPv4 if your environment has odd local IPs: set `GRADLE_OPTS="-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"`. +- Run specific modules: + - Runtime: `./gradlew :btrace-runtime:test` + - Extension: `./gradlew :btrace-extension:test` + - Compiler: `./gradlew :btrace-compiler:test` + - Instr: `./gradlew :btrace-instr:test` +- Update instrumentor golden files when bytecode output changes: `./gradlew test -PupdateTestData`. + +Integration tests (optional): +```sh +./gradlew --no-daemon integration-tests:test +``` +These may exercise privileged extensions. If you run into permission denials, provide a policy file and pass it to the test JVMs via `-Dbtrace.permissions=/path/to/permissions.properties`. ## Using BTrace + ### Installation -Download a distribution file from the [release page](https://github.com/btraceio/btrace/releases/latest). Explode the binary distribution file (either *.tar.gz or *.zip) to a directory of your choice. -You may set the system environment variable __BTRACE_HOME__ to point to the directory containing the exploded distribution. +#### JBang (Easiest - Recommended) -You may enhance the system environment variable __PATH__ with __$BTRACE_HOME/bin__ for your convenience. +Use [JBang](https://www.jbang.dev/) to run BTrace without manual installation: -Or, alternatively, you may install one of the *.rpm or *.deb packages +```sh +# Install JBang (one time) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Use BTrace immediately (replace with desired version, e.g., 2.3.0) +jbang io.btrace:btrace-client: -### Running -* `/bin/btrace ` will attach to the __java__ application with the given __PID__ and compile and submit the trace script -* `/bin/btracec ` will compile the provided trace script -* `/bin/btracer ` will start the specified java application with the btrace agent running and the script previously compiled by *btracec* loaded +# After first run, use shorter alias +jbang btrace +``` -For the detailed user guide, please, check the [Wiki](https://github.com/btraceio/btrace/wiki/Home). +**Note:** Replace `` with the desired BTrace version (e.g., `2.3.0`). See [releases](https://github.com/btraceio/btrace/releases) for available versions. -### Maven Integration -The [maven plugin](https://github.com/btraceio/btrace-maven) is providing easy compilation of __BTrace__ scripts as a part of the build process. As a bonus you can utilize the _BTrace Project Archetype_ to bootstrap developing __BTrace__ scripts. +**Benefits:** Zero installation, automatic version management, works everywhere (Windows/macOS/Linux/containers), perfect for CI/CD. + +**Extract agent JARs** (if needed for `--agent-jar`/`--boot-jar` flags): +```sh +# Extract embedded JARs +jbang btrace --extract-agent ~/.btrace + +# This creates ~/.btrace/btrace.jar (single masked JAR) +# Then use them: +jbang btrace --agent-jar ~/.btrace/btrace.jar + +# Or find in Maven local repository (after first jbang run): +# ~/.m2/repository/org/openjdk/btrace/btrace//btrace-.jar +# +# Legacy jar names (btrace-agent.jar, btrace-boot.jar) are still extracted for backward compatibility. +``` + +See [Getting Started Guide](docs/GettingStarted.md#jbang-installation-recommended-for-quick-start) for complete JBang documentation and examples. + +#### Binary Distribution -## Contributing - !!! Important !!! +**Download:** Get the latest release from the [release page](https://github.com/btraceio/btrace/releases/latest) -Pull requests can be accepted only from the signers of [Oracle Contributor Agreement](http://www.oracle.com/technetwork/community/oca-486395.html) +```sh +# Extract the archive +tar -xzf btrace-*.tar.gz +# or +unzip btrace-*.zip + +# Set environment variables (optional but recommended) +export BTRACE_HOME=/path/to/btrace +export PATH=$BTRACE_HOME/bin:$PATH +``` -### Deb Repository +#### Package Installation -Using the command line, add the following to your /etc/apt/sources.list system config file: +```sh +# RPM-based systems +sudo rpm -i btrace-*.rpm +# Debian-based systems +sudo dpkg -i btrace-*.deb ``` -echo "deb http://dl.bintray.com/btraceio/deb xenial universe" | sudo tee -a /etc/apt/sources.list + +**Docker images:** +```dockerfile +# Copy BTrace into your application image +FROM btrace/btrace:latest AS btrace +FROM bellsoft/liberica-openjdk-debian:11-cds + +COPY --from=btrace /opt/btrace /opt/btrace +ENV BTRACE_HOME=/opt/btrace PATH="${PATH}:${BTRACE_HOME}/bin" + +# Your application... ``` -Or, add the repository URLs using the "Software Sources" admin UI: +Available variants: +- `btrace/btrace:latest` - Debian-based (~25MB) +- `btrace/btrace:latest-alpine` - Alpine-based (~15MB) +- `btrace/btrace:latest-distroless` - Distroless (~10MB) + +See [docker/README.md](docker/README.md) for complete Docker documentation. +### Quick Start + +**With JBang (no installation required):** +```sh +# Attach to running application +jbang btrace + +# Extract agent JARs +jbang btrace --extract-agent ~/.btrace ``` -deb http://dl.bintray.com/btraceio/deb xenial universe + +**With installed BTrace:** +```sh +# Attach to running application +btrace + +# Compile BTrace script +btracec + +# Launch application with BTrace agent +btracer ``` -### RPM Repository +### Extensions and Deprecated libs/profiles + +Extensions add functionality via a stable API on bootstrap and an isolated implementation. See the extension development guide and examples. + +Note: The legacy `libs`/profiles mechanism is deprecated and planned for removal after N+2 minor releases. Prefer packaging integrations as extensions and using provided-style class loading patterns (object hand-off + TCCL). For migration guidance and examples, see: +- `docs/architecture/migrating-from-libs-profiles.md` +- `docs/architecture/provided-style-extensions.md` +- `docs/examples/README.md` + +As a last resort (discouraged), you may append a single jar to the system classpath: `-Dbtrace.system.appendJar=/abs/path/lib.jar -Dbtrace.trusted=true`. + +### Fat Agent JAR (Single-JAR Deployment) + +For environments where managing multiple JARs is impractical (Spark, Hadoop, Kubernetes), BTrace provides a fat agent JAR with embedded extensions: + +```sh +# Build fat agent with all extensions +./gradlew :btrace-dist:fatAgentJar + +# Build with specific extensions only +./gradlew :btrace-dist:fatAgentJar -PembedExtensions=btrace-metrics,btrace-statsd + +# Use the fat agent +java -javaagent:btrace-agent-fat.jar +``` + +The fat agent JAR includes: +- All agent and boot classes +- Embedded extension API classes (bootstrap) +- Embedded extension impl classes (runtime-loaded) +- Extension metadata for auto-discovery + +For custom fat agent builds, use the Gradle plugin: +```groovy +plugins { + id 'io.btrace.fat-agent' +} + +btraceFatAgent { + embedExtensions { + maven('io.btrace:btrace-metrics:2.3.0') + project(':my-custom-extension') + } +} +``` + +See [Fat Agent Plugin Architecture](docs/architecture/fat-agent-plugin.md) and [Gradle Plugin README](btrace-gradle-plugin/README.md) for details. + +### Oneliner Quick Examples + +BTrace now supports DTrace-style oneliners for quick debugging without writing full Java scripts: + +```sh +# Trace method entry with arguments +btrace -n 'javax.swing.*::setText @entry { print method, args }' + +# Find slow database queries (>100ms) +btrace -n 'java.sql.Statement::execute* @return if duration>100ms { print method, duration }' + +# Count method invocations +btrace -n 'java.util.HashMap::get @entry { count }' + +# Print stack trace on OutOfMemoryError +btrace -n 'java.lang.OutOfMemoryError:: @return { stack(10) }' +``` + +**Supported features:** +- **Locations**: `@entry`, `@return`, `@error` +- **Actions**: `print`, `count`, `time`, `stack` +- **Filters**: `if duration>NUMBERms`, `if args[N]==VALUE` +- **Patterns**: Wildcards (`*`, `?`) and regex (`/pattern/`) + +See [Oneliner Guide](docs/OnelinerGuide.md) for complete syntax and examples. + +### Documentation +For comprehensive documentation, tutorials, and guides: +* **[BTrace Documentation Hub](docs/README.md)** - Complete documentation index with learning paths, quick reference, troubleshooting, and more +* **[Getting Started Guide](docs/GettingStarted.md)** - Get up and running in 5 minutes +* **[BTrace Wiki](https://github.com/btraceio/btrace/wiki/Home)** - External wiki with additional resources + +### Extensions and Permissions +BTrace supports extensions (like StatsdExtension) that provide additional functionality. Extensions require explicit permissions for security: + +* **Default permissions** (always granted): MESSAGING, AGGREGATION, JFR_EVENTS, PROFILING +* **Standard permissions** (granted unless denied): FILE_READ, SYSTEM_PROPS, THREAD_INFO, MEMORY_INFO +* **Privileged permissions** (require explicit grant): FILE_WRITE, NETWORK, THREADS, NATIVE, EXEC, REFLECTION, CLASSLOADER, UNLIMITED_MEMORY + +Permissions are enforced based on extension/service descriptors and agent grants specified at attach-time. + +Grant permissions at runtime: +```sh +btrace --grant=NETWORK,THREADS MyProbe.class +``` + +If extensions fail to load, use `-le` to troubleshoot: +```sh +btrace -le +``` + +See the [Tutorial](docs/BTraceTutorial.md) for detailed documentation. + +Extensions CLI: use `btracex` to inspect and manage extensions and the simplified permission policy: +- `btracex inspect ` prints extension id, version, services, and whether it’s privileged. +- `btracex policy print|set [--policy-file |--home|--classpath ]` edits `allowExtensions`, `denyExtensions`, `allowPrivileged`. +- `btracex list` shows installed extensions; `btracex install` installs from Maven coordinates. + +Note: Extension “required permissions” are informational and help operators assess risk. Implementation linking is controlled by per‑extension allow/deny lists and the `allowPrivileged` flag; when blocked, APIs remain available and SHIMs are used so probes continue safely. + +#### Agent Policy and Allow/Deny Lists +- Launch-time policy can be set via agent args (operator-controlled): + - `-javaagent:btrace.jar=...,grant=NETWORK,THREADS,grantAll=false` + - `-javaagent:btrace.jar=...,allowExtensions=btrace-statsd,my-metrics,denyExtensions=legacy-foo` +- Optional policy file (process-local): `-Dbtrace.permissions=/path/to/permissions.properties` or `~/.btrace/permissions.properties`. +- When an extension impl is blocked, the API remains on bootstrap so SHIMs can be generated. + +See docs/PermissionPolicy.md for details and examples. + +#### btracex TUI (interactive) +- Launch: `btracex inspect` (with no args) opens an interactive view of installed extensions. +- Header: shows current policy file path and the list of scanned repositories. +- Table: columns State, Id, Version. State uses compact symbols: `?` (default), `+` (allowed), `-` (denied). +- Details: selection updates automatically; shows the full-word state: `default` / `allowed` / `denied` and the full path. +- Legend: a short legend under the table maps the state symbols. + +Screenshot / demo (optional): + +![btracex TUI](docs/images/btracex-tui.png) + +![btracex TUI demo](docs/images/btracex-tui.gif) + +See also: docs/TUI.md for recording tips and an ASCII preview. + +Keys +- Navigate: Arrow keys, PageUp/PageDown, Home/End +- Toggle state: space (flows `? → + (confirm) → - → +`; only `c` clears to default) +- Clear: `c` (removes extension id from both allow and deny lists) +- Explain privileges: `e` (opens a dialog with required permissions and risk descriptions) +- Filter: `/` (filter by id or path) +- Sort: `s` (choose column; repeat to toggle asc/desc) +- Adjust split: `m` (enter mode), then Up/Down to resize; press `Esc` or `m` again to exit +- Help / Quit: `?` / `q` + +### Maven Integration + +**Fat Agent Plugin** (in this repo): Build fat agent JARs with embedded extensions: +```xml + + io.btrace + btrace-maven-plugin + ${btrace.version} + + + io.btrace:btrace-metrics:${btrace.version} + + + +``` + +**Script Compilation Plugin** ([external repo](https://github.com/btraceio/btrace-maven)): +- Compilation of BTrace scripts during the build process +- BTrace Project Archetype for quick project setup + +## Contributing + +**Important:** Pull requests can only be accepted from signers of the [Oracle Contributor Agreement](https://oca.opensource.oracle.com/). + +### Development + +See [CLAUDE.md](CLAUDE.md) for detailed development guidelines and project architecture. + +## Community + +- **Slack:** [btrace.slack.com](http://btrace.slack.com/) +- **Gitter:** [gitter.im/btraceio/btrace](https://gitter.im/btraceio/btrace) +- **Issues:** [GitHub Issues](https://github.com/btraceio/btrace/issues) + +## License + +GPLv2 with Classpath Exception. See [LICENSE](LICENSE). + +--- -Grab the _*.repo_ file `wget https://bintray.com/btraceio/rpm/rpm -O bintray-btraceio-rpm.repo` and use it. +**Credits:** Built with [ASM](http://asm.ow2.org/), [JCTools](https://github.com/JCTools/JCTools), [hppcrt](https://github.com/vsonnier/hppcrt). Optimized with [JProfiler](http://www.ej-technologies.com/products/jprofiler/overview.html). diff --git a/benchmarks/agent-benchmark/build.gradle b/benchmarks/agent-benchmark/build.gradle index 08c7345f9..fa9d4526c 100644 --- a/benchmarks/agent-benchmark/build.gradle +++ b/benchmarks/agent-benchmark/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id("me.champeau.jmh") version "0.6.3" + alias(libs.plugins.jmh) } description 'A JMH benchmark to assert the overhead imposed by various types of BTrace instrumentation.' @@ -9,29 +9,29 @@ def env = System.getenv() def javaHome = env['JAVA_HOME'] dependencies { - compile project(path: ":btrace-dist", configuration: "shadow") - compile project(":btrace-compiler") - jmh project(":btrace-instr") - jmh project(":btrace-runtime") - jmh project(":btrace-statsd") - jmh("org.openjdk.jmh:jmh-core:1.29") - jmh("org.openjdk.jmh:jmh-generator-annprocess:1.29") + implementation project(path: ":btrace-dist", configuration: "shadow") + implementation project(":btrace-compiler") + jmh tasks.getByPath(':btrace-dist:btraceJar').outputs.getFiles() + + jmh libs.jmh + jmh libs.jmh.annprocess } task btracec(type: JavaExec) { group 'Build' inputs.files 'src/main/resources/scripts' - outputs.dir "${buildDir}/classes/java/main" + outputs.dir buildDir.toPath().resolve("classes/java/main") environment('BTRACE_HOME', "$projectDir") - classpath configurations.compile - main 'org.openjdk.btrace.compiler.Compiler' + classpath configurations.runtimeClasspath + mainClass = 'io.btrace.compiler.Compiler' args '-d' args "${buildDir}/classes/java/main/" args '-packext' args 'btclass' args fileTree(dir: "src/jmh/btrace", include: 'TraceScript.java') } +compileJmhJava.dependsOn btracec jmhClasses.dependsOn btracec jmhJar { @@ -53,8 +53,10 @@ jmh { fork = 2 jvm = "${env['JAVA_HOME']}/bin/java" duplicateClassesStrategy = DuplicatesStrategy.WARN - def agent = "-javaagent:${project.buildDir}/../../../btrace-dist/build/resources/main/${project.version}/libs/btrace-agent.jar=stdout=true,noServer=true,debug=true,script=${project.buildDir}/classes/java/main/TraceScript.btclass" - println(agent) - jvmArgsAppend = ["\"-Djmh.basedir=${project.buildDir}/.. -Dproject.version=${project.version} -Xmx128m ${agent.toString()}\""] + def agentJarPath = tasks.getByPath(':btrace-dist:btraceJar').outputs.getFiles().getSingleFile() + def scriptPath = buildDir.toPath().resolve('classes/java/main/TraceScript.btclass') + def agent = "-javaagent:${agentJarPath}=stdout=false,noServer=true,debug=false,script=${scriptPath}" + jvmArgsAppend = ["-Djmh.basedir=${buildDir.getParentFile()}", "-Dproject.version=${project.version}", "-Xmx128m", "-agentpath:/tmp/libasyncProfiler.dylib=start,event=cpu,jfr=7,file=/tmp/btrace.jfr", "${agent}"] includes = ['.*BTraceBench.*'] + profilers = ['stack'] } \ No newline at end of file diff --git a/benchmarks/agent-benchmark/src/jmh/btrace/TraceScript.java b/benchmarks/agent-benchmark/src/jmh/btrace/TraceScript.java index e279dd17c..90d52ba7c 100644 --- a/benchmarks/agent-benchmark/src/jmh/btrace/TraceScript.java +++ b/benchmarks/agent-benchmark/src/jmh/btrace/TraceScript.java @@ -1,14 +1,32 @@ -import static org.openjdk.btrace.core.BTraceUtils.*; +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.openjdk.btrace.core.annotations.BTrace; -import org.openjdk.btrace.core.annotations.Duration; -import org.openjdk.btrace.core.annotations.Kind; -import org.openjdk.btrace.core.annotations.Level; -import org.openjdk.btrace.core.annotations.Location; -import org.openjdk.btrace.core.annotations.OnMethod; -import org.openjdk.btrace.core.annotations.ProbeClassName; -import org.openjdk.btrace.core.annotations.ProbeMethodName; -import org.openjdk.btrace.core.annotations.Sampled; + +import static io.btrace.core.BTraceUtils.*; + +import io.btrace.core.annotations.BTrace; +import io.btrace.core.annotations.Duration; +import io.btrace.core.annotations.Kind; +import io.btrace.core.annotations.Level; +import io.btrace.core.annotations.Location; +import io.btrace.core.annotations.OnMethod; +import io.btrace.core.annotations.ProbeClassName; +import io.btrace.core.annotations.ProbeMethodName; +import io.btrace.core.annotations.Sampled; @BTrace public class TraceScript { diff --git a/benchmarks/agent-benchmark/src/jmh/java/benchmark/BTraceBench.java b/benchmarks/agent-benchmark/src/jmh/java/benchmark/BTraceBench.java index 4811a54cb..4592cb0c9 100644 --- a/benchmarks/agent-benchmark/src/jmh/java/benchmark/BTraceBench.java +++ b/benchmarks/agent-benchmark/src/jmh/java/benchmark/BTraceBench.java @@ -1,31 +1,23 @@ /* - * Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package benchmark; +import io.btrace.instr.MethodTracker; import java.io.IOException; -import java.io.PrintWriter; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; @@ -36,10 +28,7 @@ import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.Random; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.core.comm.CommandListener; -import org.openjdk.btrace.instr.MethodTracker; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -109,10 +98,6 @@ public void cleanup() throws IOException { long sampleCounter; long durCounter; - LinkedBlockingQueue l = new LinkedBlockingQueue<>(); - PrintWriter pw; - CommandListener cl; - @Setup public void setup() { MethodTracker.registerCounter(1, 10); @@ -303,12 +288,12 @@ public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .addProfiler("stack") - .jvmArgsPrepend( - "-javaagent:" - + bc.agentJar - + "=stdout=false,noServer=true," - + "script=" - + bc.scriptPath) + // .jvmArgsPrepend( + // "-javaagent:" + // + bc.agentJar + // + "=stdout=false,noServer=true," + // + "script=" + // + bc.scriptPath) .include(".*" + BTraceBench.class.getSimpleName() + ".*test.*") .build(); diff --git a/benchmarks/runtime-benchmarks/build.gradle b/benchmarks/runtime-benchmarks/build.gradle index 13f9becd2..dbbcb4196 100644 --- a/benchmarks/runtime-benchmarks/build.gradle +++ b/benchmarks/runtime-benchmarks/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id("me.champeau.jmh") version "0.6.3" + alias(libs.plugins.jmh) } description 'A JMH benchmark to assert the overhead imposed by various types of BTrace instrumentation.' @@ -8,14 +8,20 @@ description 'A JMH benchmark to assert the overhead imposed by various types of def env = System.getenv() def javaHome = env['JAVA_HOME'] +configurations { + compilerDeps +} + dependencies { - compile project(path: ":btrace-dist", configuration: "shadow") - compile project(":btrace-compiler") - jmh project(":btrace-instr") + implementation project(path: ":btrace-dist", configuration: "shadow") + implementation project(":btrace-compiler") + jmh project(":btrace-agent") jmh project(":btrace-runtime") - jmh project(":btrace-statsd") - jmh("org.openjdk.jmh:jmh-core:1.29") - jmh("org.openjdk.jmh:jmh-generator-annprocess:1.29") + jmh project(":btrace-extensions:btrace-statsd") + jmh libs.jmh + jmh libs.jmh.annprocess + compilerDeps project(path: ":btrace-dist", configuration: "shadow") + compilerDeps project(":btrace-compiler") } task btracec(type: JavaExec) { @@ -24,14 +30,15 @@ task btracec(type: JavaExec) { outputs.dir "${buildDir}/classes/java/main" environment('BTRACE_HOME', "$projectDir") - classpath configurations.compile - main 'org.openjdk.btrace.compiler.Compiler' + classpath configurations.compilerDeps + mainClass = 'io.btrace.compiler.Compiler' args '-d' args "${buildDir}/classes/java/main/" args '-packext' args 'btclass' args fileTree(dir: "src/jmh/btrace", include: 'TraceScript.java') } +compileJmhJava.dependsOn btracec jmhClasses.dependsOn btracec jmhJar { @@ -45,7 +52,7 @@ jmhJar { include "org/openjdk/btrace/instr/**" include 'org/openjdk/btrace/generated/**/*' include 'org/openjdk/btrace/runtime/**' - include 'org/openjdk/btrace/services/**' + // no legacy services classes to package include "joptsimple/**" include "org/apache/**" include '*.btclass' @@ -54,9 +61,8 @@ jmhJar { jmh { duplicateClassesStrategy = DuplicatesStrategy.WARN - jvmArgsAppend = ["-Djmh.basedir=${project.buildDir}/..", "-Dproject.version=${project.version}"] + jvmArgsAppend = ["-Djmh.basedir=${project.buildDir.getParent()}", "-Dproject.version=${project.version}"] // jmhVersion = '1.27' - includes = ['org.openjdk.btrace.bench.ClassFilterBenchmark'] + includes = ['io.btrace.bench.ClassFilterBenchmark'] verbosity = 'EXTRA' } - diff --git a/benchmarks/runtime-benchmarks/src/jmh/btrace/TraceScript.java b/benchmarks/runtime-benchmarks/src/jmh/btrace/TraceScript.java index 99fb5e21d..106693f2a 100644 --- a/benchmarks/runtime-benchmarks/src/jmh/btrace/TraceScript.java +++ b/benchmarks/runtime-benchmarks/src/jmh/btrace/TraceScript.java @@ -1,41 +1,59 @@ -import static org.openjdk.btrace.core.BTraceUtils.*; +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.openjdk.btrace.core.annotations.BTrace; -import org.openjdk.btrace.core.annotations.Duration; -import org.openjdk.btrace.core.annotations.Kind; -import org.openjdk.btrace.core.annotations.Level; -import org.openjdk.btrace.core.annotations.Location; -import org.openjdk.btrace.core.annotations.OnMethod; -import org.openjdk.btrace.core.annotations.ProbeClassName; -import org.openjdk.btrace.core.annotations.ProbeMethodName; -import org.openjdk.btrace.core.annotations.Sampled; + +import static io.btrace.core.BTraceUtils.*; + +import io.btrace.core.annotations.BTrace; +import io.btrace.core.annotations.Duration; +import io.btrace.core.annotations.Kind; +import io.btrace.core.annotations.Level; +import io.btrace.core.annotations.Location; +import io.btrace.core.annotations.OnMethod; +import io.btrace.core.annotations.ProbeClassName; +import io.btrace.core.annotations.ProbeMethodName; +import io.btrace.core.annotations.Sampled; @BTrace public class TraceScript { - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethod") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethod") public static void onMethodEntryEmpty(@ProbeClassName String pcn, @ProbeMethodName String pmn) {} @OnMethod( - clazz = "org.openjdk.btrace.BTraceBench", + clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodLevelNoMatch", enableAt = @Level("100")) public static void onMethodEntryEmptyLevelNoMatch( @ProbeClassName String pcn, @ProbeMethodName String pmn) {} - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethodSampled") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodSampled") @Sampled(kind = Sampled.Sampler.Const) public static void onMethodEntryEmptySampled( @ProbeClassName String pcn, @ProbeMethodName String pmn) {} @OnMethod( - clazz = "org.openjdk.btrace.BTraceBench", + clazz = "io.btrace.BTraceBench", method = "testInstrDuration", location = @Location(Kind.RETURN)) public static void onMethodRetDuration( @ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long dur) {} @OnMethod( - clazz = "org.openjdk.btrace.BTraceBench", + clazz = "io.btrace.BTraceBench", method = "testInstrDurationSampled", location = @Location(Kind.RETURN)) @Sampled(kind = Sampled.Sampler.Const) @@ -43,21 +61,21 @@ public static void onMethodRetDurationSampled( @ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long dur) {} @OnMethod( - clazz = "org.openjdk.btrace.BTraceBench", + clazz = "io.btrace.BTraceBench", method = "testInstrDurationSampledAdaptive", location = @Location(Kind.RETURN)) @Sampled public static void onMethodRetDurationSampledAdaptive( @ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long dur) {} - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethodPrintln1") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodPrintln1") public static void onMethodEntryPrintln1( @ProbeClassName String pcn, @ProbeMethodName String pmn) { println(pcn); } @OnMethod( - clazz = "org.openjdk.btrace.BTraceBench", + clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodPrintln1Sampled") @Sampled public static void onMethodEntryPrintln1Sampled( @@ -65,14 +83,14 @@ public static void onMethodEntryPrintln1Sampled( println(pcn); } - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethodPrintln2") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodPrintln2") public static void onMethodEntryPrintln2( @ProbeClassName String pcn, @ProbeMethodName String pmn) { println(pcn); println(pmn); } - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethodPrintln3") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodPrintln3") public static void onMethodEntryPrintln3( @ProbeClassName String pcn, @ProbeMethodName String pmn) { println(pcn); @@ -80,7 +98,7 @@ public static void onMethodEntryPrintln3( println(pmn); } - @OnMethod(clazz = "org.openjdk.btrace.BTraceBench", method = "testInstrumentedMethodPrintln24") + @OnMethod(clazz = "io.btrace.BTraceBench", method = "testInstrumentedMethodPrintln24") public static void onMethodEntryPrintln24( @ProbeClassName String pcn, @ProbeMethodName String pmn) { println(pcn); diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ClassFilterBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ClassFilterBenchmark.java new file mode 100644 index 000000000..a21048947 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ClassFilterBenchmark.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import io.btrace.instr.ClassFilter; +import io.btrace.instr.OnMethod; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class ClassFilterBenchmark { + private static final String CLASS_A_PKG = "io.btrace.benchmark"; + private static final String CLASS_A_NAME = "ClassA"; + private static final String CLASS_A = CLASS_A_PKG + "." + CLASS_A_NAME; + + private ClassFilter cfSimple; + private ClassFilter cfRegexName; + private ClassFilter cfSubtype; + + @Setup + public void setup() { + OnMethod simpleClassFilter = new OnMethod(); + simpleClassFilter.setClazz(CLASS_A); + + OnMethod regexNameFilter = new OnMethod(); + regexNameFilter.setClazz("/.*\\." + CLASS_A_NAME + "/"); + + OnMethod subtypeFilter = new OnMethod(); + subtypeFilter.setClazz("+java.util.List"); + + cfSimple = new ClassFilter(Collections.singleton(simpleClassFilter)); + cfRegexName = new ClassFilter(Collections.singleton(regexNameFilter)); + cfSubtype = new ClassFilter(Collections.singleton(subtypeFilter)); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testSimpleClassNameMatch(Blackhole bh) { + bh.consume(cfSimple.isNameMatching(CLASS_A)); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testRegexNameMatch(Blackhole bh) { + bh.consume(cfRegexName.isNameMatching(CLASS_A)); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testSubtypeMatch(Blackhole bh) { + bh.consume(cfSubtype.isCandidate(ArrayList.class)); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testSubtypeNoMatch(Blackhole bh) { + bh.consume(cfSubtype.isCandidate(String.class)); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("stack") + .include(".*" + ClassFilterBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/DispatchBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/DispatchBenchmark.java new file mode 100644 index 000000000..135191506 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/DispatchBenchmark.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * JMH benchmark measuring INVOKEDYNAMIC dispatch overhead as simulated by {@link ConstantCallSite} + * — the mechanism used by {@code IndyDispatcher}. + * + *

Compares a plain static method call ({@link #baseline}) against dispatch through a {@link + * ConstantCallSite} ({@link #instrumented}). + */ +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class DispatchBenchmark { + + private MethodHandle constantTarget; + private MethodHandle mutableTarget; + + @Setup(Level.Trial) + public void setup() throws Exception { + // Build a ConstantCallSite targeting the static handler method, simulating what + // IndyDispatcher.bootstrap() produces. + MethodHandle mh = + MethodHandles.lookup() + .findStatic( + DispatchBenchmark.class, + "probeHandler", + MethodType.methodType(void.class, int.class)); + CallSite cs = new ConstantCallSite(mh); + constantTarget = cs.dynamicInvoker(); + MutableCallSite mcs = new MutableCallSite(mh.type()); + mcs.setTarget(mh); + mutableTarget = mcs.dynamicInvoker(); + } + + /** Direct static call — baseline with zero dispatch overhead. */ + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) + @Benchmark + public void baseline(Blackhole bh) { + probeHandler(42); + } + + /** Dispatch through a ConstantCallSite — simulates IndyDispatcher-resolved call site. */ + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) + @Benchmark + public void instrumented(Blackhole bh) throws Throwable { + constantTarget.invokeExact(42); + } + + /** + * Dispatch through a MutableCallSite whose target is stable (set once, never re-linked). + * Simulates the IndyDispatcher-post-detach-safety variant. HotSpot should treat the target + * as @Stable and inline through it comparably to ConstantCallSite. + */ + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) + @Benchmark + public void instrumentedMutable(Blackhole bh) throws Throwable { + mutableTarget.invokeExact(42); + } + + /** Simulated probe handler method. */ + public static void probeHandler(int value) { + // intentionally empty — we measure dispatch cost, not handler body cost + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/OnMethodTemplateBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/OnMethodTemplateBenchmark.java new file mode 100644 index 000000000..a3231b5a7 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/OnMethodTemplateBenchmark.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import io.btrace.core.ArgsMap; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class OnMethodTemplateBenchmark { + private ArgsMap argsMap; + + @Setup + public void setup() { + argsMap = new ArgsMap(new String[] {"arg1=val1"}); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testEmptyTemplate(Blackhole bh) { + bh.consume(argsMap.template("")); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testMatchTemplate(Blackhole bh) { + bh.consume(argsMap.template("this-is-${arg1}")); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testNoMatchTemplate(Blackhole bh) { + bh.consume(argsMap.template("this-is-${arg2}")); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("stack") + .include(".*" + OnMethodTemplateBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProbeLoadingBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProbeLoadingBenchmark.java new file mode 100644 index 000000000..0489c8446 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProbeLoadingBenchmark.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import io.btrace.core.SharedSettings; +import io.btrace.instr.BTraceProbe; +import io.btrace.instr.BTraceProbeFactory; +import java.io.*; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class ProbeLoadingBenchmark { + private InputStream classStream; + private BTraceProbeFactory bpf; + + @Setup(Level.Trial) + public void setup() throws Exception { + bpf = new BTraceProbeFactory(SharedSettings.GLOBAL); + } + + @Setup(Level.Invocation) + public void setupRun() throws Exception { + classStream = ProbeLoadingBenchmark.class.getResourceAsStream("/TraceScript.btclass"); + } + + @TearDown(Level.Invocation) + public void tearDownRun() throws Exception { + classStream.close(); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) + @Benchmark + public void testBTraceProbeNew(Blackhole bh) throws Exception { + BTraceProbe bp = bpf.createProbe(classStream); + if (bp == null) { + throw new NullPointerException(); + } + bh.consume(bp); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("stack") + .include(".*" + ProbeLoadingBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProfilerBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProfilerBenchmark.java new file mode 100644 index 000000000..5e6cfb4c9 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/ProfilerBenchmark.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import io.btrace.runtime.profiling.MethodInvocationProfiler; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.VerboseMode; + +/** + * Basic benchmark for the performance of {@linkplain MethodInvocationProfiler} + * + * @author Jaroslav Bachorik + */ +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class ProfilerBenchmark { + private MethodInvocationProfiler mip1; + private MethodInvocationProfiler mip2; + + @Setup + public void setup() { + mip1 = new MethodInvocationProfiler(1); + mip2 = new MethodInvocationProfiler(500); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(1) + public void testOneMethodSingleThread() { + mip1.recordEntry("a"); + mip1.recordExit("a", 1); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(1) + public void testTwoMethods01Thread() { + mip2.recordEntry("a"); + mip2.recordEntry("b"); + mip2.recordExit("b", 10); + mip2.recordExit("a", 1); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(2) + public void testTwoMethods02Threads() { + mip2.recordEntry("a"); + mip2.recordEntry("b"); + mip2.recordExit("b", 10); + mip2.recordExit("a", 1); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(4) + public void testTwoMethods04Threads() { + mip2.recordEntry("a"); + mip2.recordEntry("b"); + mip2.recordExit("b", 10); + mip2.recordExit("a", 1); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(8) + public void testTwoMethods08Threads() { + mip2.recordEntry("a"); + mip2.recordEntry("b"); + mip2.recordExit("b", 10); + mip2.recordExit("a", 1); + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(16) + public void testTwoMethods16Threads() { + mip2.recordEntry("a"); + mip2.recordEntry("b"); + mip2.recordExit("b", 10); + mip2.recordExit("a", 1); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("stack") + .verbosity(VerboseMode.NORMAL) + .include(".*" + ProfilerBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StatsdBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StatsdBenchmark.java new file mode 100644 index 000000000..85090645e --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StatsdBenchmark.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import io.btrace.statsd.Statsd; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +/** + * Basic benchmark for the performance of {@linkplain Statsd} + * + * @author Jaroslav Bachorik + */ +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class StatsdBenchmark { + private Statsd c; + + @Setup + public void setup() { + // Inline no-op impl — the benchmark measures dispatch through the extension API, + // not the network-layer Statsd implementation. + c = + new Statsd() { + @Override + public void increment(String name) {} + + @Override + public void increment(String name, String tags) {} + }; + } + + @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + @Threads(1) + public void testIncrement_1() { + c.increment("g1"); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("stack") + .include(".*" + StatsdBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StringOpBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StringOpBenchmark.java new file mode 100644 index 000000000..22f1b4ee1 --- /dev/null +++ b/benchmarks/runtime-benchmarks/src/jmh/java/io/btrace/bench/StringOpBenchmark.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.bench; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +public class StringOpBenchmark { + private static final String STRING_PART = "h"; + + StringBuilder sb; + String st; + String res; + + @Setup + public void setup() { + st = ""; + } + + @Setup(Level.Invocation) + public void setupEach() { + sb = new StringBuilder(); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testStringBuilder() { + sb.append(STRING_PART).append(STRING_PART); + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testStringPlus() { + res = st + STRING_PART + STRING_PART; + } + + @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) + @Benchmark + public void testStrCat() { + res = st.concat(STRING_PART).concat(STRING_PART); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .addProfiler("gc") + .include(".*" + StringOpBenchmark.class.getSimpleName() + ".*test.*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ClassFilterBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ClassFilterBenchmark.java deleted file mode 100644 index 5cc22effc..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ClassFilterBenchmark.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2018, Jaroslav Bachorik . - * All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Copyright owner designates - * this particular file as subject to the "Classpath" exception as provided - * by the owner in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - */ -package org.openjdk.btrace.bench; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.instr.ClassFilter; -import org.openjdk.btrace.instr.OnMethod; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class ClassFilterBenchmark { - private static final String CLASS_A_PKG = "org.openjdk.btrace.benchmark"; - private static final String CLASS_A_NAME = "ClassA"; - private static final String CLASS_A = CLASS_A_PKG + "." + CLASS_A_NAME; - - private ClassFilter cfSimple; - private ClassFilter cfRegexName; - private ClassFilter cfSubtype; - - @Setup - public void setup() { - OnMethod simpleClassFilter = new OnMethod(); - simpleClassFilter.setClazz(CLASS_A); - - OnMethod regexNameFilter = new OnMethod(); - regexNameFilter.setClazz("/.*\\." + CLASS_A_NAME + "/"); - - OnMethod subtypeFilter = new OnMethod(); - subtypeFilter.setClazz("+java.util.List"); - - cfSimple = new ClassFilter(Collections.singleton(simpleClassFilter)); - cfRegexName = new ClassFilter(Collections.singleton(regexNameFilter)); - cfSubtype = new ClassFilter(Collections.singleton(subtypeFilter)); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testSimpleClassNameMatch(Blackhole bh) { - bh.consume(cfSimple.isNameMatching(CLASS_A)); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testRegexNameMatch(Blackhole bh) { - bh.consume(cfRegexName.isNameMatching(CLASS_A)); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testSubtypeMatch(Blackhole bh) { - bh.consume(cfSubtype.isCandidate(ArrayList.class)); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testSubtypeNoMatch(Blackhole bh) { - bh.consume(cfSubtype.isCandidate(String.class)); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("stack") - .include(".*" + ClassFilterBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/OnMethodTemplateBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/OnMethodTemplateBenchmark.java deleted file mode 100644 index 03f27095f..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/OnMethodTemplateBenchmark.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2018, Jaroslav Bachorik . - * All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Copyright owner designates - * this particular file as subject to the "Classpath" exception as provided - * by the owner in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - */ -package org.openjdk.btrace.bench; - -import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.core.ArgsMap; -import org.openjdk.btrace.core.DebugSupport; -import org.openjdk.btrace.core.SharedSettings; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class OnMethodTemplateBenchmark { - private ArgsMap argsMap; - - @Setup - public void setup() { - argsMap = new ArgsMap(new String[] {"arg1=val1"}, new DebugSupport(SharedSettings.GLOBAL)); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testEmptyTemplate(Blackhole bh) { - bh.consume(argsMap.template("")); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testMatchTemplate(Blackhole bh) { - bh.consume(argsMap.template("this-is-${arg1}")); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testNoMatchTemplate(Blackhole bh) { - bh.consume(argsMap.template("this-is-${arg2}")); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("stack") - .include(".*" + OnMethodTemplateBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProbeLoadingBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProbeLoadingBenchmark.java deleted file mode 100644 index 65ef41a54..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProbeLoadingBenchmark.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package org.openjdk.btrace.bench; - -import java.io.*; -import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.core.SharedSettings; -import org.openjdk.btrace.instr.BTraceProbe; -import org.openjdk.btrace.instr.BTraceProbeFactory; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class ProbeLoadingBenchmark { - private InputStream classStream; - private BTraceProbeFactory bpf; - - @Setup(Level.Trial) - public void setup() throws Exception { - bpf = new BTraceProbeFactory(SharedSettings.GLOBAL); - } - - @Setup(Level.Invocation) - public void setupRun() throws Exception { - classStream = ProbeLoadingBenchmark.class.getResourceAsStream("/TraceScript.btclass"); - } - - @TearDown(Level.Invocation) - public void tearDownRun() throws Exception { - classStream.close(); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) - @Benchmark - public void testBTraceProbeNew(Blackhole bh) throws Exception { - BTraceProbe bp = bpf.createProbe(classStream); - if (bp == null) { - throw new NullPointerException(); - } - bh.consume(bp); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("stack") - .include(".*" + ProbeLoadingBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProfilerBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProfilerBenchmark.java deleted file mode 100644 index 8b07bb7df..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/ProfilerBenchmark.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package org.openjdk.btrace.bench; - -import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.runtime.profiling.MethodInvocationProfiler; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; -import org.openjdk.jmh.runner.options.VerboseMode; - -/** - * Basic benchmark for the performance of {@linkplain MethodInvocationProfiler} - * - * @author Jaroslav Bachorik - */ -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class ProfilerBenchmark { - private MethodInvocationProfiler mip1; - private MethodInvocationProfiler mip2; - - @Setup - public void setup() { - mip1 = new MethodInvocationProfiler(1); - mip2 = new MethodInvocationProfiler(500); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(1) - public void testOneMethodSingleThread() { - mip1.recordEntry("a"); - mip1.recordExit("a", 1); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(1) - public void testTwoMethods01Thread() { - mip2.recordEntry("a"); - mip2.recordEntry("b"); - mip2.recordExit("b", 10); - mip2.recordExit("a", 1); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(2) - public void testTwoMethods02Threads() { - mip2.recordEntry("a"); - mip2.recordEntry("b"); - mip2.recordExit("b", 10); - mip2.recordExit("a", 1); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(4) - public void testTwoMethods04Threads() { - mip2.recordEntry("a"); - mip2.recordEntry("b"); - mip2.recordExit("b", 10); - mip2.recordExit("a", 1); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(8) - public void testTwoMethods08Threads() { - mip2.recordEntry("a"); - mip2.recordEntry("b"); - mip2.recordExit("b", 10); - mip2.recordExit("a", 1); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(16) - public void testTwoMethods16Threads() { - mip2.recordEntry("a"); - mip2.recordEntry("b"); - mip2.recordExit("b", 10); - mip2.recordExit("a", 1); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("stack") - .verbosity(VerboseMode.NORMAL) - .include(".*" + ProfilerBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StatsdBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StatsdBenchmark.java deleted file mode 100644 index 6c8b91902..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StatsdBenchmark.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package org.openjdk.btrace.bench; - -import java.util.concurrent.TimeUnit; -import org.openjdk.btrace.statsd.Statsd; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -/** - * Basic benchmark for the performance of {@linkplain Statsd} - * - * @author Jaroslav Bachorik - */ -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class StatsdBenchmark { - private Statsd c; - - @Setup - public void setup() { - c = Statsd.getInstance(); - } - - @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - @Threads(1) - public void testGauge_1() { - c.gauge("g1", 10); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("stack") - .include(".*" + StatsdBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StringOpBenchmark.java b/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StringOpBenchmark.java deleted file mode 100644 index a99cf2830..000000000 --- a/benchmarks/runtime-benchmarks/src/jmh/java/org/openjdk/btrace/bench/StringOpBenchmark.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package org.openjdk.btrace.bench; - -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -@State(Scope.Thread) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@Fork(1) -@BenchmarkMode(Mode.AverageTime) -public class StringOpBenchmark { - private static final String STRING_PART = "h"; - - StringBuilder sb; - String st; - String res; - - @Setup - public void setup() { - st = ""; - } - - @Setup(Level.Invocation) - public void setupEach() { - sb = new StringBuilder(); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testStringBuilder() { - sb.append(STRING_PART).append(STRING_PART); - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testStringPlus() { - res = st + STRING_PART + STRING_PART; - } - - @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) - @Measurement(iterations = 5, time = 1200, timeUnit = TimeUnit.MILLISECONDS) - @Benchmark - public void testStrCat() { - res = st.concat(STRING_PART).concat(STRING_PART); - } - - public static void main(String[] args) throws Exception { - Options opt = - new OptionsBuilder() - .addProfiler("gc") - .include(".*" + StringOpBenchmark.class.getSimpleName() + ".*test.*") - .build(); - - new Runner(opt).run(); - } -} diff --git a/btrace-agent/build.gradle b/btrace-agent/build.gradle index 63b32b415..9ae0fca2f 100644 --- a/btrace-agent/build.gradle +++ b/btrace-agent/build.gradle @@ -1,5 +1,153 @@ +import java.nio.file.Files +import java.nio.file.Paths + +compileJava { + javaCompiler = javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +compileTestJava { + options.fork = true + options.forkOptions.executable = "${getJavac(8)}" +} + +sourceSets { + java24 { + java { + srcDirs = ['src/main/java24'] + } + } +} + +compileJava24Java { + sourceCompatibility = 24 + targetCompatibility = 24 + javaCompiler = javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(24)) + } +} + +javadoc { + // Javadoc also needs access to internal APIs - but Java 8 javadoc doesn't support --add-exports + // So we exclude the file that uses internal APIs from javadoc generation + exclude '**/PerfReaderImpl.java' +} + dependencies { - compile files("${JAVA_8_HOME}/lib/tools.jar") - compile project(':btrace-runtime') - compile project(':btrace-instr') -} \ No newline at end of file + implementation libs.slf4j + implementation libs.asm + implementation libs.asm.tree + implementation libs.asm.util + implementation libs.autoService + implementation libs.jctools + + def toolsJar = getToolsJar(); + if (toolsJar.getAsFile().exists()) { + compileOnly files("${toolsJar}") + runtimeOnly files("${toolsJar}") + } + implementation project(':btrace-core') + implementation project(':btrace-runtime') + implementation project(':btrace-compiler') + + java24Implementation files(sourceSets.main.output.classesDirs) { + builtBy compileJava + } + java24Implementation project(':btrace-core') + java24Implementation project(':btrace-runtime') + java24Implementation libs.asm + java24Implementation libs.slf4j + + testImplementation libs.asm.util + testImplementation libs.slf4j.simple + testImplementation libs.junit.jupiter + testImplementation project(':btrace-extensions:btrace-statsd') + testImplementation project(':btrace-extensions:btrace-utils') +} + +jar { + into('') { + from sourceSets.java24.output + } +} + +// Exclude sources that rely on JDK-internal modules from Javadoc to avoid missing-package errors +tasks.named('javadoc').configure { + exclude 'org/openjdk/btrace/agent/PerfReaderImpl.java' +} + +task compileTestProbes { + // Mirror the integration-tests compiler invocation so dependent project outputs and extension + // API JARs are available before the compiler classpath is resolved. + dependsOn compileTestJava, processTestResources, + ':btrace-agent:jar', + ':btrace-boot:jar', + ':btrace-compiler:jar', + ':btrace-extensions:btrace-utils:buildApiJar', + ':btrace-extensions:btrace-statsd:buildApiJar' + doLast { + def path = project(':btrace-agent').sourceSets.main.runtimeClasspath + + def loader = new URLClassLoader(path.collect { f -> f.toURL() } as URL[]) + def compiler = loader.loadClass('io.btrace.compiler.Compiler') + // Use test runtime classpath so test-only extension APIs are available + def rtCp = sourceSets.test.runtimeClasspath + + def extraCp = files( + buildDir.toPath().resolve("classes/java/test"), + buildDir.toPath().resolve("classes/java/java11_dummy"), + buildDir.toPath().resolve("resources/test") + ) + // Resolve API JARs via archiveFile (not files(task)) so the path is concrete and + // the task dependency above guarantees they exist. + def utilsApi = files(project(':btrace-extensions:btrace-utils').tasks.named('buildApiJar').get().archiveFile.get().asFile) + def statsdApi = files(project(':btrace-extensions:btrace-statsd').tasks.named('buildApiJar').get().archiveFile.get().asFile) + def fullCp = rtCp.plus(extraCp).plus(utilsApi).plus(statsdApi) + def cpPath = fullCp.getAsPath() + def args = [ + "-cp", cpPath, + "-d", buildDir.toPath().resolve("classes") + ] + + def files = fileTree(dir: "src/test/btrace", include: '**/*.java', exclude: 'verifier/**/*.java').findAll { + it != null + }.collect { it } + + args.addAll(files) + + // Keep compiler resource lookups aligned with the loader that resolved the compiler classes. + def prev = Thread.currentThread().contextClassLoader + def oldCp = System.getProperty('java.class.path') + try { + Thread.currentThread().contextClassLoader = loader + System.setProperty('btrace.allow.undeclared.services', 'true') + System.setProperty('java.class.path', cpPath) + compiler.main(args as String[]) + } finally { + Thread.currentThread().contextClassLoader = prev + if (oldCp != null) System.setProperty('java.class.path', oldCp) + System.clearProperty('btrace.allow.undeclared.services') + } + } +} + +test { + dependsOn cleanTest + inputs.files compileTestProbes.outputs + testLogging.showStandardStreams = true + + def props = new Properties() + props.load(Files.newInputStream(Paths.get(System.getenv("JAVA_HOME"), "release"))) + if (!props.getProperty("JAVA_VERSION")?.contains("1.8")) { + jvmArgs '-XX:+IgnoreUnrecognizedVMOptions', + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/jdk.internal.reflect=ALL-UNNAMED', + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', '--add-exports', 'java.base/jdk.internal.reflect=ALL-UNNAMED' + } + if (project.hasProperty("updateTestData")) { + jvmArgs '-Dupdate.test.data=true' + } + jvmArgs "-Dtest.resources=${projectDir}/src/test/resources" + jvmArgs "-Dproject.version=${project.version}" +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/AgentManifestLibs.java b/btrace-agent/src/main/java/io/btrace/agent/AgentManifestLibs.java new file mode 100644 index 000000000..a668e8859 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/AgentManifestLibs.java @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class AgentManifestLibs { + private static final Logger log = LoggerFactory.getLogger(AgentManifestLibs.class); + + // Custom BTrace manifest attributes + private static final String ATTR_BTRACE_BOOT_LIBS = "BTrace-Boot-Libs"; + private static final String ATTR_BTRACE_SYSTEM_LIBS = "BTrace-System-Libs"; + private static final String ATTR_BTRACE_LIBS_ROOT = "BTrace-Libs-Root"; + private static final String ATTR_BTRACE_LIBS_PROFILE = "BTrace-Libs-Profile"; + // Standard attribute the JVM also understands; we read to unify behavior + private static final String ATTR_BOOT_CLASS_PATH = "Boot-Class-Path"; + + static final class ResolvedLibs { + final List bootJars; + final List systemJars; + + ResolvedLibs(List bootJars, List systemJars) { + this.bootJars = bootJars; + this.systemJars = systemJars; + } + } + + private AgentManifestLibs() {} + + static ResolvedLibs resolveFromManifest(Class anchor) { + boolean ignore = Boolean.getBoolean("btrace.ignoreManifestLibs"); + if (ignore) { + if (log.isDebugEnabled()) + log.debug("Ignoring manifest libs (btrace.ignoreManifestLibs=true)"); + return new ResolvedLibs(Collections.emptyList(), Collections.emptyList()); + } + + Path agentJarPath = locateAgentPath(anchor); + Manifest mf = readManifest(agentJarPath); + if (mf == null) { + if (log.isDebugEnabled()) log.debug("No manifest found for agent; skipping manifest libs"); + return new ResolvedLibs(Collections.emptyList(), Collections.emptyList()); + } + Path baseDir = agentJarPath != null ? agentJarPath.getParent() : null; + // Default libs root: BTRACE_HOME/libs if the agent is under .../libs/btrace-agent.jar + Path libsRoot = resolveLibsRoot(mf, baseDir); + + Set boot = new LinkedHashSet<>(); + Set sys = new LinkedHashSet<>(); + + // 1) Standard Boot-Class-Path + addEntries(boot, getAttr(mf, ATTR_BOOT_CLASS_PATH), baseDir); + // 2) Custom BTrace attributes + addEntries(boot, getAttr(mf, ATTR_BTRACE_BOOT_LIBS), baseDir); + addEntries(sys, getAttr(mf, ATTR_BTRACE_SYSTEM_LIBS), baseDir); + + // 3) Optional profile scan + String profile = getAttr(mf, ATTR_BTRACE_LIBS_PROFILE); + if (profile != null && libsRoot != null) { + Path profileRoot = libsRoot.resolve(profile); + scanLibTree(profileRoot.resolve("boot"), boot); + scanLibTree(profileRoot.resolve("system"), sys); + } + + // Safety: by default restrict to agent home unless explicitly allowed + boolean allowExternal = Boolean.getBoolean("btrace.allowExternalLibs"); + Path home = tryComputeBTraceHome(agentJarPath); + List bootList = filterAndNormalize(boot, home, allowExternal); + List sysList = filterAndNormalize(sys, home, allowExternal); + + if (log.isDebugEnabled()) { + log.debug("Manifest-resolved boot libs: {}", bootList); + log.debug("Manifest-resolved system libs: {}", sysList); + } + return new ResolvedLibs(bootList, sysList); + } + + private static Manifest readManifest(Path agentPath) { + if (agentPath == null) return null; + try { + if (Files.isRegularFile(agentPath) + && agentPath.toString().toLowerCase(Locale.ROOT).endsWith(".jar")) { + try (JarFile jf = new JarFile(agentPath.toFile())) { + return jf.getManifest(); + } + } + // exploded directory + Path mf = agentPath.resolve("META-INF").resolve("MANIFEST.MF"); + if (Files.exists(mf)) { + try (FileInputStream fis = new FileInputStream(mf.toFile())) { + return new Manifest(fis); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) log.debug("Failed to read manifest: {}", e.toString()); + } + return null; + } + + private static Path locateAgentPath(Class anchor) { + try { + URL url = anchor.getProtectionDomain().getCodeSource().getLocation(); + if (url == null) return null; + URI uri = url.toURI(); + return Paths.get(uri); + } catch (URISyntaxException | IllegalArgumentException | FileSystemNotFoundException e) { + if (log.isDebugEnabled()) log.debug("Failed to locate agent path: {}", e.toString()); + return null; + } + } + + private static String getAttr(Manifest mf, String key) { + if (mf.getMainAttributes() == null) return null; + String v = mf.getMainAttributes().getValue(key); + return v != null && !v.trim().isEmpty() ? v.trim() : null; + } + + static void addEntries(Set out, String value, Path baseDir) { + if (value == null || value.isEmpty()) return; + // Space-separated entries (manifest convention) + String[] parts = value.split("\\s+"); + for (String part : parts) { + Path p = resolveEntry(part, baseDir); + if (p != null) out.add(p); + } + } + + static Path resolveEntry(String entry, Path baseDir) { + try { + if (entry.startsWith("file:")) { + return Paths.get(new URI(entry)); + } + } catch (Exception ignored) { + // fallthrough to path resolution + } + Path p = Paths.get(entry); + if (!p.isAbsolute() && baseDir != null) { + p = baseDir.resolve(p).normalize(); + } + return p; + } + + static void scanLibTree(Path root, Set out) { + if (root == null || !Files.exists(root)) return; + try (java.util.stream.Stream stream = Files.walk(root)) { + stream + .filter(Files::isRegularFile) + .filter(f -> f.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".jar")) + .forEach(out::add); + } catch (IOException e) { + if (log.isDebugEnabled()) log.debug("Failed to scan libs at {}: {}", root, e.toString()); + } + } + + private static Path resolveLibsRoot(Manifest mf, Path baseDir) { + String root = getAttr(mf, ATTR_BTRACE_LIBS_ROOT); + if (root != null) { + Path p = resolveEntry(root, baseDir); + if (p != null) return p; + } + if (baseDir == null) return null; + // If agent is at .../libs/btrace-agent.jar, prefer .../btrace-libs + File parent = baseDir.toFile(); + if (parent.getName().equals("libs")) { + return parent.getParentFile() != null + ? parent.getParentFile().toPath().resolve("btrace-libs") + : baseDir.resolve("..").resolve("btrace-libs").normalize(); + } + return baseDir.resolve("btrace-libs"); + } + + private static Path tryComputeBTraceHome(Path agentJar) { + if (agentJar == null) return null; + File f = agentJar.toFile(); + File parent = f.getParentFile(); + if (parent != null && parent.getName().equals("libs")) { + File home = parent.getParentFile(); + if (home != null) return home.toPath(); + } + // fallback: parent dir of agent JAR + return parent != null ? parent.toPath() : null; + } + + /** + * Filters {@code entries} to JAR paths that exist and are within {@code home} (unless {@code + * allowExternal} is {@code true}), then returns them as a list. + * + *

Ordering contract: The returned list preserves the iteration order of + * {@code entries}. Callers pass a {@link java.util.LinkedHashSet} built by processing manifest + * attributes in declaration order, so the list reflects manifest iteration order. When the same + * class name is defined in more than one JAR the JVM resolves the first matching entry; + * callers must therefore ensure that the set passed in is already in the desired precedence order + * before invoking this method. + * + *

Package-private for testing. + */ + static List filterAndNormalize(Set entries, Path home, boolean allowExternal) { + List out = new ArrayList<>(); + Path realHome = null; + if (!allowExternal && home != null) { + try { + realHome = home.toRealPath(); + } catch (IOException e) { + if (log.isDebugEnabled()) + log.debug("toRealPath failed for home {}: {}", home, e.getMessage()); + realHome = home.toAbsolutePath().normalize(); + } + } + for (Path p : entries) { + try { + Path np = p.toAbsolutePath().normalize(); + if (!Files.exists(np)) { + log.info("Skipping non-existent manifest entry: {}", np); + continue; + } + if (!np.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".jar")) { + log.info("Skipping non-jar manifest entry: {}", np); + continue; + } + if (!allowExternal && realHome != null) { + try { + Path rp = np.toRealPath(); + if (!rp.startsWith(realHome)) { + log.warn("Rejecting manifest lib outside BTRACE_HOME: {}", rp); + continue; + } + } catch (IOException e) { + // np.toRealPath() failed; fall back to the pre-resolved realHome (which may itself + // be a normalized non-canonical path if its toRealPath() failed above). + if (log.isDebugEnabled()) log.debug("toRealPath failed for {}: {}", np, e.getMessage()); + if (!np.startsWith(realHome)) { + log.warn( + "Rejecting manifest lib outside BTRACE_HOME (symlink resolution failed, using normalized path): {}", + np); + continue; + } + log.warn( + "Symlink resolution failed for manifest lib {}; accepted against normalized BTRACE_HOME only", + np); + } + } + out.add(np); + } catch (Exception e) { + log.warn("Failed resolving manifest entry {}: {}", p, e.toString()); + } + } + return out; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/AgentRuntimeEnvironment.java b/btrace-agent/src/main/java/io/btrace/agent/AgentRuntimeEnvironment.java new file mode 100644 index 000000000..cc8eb6fb7 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/AgentRuntimeEnvironment.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.extensions.RuntimeEnvironment; + +/** + * Agent-side implementation of {@link RuntimeEnvironment} passed to {@link + * io.btrace.core.extensions.ExtensionConfigurator} instances during probe auto-selection. + */ +final class AgentRuntimeEnvironment implements RuntimeEnvironment { + + private final ClassLoader appLoader; + + AgentRuntimeEnvironment() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + appLoader = cl != null ? cl : ClassLoader.getSystemClassLoader(); + } + + @Override + public boolean hasClass(String className) { + try { + Class.forName(className, false, appLoader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public String getSystemProperty(String key) { + return System.getProperty(key); + } + + @Override + public String getSystemProperty(String key, String defaultValue) { + return System.getProperty(key, defaultValue); + } + + @Override + public String getEnv(String name) { + return System.getenv(name); + } + + @Override + public ClassLoader getClassLoader() { + return appLoader; + } + + @Override + public String getMainClassName() { + // sun.java.command = "mainClass [args]" on HotSpot; first token is the main class or jar + String cmd = System.getProperty("sun.java.command"); + if (cmd != null && !cmd.isEmpty()) { + int space = cmd.indexOf(' '); + return space > 0 ? cmd.substring(0, space) : cmd; + } + return null; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/Client.java b/btrace-agent/src/main/java/io/btrace/agent/Client.java new file mode 100644 index 000000000..9f9920962 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/Client.java @@ -0,0 +1,684 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.BTraceRuntimeBridge; +import io.btrace.core.SharedSettings; +import io.btrace.core.comm.Command; +import io.btrace.core.comm.CommandListener; +import io.btrace.core.comm.ErrorCommand; +import io.btrace.core.comm.ExitCommand; +import io.btrace.core.comm.InstrumentCommand; +import io.btrace.core.comm.MessageCommand; +import io.btrace.core.comm.RenameCommand; +import io.btrace.core.comm.RetransformationStartNotification; +import io.btrace.core.comm.StatusCommand; +import io.btrace.core.extensions.Permission; +import io.btrace.core.extensions.PermissionSet; +import io.btrace.extension.ExtensionDescriptorDTO; +import io.btrace.extension.ExtensionLoader; +import io.btrace.extension.ExtensionRegistry; +import io.btrace.instr.BTraceProbe; +import io.btrace.instr.BTraceProbeFactory; +import io.btrace.instr.BTraceProbePersisted; +import io.btrace.instr.BTraceTransformer; +import io.btrace.instr.ClassCache; +import io.btrace.instr.ClassFilter; +import io.btrace.instr.ClassInfo; +import io.btrace.instr.InstrumentUtils; +import io.btrace.instr.Instrumentor; +import io.btrace.instr.MethodTrackingContext; +import io.btrace.runtime.BTraceRuntimeAccess; +import io.btrace.runtime.BTraceRuntimes; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.annotation.Annotation; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.lang.management.ManagementFactory; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class that represents a BTrace client at the BTrace agent. + * + * @author A. Sundararajan + * @author J. Bachorik (j.bachorik@btrace.io) + */ +abstract class Client implements CommandListener { + private static final Logger log = LoggerFactory.getLogger(Client.class); + + private static final Map CLIENTS = new ConcurrentHashMap<>(); + private static final Map WRITER_MAP = new HashMap<>(); + private static final Pattern SYSPROP_PTN = Pattern.compile("\\$\\{(.+?)}"); + + static { + ClassFilter.class.getClassLoader(); + InstrumentUtils.class.getClassLoader(); + Instrumentor.class.getClassLoader(); + ClassReader.class.getClassLoader(); + ClassWriter.class.getClassLoader(); + Annotation.class.getClassLoader(); + MethodTrackingContext.class.getClassLoader(); + ClassCache.class.getClassLoader(); + ClassInfo.class.getClassLoader(); + } + + private final Instrumentation inst; + final SharedSettings settings; + final ArgsMap argsMap; + private final BTraceTransformer transformer; + volatile PrintWriter out; + private volatile BTraceRuntime.Impl runtime; + private volatile String outputName; + private BTraceProbe probe; + private Timer flusher; + private volatile boolean initialized = false; + private volatile boolean shuttingDown = false; + final UUID id = UUID.randomUUID(); + + Client(ClientContext ctx) { + this(ctx.getInstr(), ctx.getArguments(), ctx.getSettings(), ctx.getTransformer()); + } + + private Client(Instrumentation inst, ArgsMap argsMap, SharedSettings s, BTraceTransformer t) { + this.inst = inst; + this.argsMap = argsMap; + settings = s != null ? s : SharedSettings.GLOBAL; + transformer = t; + + setupWriter(); + CLIENTS.put(id, this); + } + + private static String pid() { + String pName = ManagementFactory.getRuntimeMXBean().getName(); + if (pName != null && pName.length() > 0) { + String[] parts = pName.split("@"); + if (parts.length == 2) { + return parts[0]; + } + } + + return "-1"; + } + + protected final void initialize() { + initialized = true; + } + + @SuppressWarnings("DefaultCharset") + private final void setupWriter() { + String outputFile = settings.getOutputFile(); + if (outputFile == null || outputFile.equals("::null") || outputFile.equals("/dev/null")) return; + + if (!outputFile.equals("::stdout")) { + String outputDir = settings.getScriptDir(); + String output = (outputDir != null ? outputDir + File.separator : "") + outputFile; + outputFile = templateOutputFileName(output); + log.info("Redirecting output to {}", outputFile); + } + out = WRITER_MAP.get(outputFile); + if (out == null) { + if (outputFile.equals("::stdout")) { + out = new PrintWriter(System.out); + } else { + if (settings.getFileRollMilliseconds() > 0) { + out = + new PrintWriter( + new BufferedWriter( + TraceOutputWriter.rollingFileWriter(new File(outputFile), settings))); + } else { + out = + new PrintWriter( + new BufferedWriter(TraceOutputWriter.fileWriter(new File(outputFile)))); + } + } + WRITER_MAP.put(outputFile, out); + out.append("### BTrace Log: ") + .append(DateFormat.getInstance().format(new Date())) + .append("\n\n"); + startFlusher(); + } + outputName = outputFile; + } + + private void startFlusher() { + int flushInterval; + String flushIntervalStr = System.getProperty("io.btrace.FileClient.flush"); + if (flushIntervalStr == null) { + flushIntervalStr = System.getProperty("com.sun.btrace.FileClient.flush", "5"); + } + try { + flushInterval = Integer.parseInt(flushIntervalStr); + } catch (NumberFormatException e) { + flushInterval = 5; // default + } + + int flushSec = flushInterval; + if (flushSec > -1) { + flusher = new Timer("BTrace FileClient Flusher", true); + flusher.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + try { + if (out != null) { + boolean entered = BTraceRuntime.enter(); + try { + out.flush(); + } finally { + if (entered) { + BTraceRuntime.leave(); + } + } + } + } catch (Throwable t) { + log.error("Error during periodic flush", t); + } + } + }, + flushSec, + flushSec); + } else { + flusher = null; + } + } + + private String templateOutputFileName(String fName) { + if (fName != null) { + boolean dflt = fName.contains("[default]"); + String agentName = System.getProperty("btrace.agent", "default"); + String clientName = settings.getClientName(); + fName = + fName + .replace("${client}", clientName != null ? clientName : "") + .replace("${ts}", String.valueOf(System.currentTimeMillis())) + .replace("${pid}", pid()) + .replace("${agent}", agentName != null ? "." + agentName : "") + .replace("[default]", ""); + + fName = replaceSysProps(fName); + if (dflt && log.isDebugEnabled()) { + log.debug("scriptOutputFile not specified. defaulting to {}", fName); + } + } + return fName; + } + + private String replaceSysProps(String str) { + int replaced = 0; + do { + StringBuffer sb = new StringBuffer(); + replaced = replaceSysProps(str, sb); + str = sb.toString(); + } while (replaced > 0); + return str; + } + + private int replaceSysProps(String str, StringBuffer sb) { + int cnt = 0; + Matcher m = SYSPROP_PTN.matcher(str); + while (m.find()) { + String key = m.group(1); + String val = System.getProperty(key); + if (val != null) { + cnt++; + m.appendReplacement(sb, val); + } else { + m.appendReplacement(sb, m.group(0)); + } + } + m.appendTail(sb); + return cnt; + } + + static Collection listProbes() { + List probes = new ArrayList<>(CLIENTS.size()); + for (Client client : CLIENTS.values()) { + if (client instanceof RemoteClient) { + if (((RemoteClient) client).isDisconnected()) { + probes.add(client.id + " [" + client.getClassName() + "]"); + } + } + } + return probes; + } + + synchronized void onExit(int exitCode) { + if (!shuttingDown) { + shuttingDown = true; + if (out != null) { + out.flush(); + } + + BTraceRuntime.leave(); + try { + log.debug("onExit:"); + log.debug("cleaning up transformers"); + cleanupTransformers(); + log.debug("removing instrumentation"); + retransformLoaded(); + log.debug("closing all I/O"); + // Send EXIT command to notify remote client before closing + sendCommand(new ExitCommand(exitCode)); + Thread.sleep(300); + try { + closeAll(); + } catch (IOException e) { + // ignore IOException when closing + } + log.debug("done"); + } catch (Throwable th) { + // ExitException is expected here + if (!th.getClass().getName().equals("ExitException")) { + log.debug("Failed to gracefully exit BTrace probe", th); + BTraceRuntime.handleException(th); + } + } finally { + runtime.shutdownCmdLine(); + CLIENTS.remove(id); + } + } + } + + final synchronized Class loadClass(InstrumentCommand instr) throws IOException { + ArgsMap args = instr.getArguments(); + byte[] btraceCode = instr.getCode(); + try { + probe = load(btraceCode, ArgsMap.merge(argsMap, args)); + if (probe == null) { + log.debug("Failed to load BTrace probe code"); + return null; + } + + if (!settings.isTrusted()) { + probe.checkVerified(); + } + + // Check probe's required permissions against effective permissions + Set required = probe.getRequiredPermissions(); + if (!required.isEmpty()) { + PermissionSet effective = settings.getEffectivePermissions(); + Set missing = + required.stream().filter(p -> !effective.has(p)).collect(Collectors.toSet()); + if (!missing.isEmpty()) { + throw new SecurityException(formatPermissionError(missing)); + } + } + + // Validate that all injected service types are declared by available extensions + validateDeclaredServices(probe); + } catch (Throwable th) { + log.debug("Failed to load BTrace probe code", th); + errorExit(th); + return null; + } + if (log.isDebugEnabled()) { + log.debug("creating BTraceRuntime instance for {}", probe.getClassName()); + } + runtime = BTraceRuntimes.getRuntime(probe.getClassName(), args, this, inst); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + if (runtime != null) { + runtime.handleExit(0); + } + })); + if (probe.isClassRenamed()) { + if (log.isDebugEnabled()) { + log.debug("class renamed to {}", probe.getClassName()); + } + sendCommand(new RenameCommand(probe.getClassName())); + } + if (log.isDebugEnabled()) { + log.debug("created BTraceRuntime instance for {}", probe.getClassName()); + log.debug("sending Okay command"); + } + + sendCommand(new StatusCommand()); + + // Warn about failed extensions + Map failed = ExtensionRegistry.getFailedExtensions(); + if (!failed.isEmpty()) { + StringBuilder warning = new StringBuilder(); + warning + .append("[BTRACE WARN] ") + .append(failed.size()) + .append(" extension(s) failed to load:\n"); + for (Map.Entry entry : failed.entrySet()) { + String simpleName = entry.getKey().substring(entry.getKey().lastIndexOf('.') + 1); + warning + .append(" - ") + .append(simpleName) + .append(": ") + .append(entry.getValue()) + .append("\n"); + } + warning.append("Use 'btrace -le ' for details.\n"); + sendCommand(new MessageCommand(warning.toString())); + } + + // Expose extension-declared permissions for integration visibility + // Print extension permissions only when explicitly requested (debug or system property) + if (settings.isDebug() || Boolean.getBoolean("btrace.list.extension.permissions")) { + try { + ExtensionLoader loader = Main.getExtensionLoader(); + if (loader != null) { + StringBuilder info = new StringBuilder(); + info.append("[BTRACE INFO] Extensions and declared permissions:\n"); + for (ExtensionDescriptorDTO ext : loader.getAvailableExtensions()) { + PermissionSet perms = ext.getRequiredPermissions(); + String pStr = perms != null && !perms.isEmpty() ? perms.toString() : "[]"; + info.append(" - ").append(ext.getId()).append(": ").append(pStr).append("\n"); + } + sendCommand(new MessageCommand(info.toString())); + } + } catch (Throwable t) { + // ignore, informational only + } + } + + boolean entered = false; + try { + entered = BTraceRuntimeAccess.enter((BTraceRuntimeBridge) runtime); + return probe.register(runtime, transformer); + } catch (Throwable th) { + log.debug("Failed to load BTrace probe", th); + errorExit(th); + return null; + } finally { + if (entered) { + BTraceRuntime.leave(); + } + } + } + + /** + * Validates that all {@code @Injected} service field types used by the given probe are declared + * by some available extension. This runs in the agent's runtime where the actual classloader and + * JPMS module layer apply. + * + *

Why reflection here (vs. pure ASM): - Classloader identity: Ensures types are checked + * against the agent's classes loaded by the correct loader. Name-only checks in ASM cannot detect + * split-brain issues (same FQN, different loader/JAR) that would later cause ClassCastException. + * - JPMS access rules: Surfaces missing exports/opens and other module access constraints that + * cannot be proven by static bytecode analysis. - Linkage/loadability: Fails fast if a referenced + * type is not actually resolvable on the agent's runtime path (NoClassDefFoundError/missing + * transitive dependencies). - Assignability truth: Verifies that the service type corresponds to + * something an extension actually declares in its manifest, avoiding false positives from shaded + * or version-skewed classes. + * + *

Implementation notes: - We use reflection only to access the probe's internal service field + * map (to avoid a direct compile-time dependency on the probe's delegate type) and to keep the + * agent/probe boundary clean. We do not instantiate user classes or trigger class initializers. - + * This check complements compile-time and bytecode-time validation (ASM-based) which enforce + * structural rules without loading classes. Reflection here provides the necessary runtime + * assurance in the actual environment where the agent will operate. + */ + private void validateDeclaredServices(BTraceProbe probe) throws IOException { + if (!(probe instanceof BTraceProbePersisted)) { + return; + } + ExtensionLoader loader = Main.getExtensionLoader(); + if (loader == null) { + return; + } + // Reflectively access serviceFields() from the delegate to get injected service types + try { + java.lang.reflect.Field delF = BTraceProbePersisted.class.getDeclaredField("delegate"); + delF.setAccessible(true); + Object delegate = delF.get(probe); + java.lang.reflect.Method svcM = delegate.getClass().getDeclaredMethod("serviceFields"); + svcM.setAccessible(true); + @SuppressWarnings("unchecked") + Map svcMap = (Map) svcM.invoke(delegate); + if (svcMap != null) { + for (String internalName : svcMap.values()) { + String fqcn = internalName.replace('/', '.'); + if (loader.findExtensionForService(fqcn) == null) { + throw new IOException("Injected service type not declared by any extension: " + fqcn); + } + } + } + } catch (ReflectiveOperationException e) { + log.debug("Unable to inspect injected services for validation", e); + } + } + + protected void closeAll() throws IOException { + if (flusher != null) { + flusher.cancel(); + } + if (out != null) { + out.close(); + } + WRITER_MAP.remove(outputName); + } + + private void errorExit(Throwable th) throws IOException { + log.debug("sending error command"); + sendCommand(new ErrorCommand(th)); + log.debug("sending exit command"); + sendCommand(new ExitCommand(1)); + closeAll(); + } + + private void cleanupTransformers() { + if (probe != null) { + String probeName = probe.getClassName(); + probe.unregister(); + // Drop the registry's strong reference to the BTraceRuntime.Impl created in + // initialize() via BTraceRuntimes.getRuntime(probe.getClassName(), ...). Without + // this, the registry keeps the Impl (and, transitively, the probe Class and + // its per-probe ClassLoader) reachable forever, defeating probe class unloading. + // Must use the same key that was used to register — here, the dotted class name. + BTraceRuntimes.removeRuntime(probeName); + } + } + + // package privates below this point + final boolean isInitialized() { + return initialized; + } + + final BTraceRuntime.Impl getRuntime() { + return runtime; + } + + final String getClassName() { + return probe != null ? probe.getClassName() : ""; + } + + private final boolean isCandidate(Class c) { + String cname = c.getName().replace('.', '/'); + if (c.isInterface() || c.isPrimitive() || c.isArray()) { + return false; + } + if (ClassFilter.isSensitiveClass(cname)) { + return false; + } else { + return probe.willInstrument(c); + } + } + + private final void startRetransformClasses(int numClasses) { + sendCommand(new RetransformationStartNotification(numClasses)); + if (log.isDebugEnabled()) { + log.debug("calling retransformClasses ({} classes to be retransformed)", numClasses); + } + } + + final void endRetransformClasses() { + sendCommand(new StatusCommand()); + log.debug("finished retransformClasses"); + } + + // Internals only below this point + private BTraceProbe load(byte[] buf, ArgsMap args) { + BTraceProbeFactory f = new BTraceProbeFactory(settings); + log.debug("loading BTrace class"); + BTraceProbe cn = f.createProbe(buf, args); + + if (cn != null) { + if (cn.isVerified()) { + if (log.isDebugEnabled()) { + log.debug("loaded '{}' successfully", cn.getClassName()); + } + } else { + if (log.isDebugEnabled()) { + log.debug("{} failed verification", cn.getClassName()); + } + return null; + } + } + return BTraceProbePersisted.from(cn); + } + + boolean retransformLoaded() throws UnmodifiableClassException { + if (runtime == null) { + return false; + } + if (probe.isTransforming() && settings.isRetransformStartup()) { + ArrayList> list = new ArrayList<>(); + log.debug("retransforming loaded classes"); + log.debug("filtering loaded classes"); + ClassCache cc = ClassCache.getInstance(); + for (Class c : inst.getAllLoadedClasses()) { + if (c != null) { + if (inst.isModifiableClass(c) && isCandidate(c)) { + if (log.isDebugEnabled()) { + log.debug("candidate {} added", c); + } + list.add(c); + } + } + } + list.trimToSize(); + int size = list.size(); + if (size > 0) { + Class[] classes = new Class[size]; + list.toArray(classes); + startRetransformClasses(size); + if (log.isDebugEnabled()) { + for (Class c : classes) { + try { + log.debug("Attempting to retransform class: {}", c.getName()); + inst.retransformClasses(c); + } catch (ClassFormatError | VerifyError e) { + // Avoid printing full stack traces in debug to keep target stderr clean + log.debug("Class '{}' verification failed: {}", c.getName(), e.toString()); + sendCommand( + new MessageCommand( + "[BTRACE WARN] Class verification failed: " + + c.getName() + + " (" + + e.getMessage() + + ")")); + } + } + } else { + try { + inst.retransformClasses(classes); + } catch (ClassFormatError | VerifyError e) { + /* + * If the en-block retransformation fails because of verification retry classes one-by-one. + * Otherwise all classes are rolled back to the original state and no instrumentation + * is applied. + */ + for (Class c : classes) { + try { + inst.retransformClasses(c); + } catch (ClassFormatError | VerifyError e1) { + // Avoid printing full stack traces in debug to keep target stderr clean + log.debug("Class '{}' verification failed: {}", c.getName(), e1.toString()); + sendCommand( + new MessageCommand( + "[BTRACE WARN] Class verification failed: " + + c.getName() + + " (" + + e1.getMessage() + + ")")); + } + } + } + } + } + } + return true; + } + + protected void sendCommand(Command command) { + if (runtime == null) { + log.warn( + "Cannot send command {}, runtime not initialized", command.getClass().getSimpleName()); + return; + } + runtime.sendCommand(command); + } + + static Client findClient(String uuid) { + try { + UUID id = UUID.fromString(uuid); + return CLIENTS.get(id); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public String toString() { + return "BTrace Client: " + id + "[" + probe.getClassName() + "]"; + } + + private static String formatPermissionError(Set missing) { + StringBuilder sb = new StringBuilder(); + sb.append("Probe requires permissions that are not granted:\n\n"); + for (Permission p : missing) { + sb.append(" - ").append(p.name()).append("\n"); + sb.append(" ").append(p.getRiskDescription()).append("\n"); + } + sb.append("\nTo allow these permissions, use:\n"); + sb.append(" --grant=") + .append(missing.stream().map(Permission::name).collect(Collectors.joining(","))) + .append("\n"); + sb.append("\nOr use --grantAll=true to allow all permissions (not recommended).\n"); + return sb.toString(); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/ClientContext.java b/btrace-agent/src/main/java/io/btrace/agent/ClientContext.java new file mode 100644 index 000000000..c958856d2 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/ClientContext.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.ArgsMap; +import io.btrace.core.SharedSettings; +import io.btrace.instr.BTraceTransformer; +import java.lang.instrument.Instrumentation; + +/** + * Client-context data class + * + * @author Jaroslav Bachorik + */ +class ClientContext { + private final Instrumentation instr; + private final BTraceTransformer transformer; + private final ArgsMap args; + private final SharedSettings settings; + + ClientContext( + Instrumentation instr, BTraceTransformer transformer, ArgsMap args, SharedSettings settings) { + this.instr = instr; + this.transformer = transformer; + this.args = args; + this.settings = settings; + } + + Instrumentation getInstr() { + return instr; + } + + BTraceTransformer getTransformer() { + return transformer; + } + + SharedSettings getSettings() { + return settings; + } + + ArgsMap getArguments() { + return args; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/FileClient.java b/btrace-agent/src/main/java/io/btrace/agent/FileClient.java new file mode 100644 index 000000000..39a833135 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/FileClient.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.comm.Command; +import io.btrace.core.comm.ExitCommand; +import io.btrace.core.comm.InstrumentCommand; +import io.btrace.core.comm.PrintableCommand; +import io.btrace.instr.Constants; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLDecoder; +import java.security.CodeSigner; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a local client communicated by trace file. The trace script is specified as a File of + * a .class file or a byte array containing bytecode of the trace script. + * + * @author A. Sundararajan + * @author J.Bachorik + */ +class FileClient extends Client { + private static final Logger log = LoggerFactory.getLogger(FileClient.class); + + private final AtomicBoolean noOutputNotified = new AtomicBoolean(false); + + private boolean canLoadPack = true; + + FileClient(ClientContext ctx, File scriptFile) throws IOException { + super(ctx); + if (!init(readScript(scriptFile))) { + log.warn("Unable to load BTrace script {}", scriptFile); + } + } + + private static byte[] readAll(InputStream is, long size) throws IOException { + if (is == null) throw new NullPointerException(); + + byte[] buf = new byte[size != -1 ? Math.min((int) size, 512 * 1024 * 1024) : 8192]; + int bufsize = buf.length; + int off = 0; + int read; + while ((read = is.read(buf, off, bufsize - off)) > -1) { + off += read; + if (off >= bufsize) { + buf = Arrays.copyOf(buf, bufsize * 2); + bufsize = buf.length; + } + } + return Arrays.copyOf(buf, off); + } + + private boolean init(byte[] code) throws IOException { + InstrumentCommand cmd = new InstrumentCommand(code, argsMap); + boolean ret = loadClass(cmd) != null; + if (ret) { + initialize(); + } + return ret; + } + + @SuppressWarnings("RedundantThrows") + @Override + public void onCommand(Command cmd) throws IOException { + if (log.isDebugEnabled()) { + log.debug("client {}: got {}", getClassName(), cmd); + } + switch (cmd.getType()) { + case Command.EXIT: + onExit(((ExitCommand) cmd).getExitCode()); + break; + default: + if (cmd instanceof PrintableCommand) { + if (out == null) { + if (noOutputNotified.compareAndSet(false, true)) { + log.debug("No output stream. DataCommand output is ignored."); + } + } else { + ((PrintableCommand) cmd).print(out); + out.flush(); + } + } + break; + } + } + + private byte[] readScript(File file) throws IOException { + String path = file.getPath(); + if (path.startsWith(Constants.EMBEDDED_BTRACE_SECTION_HEADER)) { + return settings.isTrusted() ? loadQuick(path) : loadWithSecurity(path); + } else { + int size = (int) file.length(); + try (FileInputStream fis = new FileInputStream(file)) { + return readAll(fis, size); + } + } + } + + private byte[] loadQuick(String path) throws IOException { + try (InputStream is = ClassLoader.getSystemResourceAsStream(path)) { + return readAll(is, -1); + } + } + + private byte[] loadWithSecurity(String path) throws IOException { + URL scriptUrl = ClassLoader.getSystemResource(path); + if (scriptUrl.getProtocol().equals("jar")) { + String jarPath = scriptUrl.getPath().substring(5, scriptUrl.getPath().indexOf("!")); + JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); + Enumeration ens = jar.entries(); + + while (ens.hasMoreElements()) { + JarEntry en = ens.nextElement(); + + if (!en.isDirectory()) { + if (en.toString().equals(path)) { + byte[] data = readAll(jar.getInputStream(en), en.getSize()); + CodeSigner[] signers = en.getCodeSigners(); + canLoadPack = signers != null && signers.length != 0; + return data; + } + } + } + } + return null; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/Main.java b/btrace-agent/src/main/java/io/btrace/agent/Main.java new file mode 100644 index 000000000..a1ad25ce2 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/Main.java @@ -0,0 +1,1514 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import static io.btrace.core.Args.ALLOW_EXTENSIONS; +import static io.btrace.core.Args.ALLOW_PRIVILEGED; +import static io.btrace.core.Args.BOOT_CLASS_PATH; +import static io.btrace.core.Args.CMD_QUEUE_LIMIT; +import static io.btrace.core.Args.CONFIG; +import static io.btrace.core.Args.DEBUG; +import static io.btrace.core.Args.DENY; +import static io.btrace.core.Args.DENY_EXTENSIONS; +import static io.btrace.core.Args.DUMP_CLASSES; +import static io.btrace.core.Args.DUMP_DIR; +import static io.btrace.core.Args.FILE_ROLL_MAX_ROLLS; +import static io.btrace.core.Args.FILE_ROLL_MILLISECONDS; +import static io.btrace.core.Args.GRANT; +import static io.btrace.core.Args.GRANT_ALL; +import static io.btrace.core.Args.HELP; +import static io.btrace.core.Args.LIBS; +import static io.btrace.core.Args.NO_SERVER; +import static io.btrace.core.Args.OUTPUT; +import static io.btrace.core.Args.PORT; +import static io.btrace.core.Args.PROBES; +import static io.btrace.core.Args.PROBE_DESC_PATH; +import static io.btrace.core.Args.SCRIPT; +import static io.btrace.core.Args.SCRIPT_DIR; +import static io.btrace.core.Args.SCRIPT_OUTPUT_DIR; +import static io.btrace.core.Args.SCRIPT_OUTPUT_FILE; +import static io.btrace.core.Args.STARTUP_RETRANSFORM; +import static io.btrace.core.Args.STATSD; +import static io.btrace.core.Args.STDOUT; +import static io.btrace.core.Args.SYSTEM_CLASS_PATH; +import static io.btrace.core.Args.TRACK_RETRANSFORMS; +import static io.btrace.core.Args.TRUSTED; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.DebugSupport; +import io.btrace.core.Messages; +import io.btrace.core.SharedSettings; +import io.btrace.core.comm.ErrorCommand; +import io.btrace.core.comm.StatusCommand; +import io.btrace.core.comm.WireIO; +import io.btrace.core.extensions.ExtensionConfigurator; +import io.btrace.core.extensions.ProbeConfiguration; +import io.btrace.extension.ExtensionDescriptorDTO; +import io.btrace.extension.ExtensionLoader; +import io.btrace.extension.impl.ExtensionBridgeImpl; +import io.btrace.instr.BTraceProbeFactory; +import io.btrace.instr.BTraceTransformer; +import io.btrace.instr.Constants; +import io.btrace.runtime.BTraceBootstrap; +import io.btrace.runtime.BTraceRuntimes; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the main class for BTrace java.lang.instrument agent. + * + * @author A. Sundararajan + */ +@SuppressWarnings("RedundantThrows") +public final class Main { + public static final int BTRACE_DEFAULT_PORT = 2020; + private static final boolean AGENT_DEBUG = Boolean.getBoolean("btrace.agent.debug"); + private static final Pattern KV_PATTERN = Pattern.compile(","); + private static final SharedSettings settings = SharedSettings.GLOBAL; + private static final BTraceTransformer transformer = + new BTraceTransformer(new DebugSupport(settings)); + // #BTRACE-42: Non-daemon thread prevents traced application from exiting + private static final ThreadFactory qProcessorThreadFactory = + r -> { + Thread result = new Thread(r, "BTrace Command Queue Processor"); + result.setDaemon(true); + return result; + }; + private static final ExecutorService serializedExecutor = + Executors.newSingleThreadExecutor(qProcessorThreadFactory); + private static final long ts = System.nanoTime(); + private static volatile ArgsMap argMap; + private static volatile Instrumentation inst; + private static volatile Long fileRollMilliseconds; + private static volatile ExtensionLoader extensionLoader; + private static volatile boolean serverRunning = true; + private static ServerSocket serverSocket; + // Track appended jars to avoid duplicate classpath entries + private static final Set BOOT_ADDED = Collections.synchronizedSet(new LinkedHashSet<>()); + private static final Set SYSTEM_ADDED = Collections.synchronizedSet(new LinkedHashSet<>()); + // Hold strong references to JarFiles passed to appendToBootstrap/SystemClassLoaderSearch so + // that GC cannot close their file descriptors before the JVM is done reading class bytes. + private static final List BOOT_JARS = new ArrayList<>(); + private static final List SYSTEM_JARS = new ArrayList<>(); + + private static final Logger log = LoggerFactory.getLogger(Main.class); + + private static volatile String agentMode = "unknown"; + + public static void premain(String args, Instrumentation inst) { + agentMode = "premain"; + startAgent(args, inst); + } + + public static void agentmain(String args, Instrumentation inst) { + agentMode = "agentmain"; + startAgent(args, inst); + } + + private static void startAgent(String args, Instrumentation inst) { + try { + main(args, inst); + } catch (Exception e) { + System.err.println("BTrace agent initialization failed: " + e.getMessage()); + throw new RuntimeException("BTrace agent initialization failed", e); + } + } + + private static synchronized void main(String args, Instrumentation inst) { + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Initialization started"); + if (Main.inst != null) { + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Agent already initialized, skipping"); + return; + } else { + Main.inst = inst; + } + + try { + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Loading arguments"); + loadArgs(args); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Arguments loaded"); + boolean isDebug = Boolean.parseBoolean(argMap.get(DEBUG)); + // set the debug level based on cmdline config + settings.setDebug(isDebug); + DebugSupport.initLoggers(isDebug, log); + + // Load defaults from file-based permission policy first + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Loading permission policy"); + io.btrace.extension.PermissionPolicy.get().loadFromDefaults(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Permission policy loaded"); + // Then parse and apply agent args (which override file policy) + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Parsing arguments"); + parseArgs(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Arguments parsed"); + Telemetry.fireAsync(readBTraceVersion(), agentMode); + // settings are all built-up; set the logging system properties accordingly + DebugSupport.initLoggers(settings.isDebug(), log); + + String tmp = argMap.get(NO_SERVER); + // noServer is defaulting to true if startup scripts are defined + boolean noServer = tmp != null ? Boolean.parseBoolean(tmp) : hasScripts(); + Thread agentThread = null; + if (noServer) { + log.debug("noServer is true, server not started"); + } else { + agentThread = + new Thread( + () -> { + BTraceRuntime.enter(); + try { + startServer(); + } finally { + BTraceRuntime.leave(); + } + }); + } + // set the fall-back instrumentation object to BTraceRuntime + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Setting up runtime"); + BTraceRuntime.instrumentation = inst; + // force back-registration of BTraceRuntimeImpl in BTraceRuntime + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Initializing BTraceRuntimes"); + BTraceRuntimes.getDefault(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] BTraceRuntimes initialized"); + registerCoreOps(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Core DSL ops registered"); + // ensure runtime accessor is registered + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Registering runtime accessor"); + BTraceRuntimes.ensureAccessorRegistered(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Runtime accessor registered"); + // init BTraceRuntime + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Initializing unsafe"); + BTraceRuntime.initUnsafe(); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Unsafe initialized"); + if (agentThread != null) { + BTraceRuntime.enter(); + try { + agentThread.setDaemon(true); + log.debug("starting agent thread"); + + agentThread.start(); + } finally { + BTraceRuntime.leave(); + } + } + + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Adding class transformer"); + log.debug("Adding class transformer"); + inst.addTransformer(transformer, true); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Transformer added"); + try { + // the MethodHandleNatives must be instrumented to track start-end of indy linking to avoid + // deadlocking + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Instrumenting MethodHandleNatives"); + Class clz = + ClassLoader.getSystemClassLoader().loadClass("java.lang.invoke.MethodHandleNatives"); + inst.retransformClasses(clz); + if (AGENT_DEBUG) System.err.println("[BTrace Agent] MethodHandleNatives instrumented"); + } catch (Throwable t) { + if (AGENT_DEBUG) + System.err.println( + "[BTrace Agent] Failed to instrument MethodHandleNatives: " + t.getMessage()); + log.debug("Failed to instrument MethodHandleNatives", t); + } + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Starting scripts"); + int startedScripts = startScripts(); + // Thread.class retransform fires after startScripts() but before initExtensions() + // deliberately: retransformation only rewrites bytecode; extension service calls + // happen at invocation time (Thread.start events). No race with extension init. + // Ensure early hooks (e.g., Thread.start) are applied even if Thread was already loaded + if (startedScripts > 0) { + try { + inst.retransformClasses(Thread.class); + if (log.isDebugEnabled()) { + log.debug("Proactively retransformed java.lang.Thread after startup scripts"); + } + } catch (Throwable t) { + log.warn( + "Thread.class retransform failed; early Thread hooks may be inactive: {}", + t.toString()); + } + } + + // initialize extension system after transformer is installed so early app code is not delayed + if (AGENT_DEBUG) System.err.println("[BTrace Agent] Initializing extensions"); + initExtensions(); + if (AGENT_DEBUG) + System.err.println( + "[BTrace Agent] Initialization complete, " + startedScripts + " scripts started"); + } catch (Throwable t) { + // FATAL errors should always be printed + System.err.println( + "[BTrace Agent] FATAL: Initialization failed: " + + t.getClass().getName() + + ": " + + t.getMessage()); + t.printStackTrace(System.err); + log.error("Failed to initialize BTrace agent", t); + throw new RuntimeException("BTrace agent initialization failed", t); + } finally { + log.debug("Agent init took: {}", (System.nanoTime() - ts) + "ns"); + } + } + + private static boolean hasScripts() { + return argMap.containsKey(SCRIPT) || argMap.containsKey(SCRIPT_DIR); + } + + private static final class LogValue { + final String logLine; + final Throwable throwable; + + public LogValue(String logLine, Throwable throwable) { + this.logLine = logLine; + this.throwable = throwable; + } + } + + private static void loadDefaultArguments(String config) { + try { + String propTarget = Constants.EMBEDDED_BTRACE_SECTION_HEADER + "agent.properties"; + InputStream is = ClassLoader.getSystemResourceAsStream(propTarget); + if (is != null) { + Properties ps = new Properties(); + ps.load(is); + StringBuilder logMsg = new StringBuilder(); + for (Map.Entry entry : ps.entrySet()) { + String keyConfig = ""; + String argKey = (String) entry.getKey(); + int configPos = argKey.lastIndexOf('#'); + if (configPos > -1) { + keyConfig = argKey.substring(0, configPos); + argKey = argKey.substring(configPos + 1); + } + if (config == null || keyConfig.isEmpty() || config.equals(keyConfig)) { + String argVal = (String) entry.getValue(); + switch (argKey) { + case SCRIPT: + { + // special treatment for the 'script' parameter + boolean replace = false; + String scriptVal = argVal; + if (scriptVal.startsWith("!")) { + scriptVal = scriptVal.substring(1); + replace = true; + } else { + String oldVal = argMap.get(argKey); + if (oldVal != null && !oldVal.isEmpty()) { + scriptVal = oldVal + ":" + scriptVal; + } else { + replace = true; + } + } + if (replace) { + logMsg + .append("setting default agent argument '") + .append(argKey) + .append("' to '") + .append(scriptVal) + .append("'\n"); + } else { + logMsg + .append("augmenting default agent argument '") + .append(argKey) + .append("':'") + .append(argMap.get(argKey)) + .append("' with '") + .append(argVal) + .append("'\n"); + } + + argMap.put(argKey, scriptVal); + break; + } + case SYSTEM_CLASS_PATH: // fall through + case BOOT_CLASS_PATH: // fall through + case CONFIG: + { + logMsg.append("argument '").append(argKey).append("' is not overridable\n"); + break; + } + default: + { + if (!argMap.containsKey(argKey)) { + logMsg + .append("applying default agent argument '") + .append(argKey) + .append("'='") + .append(argVal) + .append("'\n"); + argMap.put(argKey, argVal); + } + } + } + } + } + DebugSupport.initLoggers(Boolean.parseBoolean(argMap.get(DEBUG)), log); + if (log.isDebugEnabled()) { + log.debug(logMsg.toString()); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(e.toString(), e); + } + } + } + + /** + * Initialize the extension system by discovering and loading extensions from configured extension + * directories or embedded resources. + */ + private static void initExtensions() { + try { + log.info("Initializing BTrace extension system"); + + // Determine BTRACE_HOME from agent JAR location (null enables embedded-only mode) + String btraceHome = getBTraceHome(); + if (log.isDebugEnabled()) { + log.debug("BTRACE_HOME={}", btraceHome != null ? btraceHome : "(embedded-only mode)"); + } + + // Initialize extension loader with boot classloader as parent, configuration, and + // instrumentation + // Passing null btraceHome enables embedded-only mode (extensions from JAR resources) + ClassLoader bootClassLoader = Main.class.getClassLoader(); + if (bootClassLoader == null) { + bootClassLoader = ClassLoader.getSystemClassLoader(); + } + extensionLoader = + ExtensionLoader.initialize(btraceHome, bootClassLoader, inst, readBTraceVersion()); + + // Initialize invokedynamic bridge for extensions after loader is ready + ExtensionBridgeImpl.initialize(extensionLoader); + + // Load bundled probes from embedded extensions if configured + loadBundledProbes(); + + } catch (Exception e) { + log.error("Failed to initialize extension system: {}", e.getMessage(), e); + } + } + + /** Load bundled probes from embedded extensions based on agent args or configurator. */ + private static void loadBundledProbes() { + if (extensionLoader == null) { + return; + } + + String probesArg = argMap.get(PROBES); + String outputArg = argMap.get(OUTPUT); + + if (probesArg != null && !probesArg.isEmpty()) { + // Explicit probes= argument: honour it directly. + log.info("Loading bundled probes: {}", probesArg); + boolean toStdOut = "stdout".equalsIgnoreCase(outputArg); + if (outputArg != null && !outputArg.isEmpty()) { + if ("jfr".equalsIgnoreCase(outputArg)) { + log.info("Bundled probes will output to JFR"); + } else if ("file".equalsIgnoreCase(outputArg)) { + log.info("Bundled probes will output to file"); + } else if (toStdOut) { + log.info("Bundled probes will output to stdout"); + } + } + List requested = new ArrayList<>(); + for (String p : probesArg.split(",")) { + requested.add(p.trim()); + } + loadProbesFromNames(requested, toStdOut); + } else { + // No explicit probes= — ask each extension's configurator. + AgentRuntimeEnvironment env = new AgentRuntimeEnvironment(); + Map agentArgs = new LinkedHashMap<>(); + for (Map.Entry e : argMap) { + agentArgs.put(e.getKey(), e.getValue()); + } + for (ExtensionDescriptorDTO ext : extensionLoader.getAvailableExtensions()) { + if (!ext.isEmbedded()) { + continue; + } + String configuratorClass = ext.getConfiguratorClass(); + if (configuratorClass == null) { + continue; + } + ProbeConfiguration config = runConfigurator(ext, configuratorClass, env, agentArgs); + if (config == null || !config.hasEnabledProbes()) { + continue; + } + boolean toStdOut = config.getOutput() == ProbeConfiguration.Output.STDOUT; + log.info( + "Extension {} configurator enabled probes: {}", ext.getId(), config.getEnabledProbes()); + loadProbesFromNames(config.getEnabledProbes(), toStdOut); + } + } + } + + /** + * Instantiate and run the named {@link ExtensionConfigurator} class from the given extension, + * returning its {@link ProbeConfiguration} or null on failure. + */ + private static ProbeConfiguration runConfigurator( + ExtensionDescriptorDTO ext, + String configuratorClass, + AgentRuntimeEnvironment env, + Map agentArgs) { + try { + ClassLoader cl = ext.getClassLoader(); + if (cl == null) { + cl = Main.class.getClassLoader(); + } + Class cls = Class.forName(configuratorClass, true, cl); + ExtensionConfigurator configurator = + (ExtensionConfigurator) cls.getDeclaredConstructor().newInstance(); + return configurator.configure(env, agentArgs); + } catch (Exception e) { + log.warn( + "Configurator {} for extension {} failed: {}", + configuratorClass, + ext.getId(), + e.toString()); + return null; + } + } + + /** Load the named probes from embedded extensions. */ + private static void loadProbesFromNames(List probeNames, boolean traceToStdOut) { + for (ExtensionDescriptorDTO ext : extensionLoader.getAvailableExtensions()) { + if (!ext.isEmbedded()) { + continue; + } + List bundledProbes = ext.getBundledProbes(); + if (bundledProbes.isEmpty()) { + continue; + } + for (String probeName : probeNames) { + if (!probeName.matches("[A-Za-z0-9_.]+")) { + log.warn("Ignoring probe with invalid name: '{}'", probeName); + continue; + } + for (String bundledProbe : bundledProbes) { + if (bundledProbe.endsWith("." + probeName) || bundledProbe.equals(probeName)) { + String path = ext.getResourceBasePath() + "/probes/" + probeName + ".class"; + if (loadEmbeddedProbe(path, probeName, traceToStdOut)) { + log.info("Loaded bundled probe: {} from extension {}", probeName, ext.getId()); + break; + } + } + } + } + } + } + + /** Load an embedded probe from classpath resources. */ + private static boolean loadEmbeddedProbe( + String resourcePath, String probeName, boolean traceToStdOut) { + ClassLoader loader = Main.class.getClassLoader(); + try (InputStream is = + loader != null + ? loader.getResourceAsStream(resourcePath) + : ClassLoader.getSystemResourceAsStream(resourcePath)) { + if (is == null) { + log.debug("Probe resource not found: {}", resourcePath); + return false; + } + byte[] probeBytes = readAllBytes(is); + File tmp = File.createTempFile("btrace-probe-", ".class"); + try { + try (FileOutputStream fos = new FileOutputStream(tmp)) { + fos.write(probeBytes); + } + return loadBTraceScript(tmp.getAbsolutePath(), traceToStdOut); + } finally { + if (!tmp.delete()) { + tmp.deleteOnExit(); + } + } + } catch (IOException e) { + log.warn("Failed to load embedded probe {}: {}", probeName, e.getMessage()); + return false; + } + } + + private static byte[] readAllBytes(InputStream is) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[4096]; + int nRead; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + return buffer.toByteArray(); + } + + /** + * Get BTRACE_HOME directory by locating the agent JAR. + * + * @return BTRACE_HOME path, or null if not found + */ + private static String getBTraceHome() { + try { + // Get the agent JAR location + String agentPath = Main.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + + // Agent is typically at BTRACE_HOME/libs/btrace-agent.jar or BTRACE_HOME/libs/btrace.jar + File agentJar = new File(agentPath); + String jarName = agentJar.getName(); + if (agentJar.exists() + && (jarName.equals("btrace-agent.jar") || jarName.equals("btrace.jar"))) { + File libsDir = agentJar.getParentFile(); + if (libsDir != null && libsDir.getName().equals("libs")) { + File btraceHome = libsDir.getParentFile(); + if (btraceHome != null) { + return btraceHome.getAbsolutePath(); + } + } + } + + // Try BTRACE_HOME environment variable + String envHome = System.getenv("BTRACE_HOME"); + if (envHome != null && new File(envHome).exists()) { + return envHome; + } + + } catch (Exception e) { + log.debug("Failed to determine BTRACE_HOME: {}", e.getMessage()); + } + + return null; + } + + private static String readBTraceVersion() { + try { + String agentPath = Main.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + try (JarFile jar = new JarFile(new File(agentPath))) { + Manifest mf = jar.getManifest(); + if (mf != null) { + String v = mf.getMainAttributes().getValue("BTrace-Version"); + if (v != null && !v.isEmpty()) { + return v; + } + } + } + } catch (Exception e) { + log.debug("Could not read BTrace-Version from agent JAR manifest: {}", e.getMessage()); + } + return "unknown"; + } + + /** + * Get the extension loader instance. + * + * @return extension loader, or null if not initialized + */ + public static ExtensionLoader getExtensionLoader() { + return extensionLoader; + } + + private static int startScripts() { + int scriptCount = 0; + + String p = argMap.get(STDOUT); + boolean traceToStdOut = p != null && !"false".equals(p); + if (log.isDebugEnabled()) { + log.debug("stdout is {}", traceToStdOut); + } + + List scripts = locateScripts(argMap); + for (String script : scripts) { + if (loadBTraceScript(script, traceToStdOut)) { + scriptCount++; + } + } + return scriptCount; + } + + static List locateScripts(ArgsMap argsMap) { + String script = argsMap.get(SCRIPT); + String scriptDir = argsMap.get(SCRIPT_DIR); + + List scripts = new ArrayList<>(); + if (script != null) { + StringTokenizer tokenizer = new StringTokenizer(script, ":"); + if (log.isDebugEnabled()) { + log.debug( + ((tokenizer.countTokens() == 1) ? "initial script is {}" : "initial scripts are {}"), + script); + } + while (tokenizer.hasMoreTokens()) { + scripts.add(tokenizer.nextToken()); + } + } + if (scriptDir != null) { + File dir = new File(scriptDir); + if (dir.isDirectory()) { + if (log.isDebugEnabled()) { + log.debug("found scriptdir: {}", dir.getAbsolutePath()); + } + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + scripts.add(file.getAbsolutePath()); + } + } + } + } + return scripts; + } + + private static void usage() { + System.out.println(Messages.get("btrace.agent.usage")); + System.exit(0); + } + + private static void loadArgs(String args) { + if (args == null) { + args = ""; + } + String[] pairs = KV_PATTERN.split(args); + argMap = new ArgsMap(); + for (String s : pairs) { + int i = s.indexOf('='); + String key, value = ""; + if (i != -1) { + key = s.substring(0, i).trim(); + if (i + 1 < s.length()) { + value = s.substring(i + 1).trim(); + } + } else { + key = s; + } + argMap.put(key, value); + } + } + + private static void parseArgs() throws ClassNotFoundException { + String p = argMap.get(HELP); + if (p != null) { + usage(); + } + + String libs = argMap.get(LIBS); + if (libs != null && !libs.isEmpty()) { + log.warn( + "The 'libs' profile feature is deprecated and will be removed in a future release. " + + "Prefer packaging integrations as BTrace extensions (API on bootstrap, impl isolated). " + + "See docs/architecture/agent-manifest-libs.md for migration guidance."); + } + String config = argMap.get(CONFIG); + processClasspaths(libs); + loadDefaultArguments(config); + + p = argMap.get(DEBUG); + settings.setDebug(p != null && !"false".equals(p)); + DebugSupport.initLoggers(settings.isDebug(), log); + + log.debug("debugMode is {}", settings.isDebug()); + + for (Map.Entry e : argMap) { + String key = e.getKey(); + p = e.getValue(); + switch (key) { + case STARTUP_RETRANSFORM: + { + if (!p.isEmpty()) { + settings.setRetransformStartup(Boolean.parseBoolean(p)); + log.debug(STARTUP_RETRANSFORM + " is {}", settings.isRetransformStartup()); + } + break; + } + case DUMP_DIR: + { + String dumpClassesVal = argMap.get(DUMP_CLASSES); + if (dumpClassesVal != null) { + boolean dumpClasses = Boolean.parseBoolean(dumpClassesVal); + log.debug(DUMP_CLASSES + " is {}", dumpClasses); + if (dumpClasses) { + String dumpDir = argMap.get(DUMP_DIR); + settings.setDumpDir(dumpDir != null ? dumpDir : "."); + if (isDebug()) { + log.debug(DUMP_DIR + " is {}", dumpDir); + } + } + } + break; + } + case CMD_QUEUE_LIMIT: + { + if (!p.isEmpty()) { + System.setProperty(BTraceRuntime.CMD_QUEUE_LIMIT_KEY, p); + log.debug(CMD_QUEUE_LIMIT + " provided: {}", p); + } + + break; + } + case TRACK_RETRANSFORMS: + { + if (!p.isEmpty()) { + settings.setTrackRetransforms(Boolean.parseBoolean(p)); + if (settings.isTrackRetransforms()) { + log.debug(TRACK_RETRANSFORMS + " is on"); + } + } + break; + } + case SCRIPT_OUTPUT_FILE: + { + if (!p.isEmpty()) { + settings.setOutputFile(p); + log.debug(SCRIPT_OUTPUT_FILE + " is {}", p); + } + break; + } + case SCRIPT_OUTPUT_DIR: + { + if (!p.isEmpty()) { + settings.setScriptOutputDir(p); + log.debug(SCRIPT_OUTPUT_DIR + " is {}", p); + } + break; + } + case FILE_ROLL_MILLISECONDS: + { + if (!p.isEmpty()) { + Long msParsed = null; + try { + msParsed = Long.parseLong(p); + fileRollMilliseconds = msParsed; + } catch (NumberFormatException nfe) { + fileRollMilliseconds = null; + } + if (fileRollMilliseconds != null) { + settings.setFileRollMilliseconds(fileRollMilliseconds.intValue()); + log.debug(FILE_ROLL_MILLISECONDS + " is {}", fileRollMilliseconds); + } + } + break; + } + case FILE_ROLL_MAX_ROLLS: + { + if (!p.isEmpty()) { + Integer rolls = null; + try { + rolls = Integer.parseInt(p); + } catch (NumberFormatException ignored) { + // ignore + } + + if (rolls != null) { + settings.setFileRollMaxRolls(rolls); + } + } + break; + } + case TRUSTED: + { + if (!p.isEmpty()) { + settings.setTrusted(Boolean.parseBoolean(p)); + log.debug("trustedMode is {}", settings.isTrusted()); + } + break; + } + case STATSD: + { + if (!p.isEmpty()) { + String[] parts = p.split(":"); + if (parts.length == 2) { + settings.setStatsdHost(parts[0].trim()); + try { + settings.setStatsdPort(Integer.parseInt(parts[1].trim())); + } catch (NumberFormatException ex) { + log.warn("Invalid statsd port number: {}", parts[1]); + // leave the port unconfigured + } + } else if (parts.length == 1) { + settings.setStatsdHost(parts[0].trim()); + } + } + break; + } + case PROBE_DESC_PATH: + { + settings.setProbeDescPath(!p.isEmpty() ? p : "."); + log.debug("probe descriptor path is {}", settings.getProbeDescPath()); + break; + } + case BOOT_CLASS_PATH: + { + settings.setBootClassPath(!p.isEmpty() ? p : ""); + log.debug("probe boot class path is {}", settings.getBootClassPath()); + break; + } + case GRANT: + { + if (!p.isEmpty()) { + settings.setGrantedPermissions(SharedSettings.parsePermissions(p)); + log.debug("granted permissions: {}", settings.getGrantedPermissions()); + } + break; + } + case DENY: + { + if (!p.isEmpty()) { + settings.setDeniedPermissions(SharedSettings.parsePermissions(p)); + log.debug("denied permissions: {}", settings.getDeniedPermissions()); + } + break; + } + case GRANT_ALL: + { + if (!p.isEmpty()) { + settings.setGrantAll(Boolean.parseBoolean(p)); + log.debug("grantAll: {}", settings.isGrantAll()); + } + break; + } + case ALLOW_EXTENSIONS: + { + if (!p.isEmpty()) { + io.btrace.extension.PermissionPolicy.get().setAllowExtensionsCsv(p); + log.debug("allowExtensions: {}", p); + } + break; + } + case DENY_EXTENSIONS: + { + if (!p.isEmpty()) { + io.btrace.extension.PermissionPolicy.get().setDenyExtensionsCsv(p); + log.debug("denyExtensions: {}", p); + } + break; + } + case ALLOW_PRIVILEGED: + { + if (!p.isEmpty()) { + io.btrace.extension.PermissionPolicy.get() + .setAllowPrivileged(Boolean.parseBoolean(p)); + log.debug("allowPrivileged: {}", p); + } + break; + } + + default: + { + if (key.startsWith("$")) { + String pKey = key.substring(1); + System.setProperty(pKey, p); + log.debug("Setting system property: {}={}", pKey, p); + } + } + } + } + } + + private static void processClasspaths(String libs) throws ClassNotFoundException { + // Experimental: prefer manifest-driven libs when enabled + boolean useManifestLibs = Boolean.getBoolean("btrace.feature.manifestLibs"); + boolean hasLegacyLibs = libs != null && !libs.isEmpty(); + boolean hasManifestLibs = useManifestLibs; + if (hasManifestLibs && hasLegacyLibs) { + log.warn( + "Both libs= and manifest-attributes are present; libs= is deprecated and will be removed in N+2. Prefer manifest-based declaration."); + } + if (useManifestLibs) { + if (log.isDebugEnabled()) log.debug("Using manifest-driven libs resolution"); + AgentManifestLibs.ResolvedLibs libsFromMf = AgentManifestLibs.resolveFromManifest(Main.class); + // Append manifest-declared boot jars + for (Path p : libsFromMf.bootJars) { + appendBootJar(p); + } + // Append manifest-declared system jars + for (Path p : libsFromMf.systemJars) { + appendSystemJar(p); + } + } + // Try to find JAR via Loader.class (unmasked bootstrap class) + // Main.class won't work because it's loaded from .classdata + String bootPath = null; + Class loaderClass = Class.forName("io.btrace.boot.Loader"); + URL loaderResource = loaderClass.getResource("Loader.class"); + if (loaderResource != null) { + bootPath = loaderResource.toString(); + if (bootPath.startsWith("jar:file:")) { + // Extract JAR path from + // jar:file:/path/to/btrace.jar!/org/openjdk/btrace/boot/Loader.class + bootPath = bootPath.substring("jar:file:".length()); + int idx = bootPath.indexOf("!"); + if (idx > -1) { + bootPath = bootPath.substring(0, idx); + } + } + } + + String bootClassPath = argMap.get(BOOT_CLASS_PATH); + if (bootClassPath == null && bootPath != null) { + bootClassPath = bootPath; + } else if (bootClassPath != null && bootPath != null) { + if (".".equals(bootClassPath)) { + bootClassPath = bootPath; + } else { + bootClassPath = bootPath + File.pathSeparator + bootClassPath; + } + } + + if (bootClassPath == null || bootClassPath.isEmpty()) { + log.debug("No boot classpath configured; skipping bootstrap jar setup"); + } else { + log.debug("Bootstrap ClassPath: {}", bootClassPath); + StringTokenizer tokenizer = new StringTokenizer(bootClassPath, File.pathSeparator); + while (tokenizer.hasMoreTokens()) { + String path = tokenizer.nextToken(); + File f = new File(path); + if (!f.exists()) { + log.debug("BTrace bootstrap classpath resource [{}] does not exist", path); + } else { + if (f.isFile() && f.getName().toLowerCase().endsWith(".jar")) { + appendBootJar(f.toPath()); + } else { + log.debug("ignoring boot classpath element '{}' - only jar files allowed", path); + } + } + } + } + + String systemClassPath = argMap.get(SYSTEM_CLASS_PATH); + if (systemClassPath != null) { + log.debug("System ClassPath: {}", systemClassPath); + StringTokenizer tokenizer = new StringTokenizer(systemClassPath, File.pathSeparator); + while (tokenizer.hasMoreTokens()) { + String path = tokenizer.nextToken(); + File f = new File(path); + if (!f.exists()) { + log.debug("BTrace system classpath resource [{}] does not exist.", path); + } else { + if (f.isFile() && f.getName().toLowerCase().endsWith(".jar")) { + appendSystemJar(f.toPath()); + } else { + log.debug("ignoring system classpath element '{}' - only jar files allowed", path); + } + } + } + } + + // Skip preconfigured libs only when both the test knob is set and manifest-libs feature is + // enabled + boolean skipPreconfLibs = Boolean.getBoolean("btrace.test.skipLibs"); + if (!(skipPreconfLibs && useManifestLibs)) { + addPreconfLibs(libs); + } else if (log.isDebugEnabled()) { + log.debug( + "Skipping addPreconfLibs due to btrace.test.skipLibs=true and manifestLibs enabled"); + } + + // Explicit, last-resort escape hatch: append a single jar to system CL + String sysAppend = System.getProperty("btrace.system.appendJar"); + if (sysAppend != null && !sysAppend.isEmpty()) { + if (!settings.isTrusted()) { + log.warn( + "Ignoring btrace.system.appendJar: requires trusted=true. " + + "Use extensions or migration patterns instead."); + } else { + try { + Path p = Paths.get(sysAppend).toAbsolutePath().normalize(); + if (!Files.exists(p) || !p.getFileName().toString().toLowerCase().endsWith(".jar")) { + log.warn("btrace.system.appendJar invalid or not found: {}", p); + } else { + boolean allowExternal = Boolean.getBoolean("btrace.allowExternalLibs"); + String homeStr = getBTraceHome(); + if (!allowExternal && homeStr != null) { + try { + Path home = Paths.get(homeStr).toRealPath(); + Path rp = p.toRealPath(); + if (!rp.startsWith(home)) { + log.warn( + "Rejecting btrace.system.appendJar outside BTRACE_HOME ({}): {}. " + + "Override with -Dbtrace.allowExternalLibs=true", + home, + rp); + } else { + appendSystemJar(p); + } + } catch (Exception e) { + // Fallback to basic check if realpath resolution fails + if (!p.startsWith(homeStr)) { + log.warn( + "Rejecting btrace.system.appendJar outside BTRACE_HOME ({}): {}. " + + "Override with -Dbtrace.allowExternalLibs=true", + homeStr, + p); + } else { + appendSystemJar(p); + } + } + } else { + if (!allowExternal) { + log.warn( + "Cannot determine BTRACE_HOME; proceeding to append system jar (btrace.system.appendJar): {}", + p); + } + appendSystemJar(p); + } + } + } catch (Exception e) { + log.warn("Failed to process btrace.system.appendJar: {}", e.toString()); + } + } + } + + if (log.isDebugEnabled()) { + log.debug("Effective bootstrap jars: {}", BOOT_ADDED); + log.debug("Effective system jars: {}", SYSTEM_ADDED); + } + } + + // Resolved once; null means pre-JPMS JarFile constructor should be used. + private static final java.lang.reflect.Constructor JPMS_JAR_CTOR = resolveJpmsJarCtor(); + + @SuppressWarnings("JavaReflectionMemberAccess") + private static java.lang.reflect.Constructor resolveJpmsJarCtor() { + try { + Class.forName("java.lang.Module"); + Class rtClass = Runtime.class; + java.lang.reflect.Method m = rtClass.getMethod("version"); + Object version = m.invoke(null); + return JarFile.class.getConstructor(File.class, boolean.class, int.class, version.getClass()); + } catch (Exception ignore) { + return null; + } + } + + @SuppressWarnings("JavaReflectionMemberAccess") + private static JarFile asJarFile(File path) throws IOException { + if (JPMS_JAR_CTOR != null) { + try { + Class rtClass = Runtime.class; + Object version = rtClass.getMethod("version").invoke(null); + return (JarFile) JPMS_JAR_CTOR.newInstance(path, true, ZipFile.OPEN_READ, version); + } catch (Exception ignore) { + } + } + return new JarFile(path); + } + + // Centralized helpers with dedup and logging + private static void appendBootJar(Path jarPath) { + try { + Path rp = jarPath.toAbsolutePath().normalize(); + if (!Files.exists(rp)) { + if (log.isDebugEnabled()) log.debug("Bootstrap jar does not exist: {}", rp); + return; + } + // Synchronize the check-then-act so two threads cannot both pass the "not yet added" + // check and both call appendToBootstrapClassLoaderSearch for the same jar. + synchronized (BOOT_ADDED) { + if (BOOT_ADDED.contains(rp)) { + if (log.isDebugEnabled()) log.debug("Skipping duplicate bootstrap jar: {}", rp); + return; + } + JarFile jf = asJarFile(rp.toFile()); + inst.appendToBootstrapClassLoaderSearch(jf); + BOOT_ADDED.add(rp); + BOOT_JARS.add(jf); + } + if (log.isDebugEnabled()) log.debug("Added to bootstrap: {}", rp); + } catch (IOException e) { + log.warn("Failed to append bootstrap jar {}: {}", jarPath, e.toString()); + } + } + + private static void appendSystemJar(Path jarPath) { + try { + Path rp = jarPath.toAbsolutePath().normalize(); + if (!Files.exists(rp)) { + if (log.isDebugEnabled()) log.debug("System jar does not exist: {}", rp); + return; + } + synchronized (SYSTEM_ADDED) { + if (SYSTEM_ADDED.contains(rp)) { + if (log.isDebugEnabled()) log.debug("Skipping duplicate system jar: {}", rp); + return; + } + JarFile jf = asJarFile(rp.toFile()); + inst.appendToSystemClassLoaderSearch(jf); + SYSTEM_ADDED.add(rp); + SYSTEM_JARS.add(jf); + } + if (log.isDebugEnabled()) log.debug("Added to system: {}", rp); + } catch (IOException e) { + log.warn("Failed to append system jar {}: {}", jarPath, e.toString()); + } + } + + private static void addPreconfLibs(String libs) { + ClassLoader cl = Main.class.getClassLoader(); + String resourceName = Main.class.getName().replace('.', '/') + ".class"; + // Handle bootstrap classloader case (returns null) by using system classloader + URL u = + (cl != null) ? cl.getResource(resourceName) : ClassLoader.getSystemResource(resourceName); + if (u != null) { + String path = u.toString(); + int delimiterPos = path.lastIndexOf('!'); + if (delimiterPos > -1) { + String jar = path.substring(9, delimiterPos); + File jarFile = new File(jar); + Path libRoot = new File(jarFile.getParent() + File.separator + "btrace-libs").toPath(); + Path libFolder = libs != null ? libRoot.resolve(libs) : libRoot; + if (Files.exists(libFolder)) { + appendToBootClassPath(libFolder); + appendToSysClassPath(libFolder); + } else { + if (libs != null && !libs.isEmpty()) { + log.warn( + "Invalid 'libs' configuration [{}]. Path '{}' does not exist.", + libs, + libFolder.toAbsolutePath()); + } + } + } + } + } + + private static void appendToBootClassPath(Path libFolder) { + Path bootLibs = libFolder.resolve("boot"); + if (Files.exists(bootLibs)) { + try { + Files.walkFileTree( + bootLibs, + new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + if (file.toString().toLowerCase().endsWith(".jar")) { + appendBootJar(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + log.debug("Failed to enhance bootstrap classpath", e); + } + } + } + + private static void appendToSysClassPath(Path libFolder) { + Path sysLibs = libFolder.resolve("system"); + if (Files.exists(sysLibs)) { + try { + Files.walkFileTree( + sysLibs, + new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + if (file.toString().toLowerCase().endsWith(".jar")) { + appendSystemJar(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + log.debug("Failed to enhance sytem classpath", e); + } + } + } + + private static boolean loadBTraceScript(String filePath, boolean traceToStdOut) { + if (!BTraceProbeFactory.canLoad(filePath)) return false; + + try { + String scriptName = ""; + String scriptParent = ""; + File traceScript = new File(filePath); + scriptName = traceScript.getName(); + scriptParent = traceScript.getParent(); + + if (!traceScript.exists()) { + traceScript = new File(Constants.EMBEDDED_BTRACE_SECTION_HEADER + filePath); + } + + if (scriptName.endsWith(".java")) { + if (log.isDebugEnabled()) { + log.debug("refusing {} - script should be a pre-compiled class file", filePath); + } + return false; + } + + SharedSettings clientSettings = new SharedSettings(); + clientSettings.from(settings); + clientSettings.setClientName(scriptName); + if (traceToStdOut) { + clientSettings.setOutputFile("::stdout"); + } else { + String traceOutput = clientSettings.getOutputFile(); + String outDir = clientSettings.getScriptOutputDir(); + if (traceOutput == null || traceOutput.isEmpty()) { + clientSettings.setOutputFile("${client}-${agent}.${ts}.btrace[default]"); + if (outDir == null || outDir.isEmpty()) { + clientSettings.setScriptOutputDir(scriptParent); + } + } + } + ClientContext ctx = new ClientContext(inst, transformer, argMap, clientSettings); + Client client = new FileClient(ctx, traceScript); + if (client.isInitialized()) { + handleNewClient(client).get(); + return true; + } + } catch (NullPointerException e) { + if (log.isDebugEnabled()) { + log.debug("script {} does not exist!", filePath, e); + } + } catch (RuntimeException | IOException | ExecutionException re) { + log.warn("Failed to load BTrace script {}", filePath, re); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + } + + // -- Internals only below this point + @SuppressWarnings("InfiniteLoopStatement") + private static void startServer() { + int port = BTRACE_DEFAULT_PORT; + String p = argMap.get(PORT); + if (p != null) { + try { + port = Integer.parseInt(p); + } catch (NumberFormatException exp) { + error("invalid port assuming default.."); + } + } + try { + if (log.isDebugEnabled()) { + log.debug("starting server at port {}", port); + } + System.setProperty("btrace.wireio", String.valueOf(WireIO.VERSION)); + + String scriptOutputFile = settings.getOutputFile(); + if (scriptOutputFile != null && !scriptOutputFile.isEmpty()) { + System.setProperty("btrace.output", scriptOutputFile); + } + serverSocket = new ServerSocket(port); + System.setProperty("btrace.port", String.valueOf(serverSocket.getLocalPort())); + + // Add shutdown hook to close server socket on JVM exit + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + serverRunning = false; + if (serverSocket != null && !serverSocket.isClosed()) { + try { + serverSocket.close(); + log.debug("BTrace server socket closed"); + } catch (IOException e) { + log.debug("Error closing server socket", e); + } + } + }, + "BTrace Server Shutdown")); + + } catch (IOException ioexp) { + log.error("Failed to start BTrace server on port {}", port, ioexp); + return; + } + + while (serverRunning) { + try { + log.debug("waiting for clients"); + Socket sock = serverSocket.accept(); + if (log.isDebugEnabled()) { + log.debug("client accepted {}", sock); + } + ClientContext ctx = new ClientContext(inst, transformer, argMap, settings); + Client client = RemoteClient.getClient(ctx, sock, Main::handleNewClient); + } catch (RuntimeException | IOException re) { + if (serverRunning) { + log.warn("BTrace server accept failed", re); + } + } + } + } + + private static Future handleNewClient(Client client) { + return serializedExecutor.submit( + () -> { + try { + boolean entered = BTraceRuntime.enter(); + try { + if (log.isDebugEnabled()) { + log.debug("new Client created {}", client); + } + if (client.retransformLoaded()) { + client.getRuntime().sendCommand(new StatusCommand((byte) 1)); + } + } catch (UnmodifiableClassException uce) { + log.debug("BTrace class retransformation failed", uce); + client.getRuntime().sendCommand(new ErrorCommand(uce)); + client.getRuntime().sendCommand(new StatusCommand(-1 * StatusCommand.STATUS_FLAG)); + } finally { + if (entered) { + BTraceRuntime.leave(); + } + } + } catch (Throwable t) { + log.warn("Unhandled exception in client handler", t); + } + }); + } + + /** + * Register all public static methods from {@link io.btrace.BTrace} into {@link BTraceBootstrap}. + * Called at agent startup before any probe fires so that INVOKEDYNAMIC bootstrap lookups succeed. + */ + static void registerCoreOps() { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + // --- Output --- + reg(lookup, "print", MethodType.methodType(void.class, String.class)); + reg(lookup, "println", MethodType.methodType(void.class, String.class)); + reg(lookup, "println", MethodType.methodType(void.class)); + reg(lookup, "printf", MethodType.methodType(void.class, String.class, Object[].class)); + // --- Strings --- + reg(lookup, "str", MethodType.methodType(String.class, Object.class)); + reg(lookup, "str", MethodType.methodType(String.class, boolean.class)); + reg(lookup, "str", MethodType.methodType(String.class, int.class)); + reg(lookup, "str", MethodType.methodType(String.class, long.class)); + reg(lookup, "str", MethodType.methodType(String.class, float.class)); + reg(lookup, "str", MethodType.methodType(String.class, double.class)); + reg(lookup, "concat", MethodType.methodType(String.class, String.class, String.class)); + reg( + lookup, + "substr", + MethodType.methodType(String.class, String.class, int.class, int.class)); + reg(lookup, "matches", MethodType.methodType(boolean.class, String.class, String.class)); + reg(lookup, "startsWith", MethodType.methodType(boolean.class, String.class, String.class)); + reg(lookup, "endsWith", MethodType.methodType(boolean.class, String.class, String.class)); + reg(lookup, "length", MethodType.methodType(int.class, String.class)); + // --- Numbers --- + reg(lookup, "abs", MethodType.methodType(long.class, long.class)); + reg(lookup, "abs", MethodType.methodType(double.class, double.class)); + reg(lookup, "min", MethodType.methodType(long.class, long.class, long.class)); + reg(lookup, "max", MethodType.methodType(long.class, long.class, long.class)); + reg(lookup, "min", MethodType.methodType(double.class, double.class, double.class)); + reg(lookup, "max", MethodType.methodType(double.class, double.class, double.class)); + // --- Time --- + reg(lookup, "timestamp", MethodType.methodType(long.class)); + reg(lookup, "monotonic", MethodType.methodType(long.class)); + // --- Threads --- + reg(lookup, "currentThread", MethodType.methodType(Thread.class)); + reg(lookup, "threadName", MethodType.methodType(String.class, Thread.class)); + reg(lookup, "threadId", MethodType.methodType(long.class, Thread.class)); + // --- Stack --- + reg(lookup, "stackTrace", MethodType.methodType(String.class)); + reg(lookup, "printStack", MethodType.methodType(void.class)); + reg(lookup, "stackDepth", MethodType.methodType(int.class)); + // --- Object --- + reg(lookup, "className", MethodType.methodType(String.class, Object.class)); + reg(lookup, "identity", MethodType.methodType(int.class, Object.class)); + reg(lookup, "size", MethodType.methodType(long.class, Object.class)); + // --- Control --- + reg(lookup, "exit", MethodType.methodType(void.class, int.class)); + } catch (Exception e) { + throw new RuntimeException("BTrace core op registration failed", e); + } + } + + private static void reg(MethodHandles.Lookup lookup, String name, MethodType type) + throws NoSuchMethodException, IllegalAccessException { + BTraceBootstrap.registerCoreOp( + name, type, lookup.findStatic(io.btrace.BTrace.class, name, type)); + } + + private static void error(String msg) { + System.err.println("btrace ERROR: " + msg); + } + + private static boolean isDebug() { + return settings.isDebug(); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/PerfReaderImpl.java b/btrace-agent/src/main/java/io/btrace/agent/PerfReaderImpl.java new file mode 100644 index 000000000..705c8e0d9 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/PerfReaderImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.runtime.PerfReader; +import java.net.URISyntaxException; +import sun.jvmstat.monitor.IntegerMonitor; +import sun.jvmstat.monitor.LongMonitor; +import sun.jvmstat.monitor.Monitor; +import sun.jvmstat.monitor.MonitorException; +import sun.jvmstat.monitor.MonitoredHost; +import sun.jvmstat.monitor.MonitoredVm; +import sun.jvmstat.monitor.StringMonitor; +import sun.jvmstat.monitor.VmIdentifier; + +final class PerfReaderImpl implements PerfReader { + private volatile MonitoredVm thisVm; + + private synchronized MonitoredVm getThisVm() { + if (thisVm == null) { + try { + MonitoredHost localHost = MonitoredHost.getMonitoredHost("localhost"); + VmIdentifier vmIdent = new VmIdentifier("0"); + thisVm = localHost.getMonitoredVm(vmIdent); + } catch (MonitorException | URISyntaxException me) { + throw new IllegalArgumentException("jvmstat perf counters not available: " + me); + } + } + return thisVm; + } + + private Monitor findByName(String name) { + try { + return getThisVm().findByName(name); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public int perfInt(String name) { + Monitor mon = findByName(name); + if (mon == null) { + throw new IllegalArgumentException("no such counter: " + name); + } + if (mon instanceof IntegerMonitor) { + return ((IntegerMonitor) mon).intValue(); + } else if (mon instanceof LongMonitor) { + return (int) ((LongMonitor) mon).longValue(); + } else { + throw new IllegalArgumentException(name + " is not an int"); + } + } + + @Override + public long perfLong(String name) { + Monitor mon = findByName(name); + if (mon == null) { + throw new IllegalArgumentException("no such counter: " + name); + } + if (mon instanceof LongMonitor) { + return ((LongMonitor) mon).longValue(); + } else { + throw new IllegalArgumentException(name + " is not a long"); + } + } + + @Override + public String perfString(String name) { + Monitor mon = findByName(name); + if (mon == null) { + throw new IllegalArgumentException("no such counter: " + name); + } + if (mon instanceof StringMonitor) { + return ((StringMonitor) mon).stringValue(); + } else { + throw new IllegalArgumentException(name + " is not a string"); + } + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/RemoteClient.java b/btrace-agent/src/main/java/io/btrace/agent/RemoteClient.java new file mode 100644 index 000000000..5384e6940 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/RemoteClient.java @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.*; +import io.btrace.core.comm.BinaryWireProtocol; +import io.btrace.core.comm.Command; +import io.btrace.core.comm.DisconnectCommand; +import io.btrace.core.comm.EventCommand; +import io.btrace.core.comm.ExitCommand; +import io.btrace.core.comm.InstrumentCommand; +import io.btrace.core.comm.JavaSerializationProtocol; +import io.btrace.core.comm.ListFailedExtensionsCommand; +import io.btrace.core.comm.ListProbesCommand; +import io.btrace.core.comm.PrintableCommand; +import io.btrace.core.comm.ProtocolConfig; +import io.btrace.core.comm.ProtocolNegotiator; +import io.btrace.core.comm.ProtocolVersion; +import io.btrace.core.comm.ReconnectCommand; +import io.btrace.core.comm.SetSettingsCommand; +import io.btrace.core.comm.StatusCommand; +import io.btrace.core.comm.WireProtocol; +import io.btrace.extension.ExtensionRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.locks.LockSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a remote client communicated by socket. + * + * @author A. Sundararajan + */ +@SuppressWarnings({"SynchronizeOnNonFinalField", "SynchronizationOnLocalVariableOrMethodParameter"}) +class RemoteClient extends Client { + private static final Logger log = LoggerFactory.getLogger(RemoteClient.class); + + private final class DelayedCommandExecutor implements Function { + private final boolean isConnected; + + public DelayedCommandExecutor(boolean isConnected) { + this.isConnected = isConnected; + } + + @Override + public Boolean apply(Command value) { + return dispatchCommand(value, isConnected); + } + } + + private volatile Socket sock; + private volatile WireProtocol protocol; + private volatile boolean disconnected = false; + private final AtomicReferenceFieldUpdater sockUpdater = + AtomicReferenceFieldUpdater.newUpdater(RemoteClient.class, Socket.class, "sock"); + private final AtomicReferenceFieldUpdater protocolUpdater = + AtomicReferenceFieldUpdater.newUpdater(RemoteClient.class, WireProtocol.class, "protocol"); + + private final CircularBuffer delayedCommands = new CircularBuffer<>(5000); + + static Client getClient(ClientContext ctx, Socket sock, Function> initCallback) + throws IOException { + SharedSettings settings = ctx.getSettings(); + ProtocolConfig config = ProtocolConfig.fromSystemProperties(); + InputStream rawInput = sock.getInputStream(); + OutputStream output = sock.getOutputStream(); + ProtocolNegotiator negotiator = new ProtocolNegotiator(config.getVersion()); + PushbackInputStream input = ProtocolNegotiator.createNegotiationStream(rawInput); + ProtocolVersion negotiated; + if (config.isAutoNegotiate()) { + negotiated = negotiator.negotiateAgent(input, output); + } else if (config.getVersion() == ProtocolVersion.V2) { + negotiated = negotiator.negotiateAgent(input, output); + if (negotiated != ProtocolVersion.V2) { + throw new IOException("Protocol negotiation failed: expected V2"); + } + } else { + negotiated = ProtocolVersion.V1; + } + + WireProtocol wireProtocol = + negotiated == ProtocolVersion.V2 + ? new BinaryWireProtocol(input, output) + : new JavaSerializationProtocol(input, output); + + while (true) { + Command cmd; + try { + cmd = wireProtocol.read(); + } catch (ClassNotFoundException e) { + throw new IOException(e); + } + switch (cmd.getType()) { + case Command.SET_PARAMS: + { + settings.from(((SetSettingsCommand) cmd).getParams()); + break; + } + case Command.INSTRUMENT: + { + log.debug("got instrument command"); + try { + Client client = new RemoteClient(ctx, wireProtocol, sock, (InstrumentCommand) cmd); + initCallback.apply(client).get(); + client.sendCommand(new StatusCommand(StatusCommand.STATUS_FLAG)); + return client; + } catch (ExecutionException | InterruptedException e) { + wireProtocol.write(new StatusCommand(-1 * StatusCommand.STATUS_FLAG)); + throw new IOException(e); + } + } + case Command.RECONNECT: + { + String probeId = ((ReconnectCommand) cmd).getProbeId(); + log.debug("Attempting to reconnect client for probe {}", probeId); + Client client = Client.findClient(probeId); + log.debug("Found client {}", client); + if (client instanceof RemoteClient) { + ((RemoteClient) client).reconnect(wireProtocol, sock); + client.sendCommand(new StatusCommand(ReconnectCommand.STATUS_FLAG)); + return client; + } + wireProtocol.write(new StatusCommand(-1 * ReconnectCommand.STATUS_FLAG)); + throw new IOException("Can not reconnect to non-remote session"); + } + case Command.LIST_PROBES: + { + ListProbesCommand listProbesCommand = (ListProbesCommand) cmd; + listProbesCommand.setProbes(Client.listProbes()); + wireProtocol.write(listProbesCommand); + break; + } + case Command.LIST_FAILED_EXTENSIONS: + { + ListFailedExtensionsCommand listFailedCmd = (ListFailedExtensionsCommand) cmd; + listFailedCmd.setFailedExtensions(ExtensionRegistry.getFailedExtensions()); + wireProtocol.write(listFailedCmd); + break; + } + case Command.EXIT: + { + return null; + } + default: + { + throw new IOException( + "expecting instrument, reconnect or settings command! (" + cmd.getClass() + ")"); + } + } + } + } + + private RemoteClient(ClientContext ctx, WireProtocol protocol, Socket sock, InstrumentCommand cmd) + throws IOException { + super(ctx); + this.sock = sock; + this.protocol = protocol; + this.settings.from(ctx.getSettings()); + Class btraceClazz = loadClass(cmd); + if (btraceClazz == null) { + throw new RuntimeException("can not load BTrace class"); + } + + initClient(); + } + + private void initClient() { + BTraceRuntime.initUnsafe(); + Thread cmdHandler = + new Thread( + () -> { + try { + BTraceRuntime.enter(); + while (true) { + try { + if (protocol == null) { + LockSupport.parkNanos(500_000_000L); // sleep 500ms + continue; + } + Command cmd = protocol.read(); + switch (cmd.getType()) { + case Command.EXIT: + { + log.debug("received exit command"); + onCommand(cmd); + + return; + } + case Command.DISCONNECT: + { + log.debug("received disconnect command"); + onCommand(cmd); + break; + } + case Command.LIST_PROBES: + { + onCommand(cmd); + break; + } + case Command.LIST_FAILED_EXTENSIONS: + { + onCommand(cmd); + break; + } + case Command.EVENT: + { + BTraceRuntime.Impl rt = getRuntime(); + if (rt != null) { + rt.handleEvent((EventCommand) cmd); + } + break; + } + default: + if (log.isDebugEnabled()) { + log.debug("received {}", cmd); + } + // ignore any other command + } + } catch (Exception exp) { + // When the client disconnects normally, ObjectInputStream.read may throw + // EOFException. Treat it as a clean shutdown and avoid noisy stack traces + // that end up in target stderr during debug runs. + if (exp instanceof java.io.EOFException || exp instanceof SocketException) { + if (log.isDebugEnabled()) { + log.debug("client command stream closed: {}", exp.toString()); + } + } else { + log.debug("Error while processing BTrace command", exp); + } + break; + } + } + } finally { + BTraceRuntime.leave(); + try { + // Ensure streams and socket are closed once the client side closed first. + closeAll(); + } catch (IOException ignore) { + // best effort + } + } + }); + cmdHandler.setDaemon(true); + log.debug("starting client command handler thread"); + cmdHandler.start(); + } + + @SuppressWarnings("RedundantThrows") + @Override + public void onCommand(Command cmd) throws IOException { + WireProtocol output = protocol; + if (output == null) { + if (!cmd.isUrgent()) { + delayedCommands.add(cmd); + } + return; + } + if (log.isDebugEnabled()) { + log.debug("client {}: got {}", getClassName(), cmd); + } + boolean isConnected = true; + try { + synchronized (output) { + output.flush(); + } + } catch (IOException e) { + isConnected = false; + } + + delayedCommands.forEach(new DelayedCommandExecutor(isConnected)); + + if (!dispatchCommand(cmd, isConnected)) { + if (!cmd.isUrgent()) { + delayedCommands.add(cmd); + } + } + } + + private boolean dispatchCommand(Command cmd, boolean isConnected) { + if (cmd == Command.NULL) { + return true; // do not dispatch the NULL command + } + WireProtocol output = protocol; + Socket socket = sock; + if (output == null) { + return false; + } + try { + switch (cmd.getType()) { + case Command.EXIT: + if (isConnected) { + output.write(cmd); + } + onExit(((ExitCommand) cmd).getExitCode()); + break; + case Command.LIST_PROBES: + { + if (isConnected) { + ((ListProbesCommand) cmd).setProbes(listProbes()); + output.write(cmd); + } + break; + } + case Command.LIST_FAILED_EXTENSIONS: + { + if (isConnected) { + ((ListFailedExtensionsCommand) cmd) + .setFailedExtensions(ExtensionRegistry.getFailedExtensions()); + output.write(cmd); + } + break; + } + case Command.DISCONNECT: + { + ((DisconnectCommand) cmd).setProbeId(id.toString()); + synchronized (output) { + output.write(cmd); + output.flush(); + try { + // Half-close the output to allow the client to read DISCONNECT reliably + if (socket != null && !socket.isClosed()) { + socket.shutdownOutput(); + } + } catch (IOException ioe) { + // ignore; best effort + } + } + if (log.isDebugEnabled()) { + log.debug("sent DISCONNECT to client and shutdown socket output"); + } + disconnected = true; + break; + } + default: + if (out != null) { + if (cmd instanceof PrintableCommand) { + ((PrintableCommand) cmd).print(out); + break; + } + } + if (isConnected) { + output.write(cmd); + } + } + return true; + } catch (IOException e) { + return false; + } + } + + public boolean isDisconnected() { + return disconnected; + } + + @Override + protected void sendCommand(Command command) { + if (getRuntime() != null) { + super.sendCommand(command); + return; + } + // Runtime not yet initialized - send directly via protocol + WireProtocol output = protocol; + if (output != null) { + try { + synchronized (output) { + output.write(command); + output.flush(); + } + } catch (IOException e) { + log.warn("Failed to send command {} via protocol", command.getClass().getSimpleName(), e); + } + } else { + log.warn( + "Cannot send command {}, neither runtime nor protocol available", + command.getClass().getSimpleName()); + } + } + + @Override + protected void closeAll() throws IOException { + super.closeAll(); + disconnected = true; + + WireProtocol output = protocol; + if (output != null) { + output.close(); + protocolUpdater.compareAndSet(this, output, null); + } + Socket socket = sock; + if (socket != null) { + socket.close(); + sockUpdater.compareAndSet(this, socket, null); + } + } + + void reconnect(WireProtocol protocol, Socket socket) throws IOException { + this.sock = socket; + this.protocol = protocol; + this.disconnected = false; + onCommand(Command.NULL); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/Telemetry.java b/btrace-agent/src/main/java/io/btrace/agent/Telemetry.java new file mode 100644 index 000000000..b74577300 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/Telemetry.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.UUID; + +final class Telemetry { + + // Public project API key — safe to commit (PostHog design intent). + // Replace with the actual key from your PostHog project settings. + static final String API_KEY = "phc_tGurJ2fAYeouW4k8Txkn3zrfrKgoiuXgrAJP33ufX9Hv"; + + static final String ENDPOINT = "https://eu.posthog.com/capture/"; + + // Hard wall-clock cap for the guard thread (covers DNS + connect + read). + // connectTimeout alone does not bound DNS resolution — the guard join does. + static final int GUARD_TIMEOUT_MS = 2000; + static final int CONNECT_TIMEOUT_MS = 1000; + static final int READ_TIMEOUT_MS = 1000; + + private static final String PROP_DISABLED = "btrace.telemetry"; + + private Telemetry() {} + + static boolean isEnabled() { + return !"false".equalsIgnoreCase(System.getProperty(PROP_DISABLED, "true")); + } + + static String buildPayload(String btraceVersion, String agentMode) { + String javaVersion = escape(System.getProperty("java.version", "unknown")); + String osName = escape(System.getProperty("os.name", "unknown")); + String distinctId = UUID.randomUUID().toString(); + return "{" + + "\"api_key\":\"" + + API_KEY + + "\"," + + "\"event\":\"agent_start\"," + + "\"distinct_id\":\"" + + distinctId + + "\"," + + "\"properties\":{" + + "\"java_version\":\"" + + javaVersion + + "\"," + + "\"os_name\":\"" + + osName + + "\"," + + "\"btrace_version\":\"" + + escape(btraceVersion) + + "\"," + + "\"agent_mode\":\"" + + escape(agentMode) + + "\"" + + "}" + + "}"; + } + + // fireAsync returns immediately — the calling thread (agent startup) is never blocked. + static void fireAsync(final String btraceVersion, final String agentMode) { + if (!isEnabled()) { + return; + } + final Thread worker = + new Thread( + new Runnable() { + @Override + public void run() { + send(btraceVersion, agentMode); + } + }); + worker.setDaemon(true); + worker.setName("btrace-telemetry"); + + // Guard caps total wall-clock time including DNS, which connectTimeout + // alone does not cover. Both threads are daemon threads. + Thread guard = + new Thread( + new Runnable() { + @Override + public void run() { + try { + worker.start(); + worker.join(GUARD_TIMEOUT_MS); + worker.interrupt(); + } catch (Throwable ignored) { + } + } + }); + guard.setDaemon(true); + guard.setName("btrace-telemetry-guard"); + guard.start(); + } + + private static void send(String btraceVersion, String agentMode) { + try { + String payload = buildPayload(btraceVersion, agentMode); + byte[] body = payload.getBytes(Charset.forName("UTF-8")); + URL url = new URL(ENDPOINT); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + conn.setRequestProperty("Content-Length", String.valueOf(body.length)); + conn.setDoOutput(true); + try (OutputStream out = conn.getOutputStream()) { + out.write(body); + } + conn.getResponseCode(); + conn.disconnect(); + } catch (Throwable ignored) { + } + } + + private static String escape(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\""); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/agent/TraceOutputWriter.java b/btrace-agent/src/main/java/io/btrace/agent/TraceOutputWriter.java new file mode 100644 index 000000000..789f7487e --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/agent/TraceOutputWriter.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.agent; + +import io.btrace.core.SharedSettings; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents various strategies available for dumping BTrace output to a file. + * + * @author Jaroslav Bachorik + */ +abstract class TraceOutputWriter extends Writer { + private static final Logger log = LoggerFactory.getLogger(TraceOutputWriter.class); + + protected TraceOutputWriter() {} + + /** + * Plain file writer - all output will go to one specified file + * + * @param output The file to put the output to + * @return Returns an appropriate {@linkplain TraceOutputWriter} instance or NULL + */ + public static TraceOutputWriter fileWriter(File output) { + TraceOutputWriter instance = null; + try { + instance = new SimpleFileWriter(output); + } catch (IOException e) { + // ignore + } + return instance; + } + + /** + * Time based rolling file writer. Defaults to 100 allowed output chunks. + * + * @param output The file to put the output to + * @param settings The shared settings + * @return Returns an appropriate {@linkplain TraceOutputWriter} instance or NULL + */ + public static TraceOutputWriter rollingFileWriter(File output, SharedSettings settings) { + TraceOutputWriter instance = null; + try { + instance = new TimeBasedRollingFileWriter(output, settings); + } catch (IOException e) { + // ignore + } + return instance; + } + + private static void ensurePathExists(File f) { + if (f == null || f.exists()) return; + + ensurePathExists(f.getParentFile()); + + if (!f.getName().endsWith(".btrace")) { // not creating the actual file + try { + f.createNewFile(); + } catch (IOException e) { + log.debug("Failed to create directory: {}", f, e); + // ignore and continue + } + } + } + + private static class SimpleFileWriter extends TraceOutputWriter { + private static final Logger log = LoggerFactory.getLogger(SimpleFileWriter.class); + + private final FileWriter delegate; + + @SuppressWarnings("DefaultCharset") + public SimpleFileWriter(File output) throws IOException { + try { + File parent = output.getParentFile(); + if (parent != null) { + output.getParentFile().mkdirs(); + } + delegate = new FileWriter(output); + } catch (IOException e) { + log.debug("Failed to create file output {}", output.getName(), e); + throw e; + } + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + delegate.write(cbuf, off, len); + } + } + + @SuppressWarnings("DefaultCharset") + private abstract static class RollingFileWriter extends TraceOutputWriter { + private static final Logger log = LoggerFactory.getLogger(RollingFileWriter.class); + protected final SharedSettings settings; + private final ReentrantReadWriteLock writerLock = new ReentrantReadWriteLock(); + private final String path, baseName; + // @GuardedBy writerLock + private FileWriter currentFileWriter; + private int counter = 1; + + public RollingFileWriter(File output, SharedSettings settings) throws IOException { + try { + output.getParentFile().mkdirs(); + currentFileWriter = new FileWriter(output); + path = output.getParentFile().getAbsolutePath(); + baseName = output.getName(); + this.settings = settings; + } catch (IOException e) { + log.debug("Failed to create rolling file output {}", output.getName(), e); + throw e; + } + } + + @Override + public final void close() throws IOException { + try { + writerLock.readLock().lock(); + currentFileWriter.close(); + } finally { + writerLock.readLock().unlock(); + } + } + + @Override + public final void flush() throws IOException { + try { + writerLock.readLock().lock(); + currentFileWriter.flush(); + } finally { + writerLock.readLock().unlock(); + } + + if (needsRoll()) { + nextWriter(); + } + } + + @Override + public final void write(char[] cbuf, int off, int len) throws IOException { + try { + writerLock.readLock().lock(); + currentFileWriter.write(cbuf, off, len); + } finally { + writerLock.readLock().unlock(); + } + } + + private void nextWriter() { + try { + writerLock.writeLock().lock(); + currentFileWriter = getNextWriter(); + } catch (IOException e) { + log.debug("Failed to roll over", e); + } finally { + writerLock.writeLock().unlock(); + } + } + + private FileWriter getNextWriter() throws IOException { + currentFileWriter.close(); + File scriptOutputFile_renameFrom = new File(path + File.separator + baseName); + File scriptOutputFile_renameTo = + new File(path + File.separator + baseName + "." + (counter++)); + + if (scriptOutputFile_renameTo.exists()) { + scriptOutputFile_renameTo.delete(); + } + scriptOutputFile_renameFrom.renameTo(scriptOutputFile_renameTo); + scriptOutputFile_renameFrom = new File(path + File.separator + baseName); + if (counter > settings.getFileRollMaxRolls()) { + counter = 1; + } + return new FileWriter(scriptOutputFile_renameFrom); + } + + protected abstract boolean needsRoll(); + } + + private static class TimeBasedRollingFileWriter extends RollingFileWriter { + private final TimeUnit unit = TimeUnit.MILLISECONDS; + private long lastTimeStamp = System.currentTimeMillis(); + + public TimeBasedRollingFileWriter(File output, SharedSettings settings) throws IOException { + super(output, settings); + } + + @Override + protected boolean needsRoll() { + long currTime = System.currentTimeMillis(); + long myInterval = currTime - lastTimeStamp; + if (unit.convert(myInterval, TimeUnit.MILLISECONDS) >= settings.getFileRollMilliseconds()) { + lastTimeStamp = currTime; + return true; + } + return false; + } + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/ArrayAccessInstrumentor.java b/btrace-agent/src/main/java/io/btrace/instr/ArrayAccessInstrumentor.java new file mode 100644 index 000000000..936346dc0 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ArrayAccessInstrumentor.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static org.objectweb.asm.Opcodes.*; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +/** + * This visitor helps in inserting code whenever an array access is done. Code to insert on array + * access may be decided by derived class. By default, this class inserts code to print array + * access. + * + * @author A. Sundararajan + */ +public class ArrayAccessInstrumentor extends MethodInstrumentor { + public ArrayAccessInstrumentor( + ClassLoader cl, + MethodVisitor mv, + MethodInstrumentorHelper mHelper, + String parentClz, + String superClz, + int access, + String name, + String desc) { + super(cl, mv, mHelper, parentClz, superClz, access, name, desc); + } + + @Override + public void visitInsn(int opcode) { + boolean arrayload = false; + boolean arraystore = false; + switch (opcode) { + case IALOAD: + case LALOAD: + case FALOAD: + case DALOAD: + case AALOAD: + case BALOAD: + case CALOAD: + case SALOAD: + arrayload = true; + break; + + case IASTORE: + case LASTORE: + case FASTORE: + case DASTORE: + case AASTORE: + case BASTORE: + case CASTORE: + case SASTORE: + arraystore = true; + break; + } + if (arrayload) { + onBeforeArrayLoad(opcode); + } else if (arraystore) { + onBeforeArrayStore(opcode); + } + super.visitInsn(opcode); + if (arrayload) { + onAfterArrayLoad(opcode); + } else if (arraystore) { + onAfterArrayStore(opcode); + } + } + + protected final boolean locationTypeMismatch(Location loc, Type arrtype, Type itemType) { + return !loc.getType().isEmpty() + && (!loc.getType().equals(arrtype.getClassName()) + && !loc.getType().equals(itemType.getClassName())); + } + + protected void onBeforeArrayLoad(int opcode) {} + + protected void onAfterArrayLoad(int opcode) {} + + protected void onBeforeArrayStore(int opcode) {} + + protected void onAfterArrayStore(int opcode) {} +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/ArrayAllocInstrumentor.java b/btrace-agent/src/main/java/io/btrace/instr/ArrayAllocInstrumentor.java new file mode 100644 index 000000000..67fd03796 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ArrayAllocInstrumentor.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static org.objectweb.asm.Opcodes.ANEWARRAY; +import static org.objectweb.asm.Opcodes.NEWARRAY; + +import org.objectweb.asm.MethodVisitor; + +/** + * This visitor helps in inserting code whenever an array is allocated. The code to insert on method + * entry may be decided by derived class. By default, this class inserts code to print allocated + * array objects. + * + * @author A. Sundararajan + */ +public class ArrayAllocInstrumentor extends MethodInstrumentor { + public ArrayAllocInstrumentor( + ClassLoader cl, + MethodVisitor mv, + MethodInstrumentorHelper mHelper, + String parentClz, + String superClz, + int access, + String name, + String desc) { + super(cl, mv, mHelper, parentClz, superClz, access, name, desc); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + String desc = null; + if (opcode == NEWARRAY) { + desc = InstrumentUtils.arrayDescriptorFor(operand); + onBeforeArrayNew(getPlainType(desc), 1); + } + super.visitIntInsn(opcode, operand); + if (opcode == NEWARRAY) { + onAfterArrayNew(getPlainType(desc), 1); + } + } + + @Override + public void visitTypeInsn(int opcode, String desc) { + if (opcode == ANEWARRAY) { + onBeforeArrayNew("L" + desc + ";", 1); + } + super.visitTypeInsn(opcode, desc); + if (opcode == ANEWARRAY) { + onAfterArrayNew("L" + desc + ";", 1); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + String type = getPlainType(desc); + onBeforeArrayNew(type, dims); + super.visitMultiANewArrayInsn(desc, dims); + onAfterArrayNew(type, dims); + } + + protected void onBeforeArrayNew(String desc, int dims) { + asm.println("before allocating " + desc); + } + + protected void onAfterArrayNew(String desc, int dims) { + asm.dup().printObject(); + } + + private String getPlainType(String desc) { + int index = desc.lastIndexOf('[') + 1; + if (index > 0) { + return desc.substring(index); + } + return desc; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/AsmInstrumentationBackend.java b/btrace-agent/src/main/java/io/btrace/instr/AsmInstrumentationBackend.java new file mode 100644 index 000000000..692526450 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/AsmInstrumentationBackend.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.Collection; + +/** + * The default instrumentation backend; delegates to the existing ASM-based pipeline. Supports class + * file major versions up to {@value #MAX_ASM_MAJOR_VERSION} (Java 25), which is the ceiling for ASM + * 9.9.x. + */ +final class AsmInstrumentationBackend implements InstrumentationBackend { + + /** Highest class file major version ASM 9.9.x can parse without throwing. */ + static final int MAX_ASM_MAJOR_VERSION = 69; // Java 25 + + @Override + public boolean supports(int classFileMajorVersion) { + return classFileMajorVersion <= MAX_ASM_MAJOR_VERSION; + } + + @Override + public byte[] instrument( + ClassLoader loader, byte[] classfileBuffer, Collection probes) { + BTraceClassReader cr = InstrumentUtils.newClassReader(loader, classfileBuffer); + BTraceClassWriter cw = InstrumentUtils.newClassWriter(cr); + for (BTraceProbe p : probes) { + cw.addInstrumentor(p, loader); + } + return cw.instrument(); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/Assembler.java b/btrace-agent/src/main/java/io/btrace/instr/Assembler.java new file mode 100644 index 000000000..5c71d7c69 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/Assembler.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static io.btrace.instr.Constants.BOOLEAN_BOXED_INTERNAL; +import static io.btrace.instr.Constants.BOOLEAN_VALUE; +import static io.btrace.instr.Constants.BOOLEAN_VALUE_DESC; +import static io.btrace.instr.Constants.BOX_BOOLEAN_DESC; +import static io.btrace.instr.Constants.BOX_BYTE_DESC; +import static io.btrace.instr.Constants.BOX_CHARACTER_DESC; +import static io.btrace.instr.Constants.BOX_DOUBLE_DESC; +import static io.btrace.instr.Constants.BOX_FLOAT_DESC; +import static io.btrace.instr.Constants.BOX_INTEGER_DESC; +import static io.btrace.instr.Constants.BOX_LONG_DESC; +import static io.btrace.instr.Constants.BOX_SHORT_DESC; +import static io.btrace.instr.Constants.BOX_VALUEOF; +import static io.btrace.instr.Constants.BTRACE_LEVEL_FLD; +import static io.btrace.instr.Constants.BYTE_BOXED_INTERNAL; +import static io.btrace.instr.Constants.BYTE_VALUE; +import static io.btrace.instr.Constants.BYTE_VALUE_DESC; +import static io.btrace.instr.Constants.CHARACTER_BOXED_INTERNAL; +import static io.btrace.instr.Constants.CHAR_VALUE; +import static io.btrace.instr.Constants.CHAR_VALUE_DESC; +import static io.btrace.instr.Constants.DOUBLE_BOXED_INTERNAL; +import static io.btrace.instr.Constants.DOUBLE_VALUE; +import static io.btrace.instr.Constants.DOUBLE_VALUE_DESC; +import static io.btrace.instr.Constants.FLOAT_BOXED_INTERNAL; +import static io.btrace.instr.Constants.FLOAT_VALUE; +import static io.btrace.instr.Constants.FLOAT_VALUE_DESC; +import static io.btrace.instr.Constants.INTEGER_BOXED_INTERNAL; +import static io.btrace.instr.Constants.INT_DESC; +import static io.btrace.instr.Constants.INT_VALUE; +import static io.btrace.instr.Constants.INT_VALUE_DESC; +import static io.btrace.instr.Constants.LONG_BOXED_INTERNAL; +import static io.btrace.instr.Constants.LONG_VALUE; +import static io.btrace.instr.Constants.LONG_VALUE_DESC; +import static io.btrace.instr.Constants.NUMBER_INTERNAL; +import static io.btrace.instr.Constants.SHORT_BOXED_INTERNAL; +import static io.btrace.instr.Constants.SHORT_VALUE; +import static io.btrace.instr.Constants.SHORT_VALUE_DESC; +import static org.objectweb.asm.Opcodes.AALOAD; +import static org.objectweb.asm.Opcodes.AASTORE; +import static org.objectweb.asm.Opcodes.ACONST_NULL; +import static org.objectweb.asm.Opcodes.ANEWARRAY; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.BALOAD; +import static org.objectweb.asm.Opcodes.BASTORE; +import static org.objectweb.asm.Opcodes.BIPUSH; +import static org.objectweb.asm.Opcodes.CALOAD; +import static org.objectweb.asm.Opcodes.CASTORE; +import static org.objectweb.asm.Opcodes.CHECKCAST; +import static org.objectweb.asm.Opcodes.DALOAD; +import static org.objectweb.asm.Opcodes.DASTORE; +import static org.objectweb.asm.Opcodes.DCONST_0; +import static org.objectweb.asm.Opcodes.DRETURN; +import static org.objectweb.asm.Opcodes.DSUB; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.DUP2; +import static org.objectweb.asm.Opcodes.DUP2_X1; +import static org.objectweb.asm.Opcodes.DUP2_X2; +import static org.objectweb.asm.Opcodes.DUP_X1; +import static org.objectweb.asm.Opcodes.DUP_X2; +import static org.objectweb.asm.Opcodes.FALOAD; +import static org.objectweb.asm.Opcodes.FASTORE; +import static org.objectweb.asm.Opcodes.FCONST_0; +import static org.objectweb.asm.Opcodes.FRETURN; +import static org.objectweb.asm.Opcodes.FSUB; +import static org.objectweb.asm.Opcodes.GETFIELD; +import static org.objectweb.asm.Opcodes.GETSTATIC; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IALOAD; +import static org.objectweb.asm.Opcodes.IASTORE; +import static org.objectweb.asm.Opcodes.ICONST_0; +import static org.objectweb.asm.Opcodes.ICONST_1; +import static org.objectweb.asm.Opcodes.ICONST_2; +import static org.objectweb.asm.Opcodes.ICONST_3; +import static org.objectweb.asm.Opcodes.ICONST_4; +import static org.objectweb.asm.Opcodes.ICONST_5; +import static org.objectweb.asm.Opcodes.ICONST_M1; +import static org.objectweb.asm.Opcodes.IFNE; +import static org.objectweb.asm.Opcodes.IF_ICMPGT; +import static org.objectweb.asm.Opcodes.IF_ICMPLT; +import static org.objectweb.asm.Opcodes.ILOAD; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.IRETURN; +import static org.objectweb.asm.Opcodes.ISTORE; +import static org.objectweb.asm.Opcodes.ISUB; +import static org.objectweb.asm.Opcodes.LALOAD; +import static org.objectweb.asm.Opcodes.LASTORE; +import static org.objectweb.asm.Opcodes.LCONST_0; +import static org.objectweb.asm.Opcodes.LCONST_1; +import static org.objectweb.asm.Opcodes.LRETURN; +import static org.objectweb.asm.Opcodes.LSUB; +import static org.objectweb.asm.Opcodes.NEW; +import static org.objectweb.asm.Opcodes.POP; +import static org.objectweb.asm.Opcodes.PUTFIELD; +import static org.objectweb.asm.Opcodes.PUTSTATIC; +import static org.objectweb.asm.Opcodes.RETURN; +import static org.objectweb.asm.Opcodes.SALOAD; +import static org.objectweb.asm.Opcodes.SASTORE; +import static org.objectweb.asm.Opcodes.SIPUSH; +import static org.objectweb.asm.Opcodes.SWAP; + +import io.btrace.runtime.Interval; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +/** + * Convenient fluent wrapper over the ASM method visitor + * + * @author Jaroslav Bachorik + */ +@SuppressWarnings("UnusedReturnValue") +public final class Assembler { + private final MethodVisitor mv; + private final MethodInstrumentorHelper mHelper; + + public Assembler(MethodVisitor mv, MethodInstrumentorHelper mHelper) { + this.mv = mv; + this.mHelper = mHelper; + } + + public Assembler push(int value) { + if (value >= -1 && value <= 5) { + mv.visitInsn(ICONST_0 + value); + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + mv.visitIntInsn(BIPUSH, value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + mv.visitIntInsn(SIPUSH, value); + } else { + mv.visitLdcInsn(value); + } + return this; + } + + public Assembler arrayLoad(Type type) { + mv.visitInsn(type.getOpcode(IALOAD)); + return this; + } + + public Assembler arrayStore(Type type) { + mv.visitInsn(type.getOpcode(IASTORE)); + return this; + } + + public Assembler jump(int opcode, Label l) { + mv.visitJumpInsn(opcode, l); + return this; + } + + public Assembler ldc(Object o) { + if (o == null) { + return loadNull(); + } + if (o instanceof Integer) { + int i = (int) o; + if (i >= -1 && i <= 5) { + int opcode = -1; + switch (i) { + case 0: + { + opcode = ICONST_0; + break; + } + case 1: + { + opcode = ICONST_1; + break; + } + case 2: + { + opcode = ICONST_2; + break; + } + case 3: + { + opcode = ICONST_3; + break; + } + case 4: + { + opcode = ICONST_4; + break; + } + case 5: + { + opcode = ICONST_5; + break; + } + case -1: + { + opcode = ICONST_M1; + break; + } + } + mv.visitInsn(opcode); + return this; + } + } + if (o instanceof Long) { + long l = (long) o; + if (l >= 0 && l <= 1) { + int opcode = -1; + switch ((int) l) { + case 0: + { + opcode = LCONST_0; + break; + } + case 1: + { + opcode = LCONST_1; + break; + } + } + mv.visitInsn(opcode); + return this; + } + } + mv.visitLdcInsn(o); + return this; + } + + public Assembler sub(Type t) { + int opcode = -1; + switch (t.getSort()) { + case Type.SHORT: + case Type.BYTE: + case Type.INT: + { + opcode = ISUB; + break; + } + case Type.LONG: + { + opcode = LSUB; + break; + } + case Type.FLOAT: + { + opcode = FSUB; + break; + } + case Type.DOUBLE: + { + opcode = DSUB; + break; + } + } + if (opcode != -1) { + mv.visitInsn(opcode); + } + return this; + } + + public Assembler loadNull() { + mv.visitInsn(ACONST_NULL); + return this; + } + + public Assembler loadLocal(Type type, int index) { + mv.visitVarInsn(type.getOpcode(ILOAD), index); + return this; + } + + public Assembler storeLocal(Type type, int index) { + mv.visitVarInsn(type.getOpcode(ISTORE), index); + return this; + } + + public Assembler storeField(Type owner, String name, Type t) { + mv.visitFieldInsn(PUTFIELD, owner.getInternalName(), name, t.getDescriptor()); + return this; + } + + public Assembler storeStaticField(Type owner, String name, Type t) { + mv.visitFieldInsn(PUTSTATIC, owner.getInternalName(), name, t.getDescriptor()); + return this; + } + + public Assembler loadField(Type owner, String name, Type t) { + mv.visitFieldInsn(GETFIELD, owner.getInternalName(), name, t.getDescriptor()); + return this; + } + + public Assembler loadStaticField(Type owner, String name, Type t) { + mv.visitFieldInsn(GETSTATIC, owner.getInternalName(), name, t.getDescriptor()); + return this; + } + + public Assembler pop() { + mv.visitInsn(POP); + return this; + } + + public Assembler dup() { + mv.visitInsn(DUP); + return this; + } + + public Assembler dup_x1() { + mv.visitInsn(DUP_X1); + return this; + } + + public Assembler dup_x2() { + mv.visitInsn(DUP_X2); + return this; + } + + public Assembler dup2() { + mv.visitInsn(DUP2); + return this; + } + + public Assembler dup2_x1() { + mv.visitInsn(DUP2_X1); + return this; + } + + public Assembler dup2_x2() { + mv.visitInsn(DUP2_X2); + return this; + } + + public Assembler swap() { + mv.visitInsn(SWAP); + return this; + } + + public Assembler newInstance(Type t) { + mv.visitTypeInsn(NEW, t.getInternalName()); + return this; + } + + public Assembler newArray(Type t) { + mv.visitTypeInsn(ANEWARRAY, t.getInternalName()); + return this; + } + + public Assembler dupArrayValue(int arrayOpcode) { + switch (arrayOpcode) { + case IALOAD: + case FALOAD: + case AALOAD: + case BALOAD: + case CALOAD: + case SALOAD: + case IASTORE: + case FASTORE: + case AASTORE: + case BASTORE: + case CASTORE: + case SASTORE: + dup(); + break; + + case LALOAD: + case DALOAD: + case LASTORE: + case DASTORE: + dup2(); + break; + } + return this; + } + + public Assembler dupReturnValue(int returnOpcode) { + switch (returnOpcode) { + case IRETURN: + case FRETURN: + case ARETURN: + mv.visitInsn(DUP); + break; + case LRETURN: + case DRETURN: + mv.visitInsn(DUP2); + break; + case RETURN: + break; + default: + throw new IllegalArgumentException("not return"); + } + return this; + } + + public Assembler dupValue(Type type) { + switch (type.getSize()) { + case 1: + dup(); + break; + case 2: + dup2(); + break; + } + return this; + } + + public Assembler dupValue(String desc) { + int typeCode = desc.charAt(0); + switch (typeCode) { + case '[': + case 'L': + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitInsn(DUP); + break; + case 'J': + case 'D': + mv.visitInsn(DUP2); + break; + default: + throw new InstrumentationException( + String.format("Invalid bytecode signature in dupValue: %s", desc)); + } + return this; + } + + public Assembler box(Type type) { + return box(type.getDescriptor()); + } + + public Assembler box(String desc) { + int typeCode = desc.charAt(0); + switch (typeCode) { + case '[': + case 'L': + break; + case 'Z': + invokeStatic(BOOLEAN_BOXED_INTERNAL, BOX_VALUEOF, BOX_BOOLEAN_DESC); + break; + case 'C': + invokeStatic(CHARACTER_BOXED_INTERNAL, BOX_VALUEOF, BOX_CHARACTER_DESC); + break; + case 'B': + invokeStatic(BYTE_BOXED_INTERNAL, BOX_VALUEOF, BOX_BYTE_DESC); + break; + case 'S': + invokeStatic(SHORT_BOXED_INTERNAL, BOX_VALUEOF, BOX_SHORT_DESC); + break; + case 'I': + invokeStatic(INTEGER_BOXED_INTERNAL, BOX_VALUEOF, BOX_INTEGER_DESC); + break; + case 'J': + invokeStatic(LONG_BOXED_INTERNAL, BOX_VALUEOF, BOX_LONG_DESC); + break; + case 'F': + invokeStatic(FLOAT_BOXED_INTERNAL, BOX_VALUEOF, BOX_FLOAT_DESC); + break; + case 'D': + invokeStatic(DOUBLE_BOXED_INTERNAL, BOX_VALUEOF, BOX_DOUBLE_DESC); + break; + } + return this; + } + + public Assembler unbox(Type type) { + return unbox(type.getDescriptor()); + } + + public Assembler unbox(String desc) { + int typeCode = desc.charAt(0); + switch (typeCode) { + case '[': + case 'L': + mv.visitTypeInsn(CHECKCAST, Type.getType(desc).getInternalName()); + break; + case 'Z': + mv.visitTypeInsn(CHECKCAST, BOOLEAN_BOXED_INTERNAL); + invokeVirtual(BOOLEAN_BOXED_INTERNAL, BOOLEAN_VALUE, BOOLEAN_VALUE_DESC); + break; + case 'C': + mv.visitTypeInsn(CHECKCAST, CHARACTER_BOXED_INTERNAL); + invokeVirtual(CHARACTER_BOXED_INTERNAL, CHAR_VALUE, CHAR_VALUE_DESC); + break; + case 'B': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, BYTE_VALUE, BYTE_VALUE_DESC); + break; + case 'S': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, SHORT_VALUE, SHORT_VALUE_DESC); + break; + case 'I': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, INT_VALUE, INT_VALUE_DESC); + break; + case 'J': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, LONG_VALUE, LONG_VALUE_DESC); + break; + case 'F': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, FLOAT_VALUE, FLOAT_VALUE_DESC); + break; + case 'D': + mv.visitTypeInsn(CHECKCAST, NUMBER_INTERNAL); + invokeVirtual(NUMBER_INTERNAL, DOUBLE_VALUE, DOUBLE_VALUE_DESC); + break; + } + return this; + } + + public Assembler defaultValue(String desc) { + int typeCode = desc.charAt(0); + switch (typeCode) { + case '[': + case 'L': + mv.visitInsn(ACONST_NULL); + break; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitInsn(ICONST_0); + break; + case 'J': + mv.visitInsn(LCONST_0); + break; + case 'F': + mv.visitInsn(FCONST_0); + break; + case 'D': + mv.visitInsn(DCONST_0); + break; + } + return this; + } + + public Assembler println(String msg) { + mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + mv.visitLdcInsn(msg); + invokeVirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V"); + return this; + } + + // print the object on the top of the stack + public Assembler printObject() { + mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + mv.visitInsn(SWAP); + mv.visitMethodInsn( + INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); + return this; + } + + public Assembler invokeVirtual(String owner, String method, String desc) { + mv.visitMethodInsn(INVOKEVIRTUAL, owner, method, desc, false); + return this; + } + + public Assembler invokeSpecial(String owner, String method, String desc) { + mv.visitMethodInsn(INVOKESPECIAL, owner, method, desc, false); + return this; + } + + public Assembler invokeStatic(String owner, String method, String desc) { + mv.visitMethodInsn(INVOKESTATIC, owner, method, desc, false); + return this; + } + + public Assembler invokeDynamic( + String name, String descriptor, Handle bootstrap, Object... bootstrapArguments) { + mv.visitInvokeDynamicInsn(name, descriptor, bootstrap, bootstrapArguments); + return this; + } + + public Assembler invokeInterface(String owner, String method, String desc) { + mv.visitMethodInsn(INVOKEVIRTUAL, owner, method, desc, true); + return this; + } + + public Assembler getStatic(String owner, String name, String desc) { + mv.visitFieldInsn(GETSTATIC, owner, name, desc); + return this; + } + + public Assembler putStatic(String owner, String name, String desc) { + mv.visitFieldInsn(PUTSTATIC, owner, name, desc); + return this; + } + + public Assembler label(Label l) { + mv.visitLabel(l); + return this; + } + + public Assembler addLevelCheck(String clsName, Level level, Label jmp) { + return addLevelCheck(clsName, level.getValue(), jmp); + } + + public Assembler addLevelCheck(String clsName, Interval itv, Label jmp) { + getStatic(clsName, "$btrace$$level", INT_DESC); + if (itv.getA() <= 0) { + if (itv.getB() != Integer.MAX_VALUE) { + ldc(itv.getB()); + jump(IF_ICMPGT, jmp); + } + } else if (itv.getA() < itv.getB()) { + if (itv.getB() == Integer.MAX_VALUE) { + ldc(itv.getA()); + jump(IF_ICMPLT, jmp); + } else { + ldc(itv.getA()); + jump(IF_ICMPLT, jmp); + getStatic(clsName, "$btrace$$level", INT_DESC); + ldc(itv.getB()); + jump(IF_ICMPGT, jmp); + } + } + return this; + } + + /** + * Compares the instrumentation level interval against the runtime value. + * + *

If the runtime value is fitting the level interval there will be 0 on stack upon return from + * this method. Otherwise there will be -1. + * + * @param clsName The probe class name + * @param level The probe instrumentation level + * @return itself + */ + public Assembler compareLevel(String clsName, Level level) { + Interval itv = level.getValue(); + if (itv.getA() <= 0) { + if (itv.getB() != Integer.MAX_VALUE) { + ldc(itv.getB()); + getStatic(clsName, BTRACE_LEVEL_FLD, INT_DESC); + sub(Type.INT_TYPE); + } + } else if (itv.getA() < itv.getB()) { + if (itv.getB() == Integer.MAX_VALUE) { + getStatic(clsName, BTRACE_LEVEL_FLD, INT_DESC); + ldc(itv.getA()); + sub(Type.INT_TYPE); + } else { + Label l1 = new Label(); + Label l2 = new Label(); + ldc(itv.getA()); + jump(IF_ICMPLT, l1); + getStatic(clsName, BTRACE_LEVEL_FLD, INT_DESC); + ldc(itv.getB()); + jump(IF_ICMPGT, l1); + ldc(0); + Label l3 = new Label(); + label(l3); + mHelper.insertFrameSameStack(l3); + jump(GOTO, l2); + label(l1); + mHelper.insertFrameSameStack(l1); + ldc(-1); + label(l2); + mHelper.insertFrameSameStack(l2); + } + } + return this; + } + + public Label openLinkerCheck() { + Label l = new Label(); + invokeStatic(Constants.LINKING_FLAG_INTERNAL, "get", "()I"); + // if the linking flag is 0, then we are not in a reentrant call + jump(IFNE, l); + return l; + } + + public void closeLinkerCheck(Label l) { + label(l); + mHelper.insertFrameSameStack(l); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceBCPClassLoader.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceBCPClassLoader.java new file mode 100644 index 000000000..5f35aaabf --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceBCPClassLoader.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.SharedSettings; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class BTraceBCPClassLoader extends URLClassLoader { + private static final Logger log = LoggerFactory.getLogger(BTraceBCPClassLoader.class); + + BTraceBCPClassLoader(SharedSettings settings) { + super(getBCPUrls(settings), null); + } + + private static URL[] getBCPUrls(SharedSettings settings) { + String bcp = settings.getBootClassPath(); + if (bcp != null && !bcp.isEmpty()) { + List urls = new ArrayList<>(); + for (String cpElement : bcp.split(File.pathSeparator)) { + try { + urls.add(new File(cpElement).toURI().toURL()); + } catch (MalformedURLException e) { + log.debug("Invalid classpath definition: {}", cpElement, e); + } + } + return urls.toArray(new URL[0]); + } + return new URL[0]; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + // delegate class loading to parent directly + ClassLoader parent = getParent(); + if (parent == null) { + parent = ClassLoader.getSystemClassLoader(); + } + return parent.loadClass(name); + } + + @Override + public URL getResource(String name) { + // follow the standard process to load resources + return super.getResource(name); + } +} diff --git a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceClassReader.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceClassReader.java similarity index 77% rename from btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceClassReader.java rename to btrace-agent/src/main/java/io/btrace/instr/BTraceClassReader.java index daa7cbd32..b9bdaf2e4 100644 --- a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceClassReader.java +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceClassReader.java @@ -1,28 +1,20 @@ /* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the Classpath exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package org.openjdk.btrace.instr; +package io.btrace.instr; import java.io.IOException; import java.io.InputStream; @@ -37,6 +29,8 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A hacked version of . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedHashSet; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +/** + * A hacked version of ClassWriter + * allowing to plug-in instrumentation providers and instrument class in single invocation. Also, it + * provides a smart and lightweight common supertype resolution method for computing frames. + * + *

The class is not thread-safe but since there is exactly one instance per instrumented class + * there is no chance of parallel access ever happening. + * + * @author Jaroslav Bachorik + */ +final class BTraceClassWriter extends ClassWriter { + private final Deque instrumentors = new ArrayDeque<>(); + private final ClassLoader targetCL; + private final BTraceClassReader cr; + + BTraceClassWriter(ClassLoader cl, int flags) { + super(flags); + targetCL = cl != null ? cl : ClassLoader.getSystemClassLoader(); + cr = null; + } + + BTraceClassWriter(ClassLoader cl, BTraceClassReader reader, int flags) { + super(reader, flags); + targetCL = cl != null ? cl : ClassLoader.getSystemClassLoader(); + cr = reader; + } + + public void addInstrumentor(BTraceProbe bp) { + addInstrumentor(bp, null); + } + + public void addInstrumentor(BTraceProbe bp, ClassLoader cl) { + if (cr != null && bp != null) { + Instrumentor top = instrumentors.peekLast(); + ClassVisitor parent = top != null ? top : this; + Instrumentor i = Instrumentor.create(cr, bp, parent, cl); + if (i != null) { + instrumentors.add(i); + } + } + } + + public byte[] instrument() { + boolean hit = false; + if (instrumentors.isEmpty()) return null; + + Instrumentor top = instrumentors.peekLast(); + ClassVisitor cv = top != null ? top : this; + InstrumentUtils.accept(cr, cv); + for (Instrumentor i : instrumentors) { + hit |= i.hasMatch(); + } + return hit ? toByteArray() : null; + } + + @Override + protected String getCommonSuperClass(String type1, String type2) { + // Using type closures resolved via the associate classloader + LinkedHashSet type1Closure = new LinkedHashSet<>(); + LinkedHashSet type2Closure = new LinkedHashSet<>(); + InstrumentUtils.collectHierarchyClosure(targetCL, type1, type1Closure, true); + InstrumentUtils.collectHierarchyClosure(targetCL, type2, type2Closure, true); + // basically, do intersection + type1Closure.retainAll(type2Closure); + + // if the intersection is not empty the first element is the closest common ancestor + Iterator iter = type1Closure.iterator(); + if (iter.hasNext()) { + return iter.next(); + } + return Constants.OBJECT_INTERNAL; + } +} diff --git a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceMethodNode.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceMethodNode.java similarity index 85% rename from btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceMethodNode.java rename to btrace-agent/src/main/java/io/btrace/instr/BTraceMethodNode.java index 3e42bd1b0..6643cb207 100644 --- a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceMethodNode.java +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceMethodNode.java @@ -1,47 +1,39 @@ /* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package org.openjdk.btrace.instr; +package io.btrace.instr; +import io.btrace.core.annotations.Kind; +import io.btrace.core.annotations.Sampled; +import io.btrace.core.annotations.Where; import java.util.Comparator; import java.util.Set; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.MethodNode; -import org.openjdk.btrace.core.DebugSupport; -import org.openjdk.btrace.core.annotations.Kind; -import org.openjdk.btrace.core.annotations.Sampled; -import org.openjdk.btrace.core.annotations.Where; -/** @author Jaroslav Bachorik */ +/** + * @author Jaroslav Bachorik + */ public class BTraceMethodNode extends MethodNode { public static final Comparator COMPARATOR = (o1, o2) -> (o1.name + "#" + o1.desc).compareTo(o2.name + "#" + o2.desc); private final BTraceProbeNode cn; private final CallGraph graph; private final String methodId; - private final DebugSupport debug; private OnMethod om; private OnProbe op; private Location loc; @@ -54,7 +46,7 @@ public class BTraceMethodNode extends MethodNode { BTraceMethodNode(MethodNode from, BTraceProbeNode cn, boolean initBTraceHandler) { super( - Opcodes.ASM7, + Opcodes.ASM9, from.access, from.name, from.desc, @@ -63,7 +55,6 @@ public class BTraceMethodNode extends MethodNode { this.cn = cn; graph = cn.getGraph(); methodId = CallGraph.methodId(name, desc); - debug = cn.debug; isBTraceHandler = initBTraceHandler; } @@ -77,7 +68,7 @@ public void visitEnd() { cn.addOnProbe(op); } if (isBTraceHandler) { - graph.addStarting(new CallGraph.Node(methodId)); + graph.addStarting(methodId); } super.visitEnd(); } @@ -86,14 +77,14 @@ public void visitEnd() { public AnnotationVisitor visitAnnotation(String type, boolean visible) { AnnotationVisitor av = super.visitAnnotation(type, visible); - if (type.startsWith("Lorg/openjdk/btrace/core/annotations/")) { + if (type.startsWith("Lio/btrace/core/annotations/")) { isBTraceHandler = true; } if (type.equals(Constants.ONMETHOD_DESC)) { - om = new OnMethod(this, debug); + om = new OnMethod(this); om.setTargetName(name); om.setTargetDescriptor(desc); - return new AnnotationVisitor(Opcodes.ASM7, av) { + return new AnnotationVisitor(Opcodes.ASM9, av) { @Override public void visit(String name, Object value) { super.visit(name, value); @@ -123,7 +114,7 @@ public AnnotationVisitor visitAnnotation(String name, String desc) { AnnotationVisitor av1 = super.visitAnnotation(name, desc); if (desc.equals(Constants.LOCATION_DESC)) { loc = new Location(); - return new AnnotationVisitor(Opcodes.ASM7, av1) { + return new AnnotationVisitor(Opcodes.ASM9, av1) { @Override public void visitEnum(String name, String desc, String value) { super.visitEnum(name, desc, value); @@ -167,7 +158,7 @@ public void visitEnd() { } }; } else if (desc.equals(Constants.LEVEL_DESC)) { - return new AnnotationVisitor(Opcodes.ASM7, av1) { + return new AnnotationVisitor(Opcodes.ASM9, av1) { @Override public void visit(String name, Object value) { super.visit(name, value); @@ -185,7 +176,7 @@ public void visit(String name, Object value) { op = new OnProbe(this); op.setTargetName(name); op.setTargetDescriptor(desc); - return new AnnotationVisitor(Opcodes.ASM7, av) { + return new AnnotationVisitor(Opcodes.ASM9, av) { @Override public void visit(String name, Object value) { super.visit(name, value); @@ -203,7 +194,7 @@ public void visit(String name, Object value) { } else if (type.equals(Constants.SAMPLED_DESC)) { if (om != null) { om.setSamplerKind(Sampled.Sampler.Adaptive); - return new AnnotationVisitor(Opcodes.ASM7, av) { + return new AnnotationVisitor(Opcodes.ASM9, av) { private boolean meanSet = false; @Override @@ -308,6 +299,10 @@ boolean isFieldInjected(String name) { return cn.isFieldInjected(name); } + boolean isServiceType(String typeName) { + return cn.isServiceType(typeName); + } + OnProbe getOnProbe() { return op; } @@ -322,7 +317,7 @@ private AnnotationVisitor setSpecialParameters( } else if (desc.equals(Constants.BTRACE_PROBEMETHODNAME_DESC)) { ph.setMethodParameter(parameter); av = - new AnnotationVisitor(Opcodes.ASM7, av) { + new AnnotationVisitor(Opcodes.ASM9, av) { @Override public void visit(String name, Object val) { if (name.equals("fqn")) { @@ -337,7 +332,7 @@ public void visit(String name, Object val) { ph.setTargetMethodOrFieldParameter(parameter); av = - new AnnotationVisitor(Opcodes.ASM7, av) { + new AnnotationVisitor(Opcodes.ASM9, av) { @Override public void visit(String name, Object val) { if (name.equals("fqn")) { diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceMethodVisitor.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceMethodVisitor.java new file mode 100644 index 000000000..db53ce8e9 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceMethodVisitor.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +public class BTraceMethodVisitor extends MethodVisitor { + private final MethodInstrumentorHelper mHelper; + + public BTraceMethodVisitor(MethodVisitor mv, MethodInstrumentorHelper mHelper) { + super(Opcodes.ASM9, mv); + this.mHelper = mHelper; + } + + public final int storeAsNew() { + return mHelper.storeAsNew(); + } + + public final int storeNewLocal(Type t) { + int index = mHelper.newVar(t); + super.visitVarInsn(t.getOpcode(Opcodes.ISTORE), index); + return index; + } + + public final void addTryCatchHandler(Label start, Label handler) { + mHelper.addTryCatchHandler(start, handler); + } + + public void insertFrameReplaceStack(Label l, Type... stack) { + mHelper.insertFrameReplaceStack(l, stack); + } + + public void insertFrameAppendStack(Label l, Type... stack) { + mHelper.insertFrameAppendStack(l, stack); + } + + public void insertFrameSameStack(Label l) { + mHelper.insertFrameSameStack(l); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceProbe.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbe.java new file mode 100644 index 000000000..bc1720025 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbe.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.extensions.Permission; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import org.objectweb.asm.ClassVisitor; + +public interface BTraceProbe { + /** + * Returns the action method prefix for this probe. This is computed once and cached. + * + *

Format: BTRACE_METHOD_PREFIX + className.replace('/', '$') + "$" Example: + * "$btrace$com$example$MyProbe$" + * + * @return cached action prefix + */ + String getActionPrefix(); + + Collection getApplicableHandlers(BTraceClassReader cr); + + /** + * Returns the {@link OnMethod} handlers applicable to the described class, using raw metadata + * instead of an ASM {@code ClassReader}. Used by backends that cannot construct a {@link + * BTraceClassReader} (e.g. the ClassFile API backend for unsupported class versions). + * + *

The default implementation returns an empty list. Full implementations (e.g. {@link + * BTraceProbeNode} and {@link BTraceProbePersisted}) override this to perform real matching. + */ + default Collection getApplicableHandlers(ClassMeta meta) { + return Collections.emptyList(); + } + + byte[] getFullBytecode(); + + byte[] getDataHolderBytecode(); + + String getClassName(); + + String getClassName(boolean internal); + + boolean isClassRenamed(); + + boolean isTransforming(); + + boolean isVerified(); + + void notifyTransform(String className); + + Iterable onmethods(); + + Iterable onprobes(); + + Class register(BTraceRuntime.Impl rt, BTraceTransformer t); + + /** + * @return the defined probe {@link Class}, or {@code null} if the probe has not been registered + * (or has been unregistered). + */ + Class getProbeClass(); + + void unregister(); + + boolean willInstrument(Class clz); + + void checkVerified(); + + void copyHandlers(ClassVisitor copyingVisitor); + + void applyArgs(ArgsMap argsMap); + + BTraceRuntime.Impl getRuntime(); + + /** + * Returns the set of permissions required by this probe. + * + * @return unmodifiable set of required permissions + */ + Set getRequiredPermissions(); + + /** + * Look up a previously cached {@link MethodHandle} for a handler on this probe. + * + *

The cache is per-probe so it dies naturally when the probe object is collected — no + * cross-probe scan required on unregister. Returns {@code null} on miss, or when the probe + * implementation has no cache (e.g. stub probes in tests). + */ + default MethodHandle getCachedHandler(String handlerName, MethodType type) { + return null; + } + + /** + * Store a resolved handler {@link MethodHandle} for subsequent lookups. Implementations without a + * backing cache (e.g. stub probes in tests) may silently drop the entry. + */ + default void cacheHandler(String handlerName, MethodType type, MethodHandle mh) { + // no-op by default + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeFactory.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeFactory.java new file mode 100644 index 000000000..c3bceda52 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeFactory.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.ArgsMap; +import io.btrace.core.SharedSettings; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for {@link BTraceProbeNode} instances + * + * @author Jaroslav Bachorik + */ +public final class BTraceProbeFactory { + private static final Logger log = LoggerFactory.getLogger(BTraceProbeFactory.class); + + private static final int CLASS_MAGIC = 0xCAFEBABE; + + private final SharedSettings settings; + + public BTraceProbeFactory(SharedSettings settings) { + this.settings = settings; + } + + private static void applyArgs(BTraceProbe bp, ArgsMap argsMap) { + if (bp != null && argsMap != null && !argsMap.isEmpty()) { + bp.applyArgs(argsMap); + } + } + + /** + * Check if a particular file can be loaded as a BTrace probe. Currently only the plain class file + * and BTrace probe pack are supported. + * + * @param filePath the file path + * @return {@literal true} if a BTrace probe can be reconstructed from data in the given file + */ + public static boolean canLoad(String filePath) { + return canLoad(filePath, null); + } + + public static boolean canLoad(String filePath, ClassLoader cl) { + if (filePath == null) { + return false; + } + Path path = Paths.get(filePath); + InputStream is = null; + try { + if (!Files.exists(path)) { + // try to load from the classpath + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + is = cl.getResourceAsStream("META-INF/btrace/" + filePath); + } else { + is = Files.newInputStream(path); + } + if (is != null) { + try { + try (DataInputStream dis = new DataInputStream(is)) { + int magic = dis.readInt(); + return magic == CLASS_MAGIC || magic == BTraceProbePersisted.MAGIC; + } + } catch (IOException ignored) { + is = null; + } + } + } catch (IOException ignored) { + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignored) { + } + } + } + return false; + } + + SharedSettings getSettings() { + return settings; + } + + public BTraceProbe createProbe(byte[] code) { + return createProbe(code, null); + } + + public BTraceProbe createProbe(byte[] code, ArgsMap argsMap) { + BTraceProbe bp = null; + + int mgc = + ((code[0] & 0xff) << 24) + | ((code[1] & 0xff) << 16) + | ((code[2] & 0xff) << 8) + | ((code[3] & 0xff)); + if (mgc == BTraceProbePersisted.MAGIC) { + BTraceProbePersisted bpp = new BTraceProbePersisted(this); + try (DataInputStream dis = + new DataInputStream(new ByteArrayInputStream(Arrays.copyOfRange(code, 4, code.length)))) { + bpp.read(dis); + bp = bpp; + } catch (IOException e) { + log.debug("Failed to read BTrace pack", e); + } + } else { + bp = new BTraceProbeNode(this, code); + } + + applyArgs(bp, argsMap); + return bp; + } + + public BTraceProbe createProbe(InputStream code) { + return createProbe(code, null); + } + + public BTraceProbe createProbe(InputStream code, ArgsMap argsMap) { + BTraceProbe bp = null; + try (DataInputStream dis = new DataInputStream(code)) { + dis.mark(0); + int mgc = dis.readInt(); + if (mgc == BTraceProbePersisted.MAGIC) { + BTraceProbePersisted bpp = new BTraceProbePersisted(this); + bpp.read(dis); + bp = bpp; + } else { + code.reset(); + bp = new BTraceProbeNode(this, code); + } + } catch (IOException e) { + log.debug("Failed to create a probe", e); + } + + applyArgs(bp, argsMap); + return bp; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeNode.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeNode.java new file mode 100644 index 000000000..329b09f99 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeNode.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.DebugSupport; +import io.btrace.core.Messages; +import io.btrace.core.VerifierException; +import io.btrace.core.comm.RetransformClassNotification; +import io.btrace.core.extensions.Permission; +import io.btrace.extension.ServiceDeclarationRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Jaroslav Bachorik + */ +public final class BTraceProbeNode extends ClassNode implements BTraceProbe { + private static final Logger log = LoggerFactory.getLogger(BTraceProbeNode.class); + + final BTraceProbeSupport delegate; + + final BTraceProbeFactory factory; + + final DebugSupport debug; + + private final CallGraph graph; + + private final Map idmap; + private final Set jfrHandlers = new HashSet<>(); + private final Preprocessor prep; + private final BTraceBCPClassLoader bcpResourceClassLoader; + + private volatile BTraceRuntime.Impl rt = null; + + private BTraceTransformer transformer; + private VerifierException verifierException = null; + + private BTraceProbeNode(BTraceProbeFactory factory) { + super(Opcodes.ASM9); + this.factory = factory; + bcpResourceClassLoader = new BTraceBCPClassLoader(factory.getSettings()); + debug = new DebugSupport(factory.getSettings()); + delegate = new BTraceProbeSupport(); + idmap = new HashMap<>(); + graph = new CallGraph(); + prep = new Preprocessor(); + } + + BTraceProbeNode(BTraceProbeFactory factory, byte[] code) { + this(factory); + initialize(code); + } + + BTraceProbeNode(BTraceProbeFactory factory, InputStream code) throws IOException { + this(factory); + initialize(code); + } + + @Override + public boolean isTransforming() { + return delegate.isTransforming(); + } + + @Override + public void visit( + int version, int access, String name, String sig, String superType, String[] itfcs) { + delegate.setClassName(name); + super.visit(version, access, delegate.getClassName(true), sig, superType, itfcs); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String sig, String[] exceptions) { + super.visitMethod(access, name, desc, sig, exceptions); + MethodNode mn = methods.remove(methods.size() - 1); + BTraceMethodNode bmn = new BTraceMethodNode(mn, this, jfrHandlers.contains(name)); + methods.add(bmn); + idmap.put(CallGraph.methodId(name, desc), bmn); + return isTrusted() + ? bmn + : new MethodVerifier( + bmn, access, delegate.getOrigName(), name, desc, bcpResourceClassLoader); + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + return new FieldVisitor(Opcodes.ASM9, super.visitField(access, name, desc, signature, value)) { + @Override + public AnnotationVisitor visitAnnotation(String type, boolean aVisible) { + AnnotationVisitor av = super.visitAnnotation(type, aVisible); + if (type.equals(Constants.INJECTED_DESC)) { + // Bytecode-time validation for @Injected fields: + // This ASM-based check enforces that the injected service type is declared by + // some available extension (as exposed via ServiceDeclarationRegistry). It does + // not load classes; instead, the agent registers a resolver that knows about + // extension manifests. This complements the runtime validation in + // io.btrace.agent.Client#validateDeclaredServices, which uses reflection + // to account for classloader identity, JPMS access, and linkage in the actual + // target JVM. + String internal = Type.getType(desc).getInternalName(); + String fqcn = internal.replace('/', '.'); + if (!ServiceDeclarationRegistry.isDeclaredService(fqcn)) { + throw new VerifierException(Messages.get("invalid.injected.service") + ": " + fqcn); + } + delegate.addServiceField(name, internal); + } + if (type.equals("Lio/btrace/core/annotations/Event;")) { + av = + new AnnotationVisitor(Opcodes.ASM8, av) { + @Override + public void visit(String name, Object value) { + if (name.equals("handler") && value instanceof String) { + jfrHandlers.add((String) value); + } + super.visit(name, value); + } + }; + } + return av; + } + }; + } + + @Override + public Collection getApplicableHandlers(BTraceClassReader cr) { + return delegate.getApplicableHandlers(cr); + } + + @Override + public Collection getApplicableHandlers(ClassMeta meta) { + return delegate.getApplicableHandlers(meta); + } + + @Override + public Iterable onmethods() { + return delegate.onmethods(); + } + + public Collection getOnMethods() { + return delegate.getOnMethods(); + } + + @Override + public Iterable onprobes() { + return delegate.onprobes(); + } + + @Override + public String getClassName() { + return getClassName(false); + } + + @Override + public String getClassName(boolean internal) { + return delegate.getClassName(internal); + } + + String translateOwner(String owner) { + return delegate.translateOwner(owner); + } + + @Override + public Class register(BTraceRuntime.Impl rt, BTraceTransformer t) { + byte[] code = getBytecode(true); + if (debug.isDumpClasses()) { + debug.dumpClass(name + "_bcp", code); + } + Class clz = delegate.defineClass(rt, code); + HandlerRepositoryImpl.registerProbe(this); + t.register(this); + transformer = t; + this.rt = rt; + return clz; + } + + @Override + public Class getProbeClass() { + return delegate.getProbeClass(); + } + + @Override + public java.lang.invoke.MethodHandle getCachedHandler( + String handlerName, java.lang.invoke.MethodType type) { + return delegate.getCachedHandler(handlerName, type); + } + + @Override + public void cacheHandler( + String handlerName, java.lang.invoke.MethodType type, java.lang.invoke.MethodHandle mh) { + delegate.cacheHandler(handlerName, type, mh); + } + + @Override + public void unregister() { + HandlerRepositoryImpl.unregisterProbe(this); + if (transformer != null && isTransforming()) { + if (log.isDebugEnabled()) { + log.debug("onExit: removing transformer for {}", getClassName()); + } + transformer.unregister(this); + } + delegate.clearProbeClass(); + rt = null; + } + + @Override + public byte[] getFullBytecode() { + return getBytecode(false); + } + + @Override + public byte[] getDataHolderBytecode() { + return getBytecode(true); + } + + @Override + public BTraceRuntime.Impl getRuntime() { + return rt; + } + + private byte[] getBytecode(boolean onlyBcpMethods) { + ClassWriter cw = InstrumentUtils.newClassWriter(true); + ClassVisitor cv = cw; + if (onlyBcpMethods) { + cv = + new ClassVisitor(Opcodes.ASM9, cw) { + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String sig, String[] exceptions) { + if (name.startsWith("<")) { + // never check constructor and static initializer + return super.visitMethod(access, name, desc, sig, exceptions); + } + BTraceMethodNode bmn = idmap.get(CallGraph.methodId(name, desc)); + if (bmn != null) { + // Include BCP-required methods AND probe handler methods: + // Handlers are invoked via INVOKEDYNAMIC (IndyDispatcher.bootstrap), + // so the handler method body must be present in the bootstrap-CL probe class. + // This applies to @OnMethod handlers (om != null) and @OnProbe handlers + // (op != null — mapped to @OnMethod entries via mapOnProbes()). + boolean isHandler = bmn.getOnMethod() != null || bmn.getOnProbe() != null; + if (bmn.isBcpRequired() || isHandler) { + // Handlers: rewrite descriptor AnyType → Object to match the INDY call site + // type (Instrumentor.invokeBTraceAction replaces AnyType with Object in + // the INDY descriptor for JVM stack compatibility). + String effectiveDesc = + isHandler + ? desc.replace(Constants.ANYTYPE_DESC, Constants.OBJECT_DESC) + : desc; + return super.visitMethod(access, name, effectiveDesc, sig, exceptions); + } + for (BTraceMethodNode c : bmn.getCallers()) { + boolean callerIsHandler = c.getOnMethod() != null || c.getOnProbe() != null; + if (c.isBcpRequired() || callerIsHandler) { + return super.visitMethod(access, name, desc, sig, exceptions); + } + } + return null; + } + return super.visitMethod(access, name, desc, sig, exceptions); + } + }; + } + accept(cv); + return cw.toByteArray(); + } + + /** + * Collects all the methods reachable from this particular method + * + * @param name the method name + * @param desc the method descriptor + * @return the callee reachability closure + */ + Set callees(String name, String desc) { + Set closure = new HashSet<>(); + graph.callees(name, desc, closure); + return fromIdSet(closure); + } + + /** + * Collects all the methods from which this particular method is reachable + * + * @param name the method name + * @param desc the method descriptor + * @return the caller reachability closure + */ + Set callers(String name, String desc) { + Set closure = new HashSet<>(); + graph.callers(name, desc, closure); + return fromIdSet(closure); + } + + @Override + public boolean willInstrument(Class clz) { + return delegate.willInstrument(clz); + } + + @Override + public boolean isClassRenamed() { + return delegate.isClassRenamed(); + } + + @Override + public boolean isVerified() { + return verifierException == null; + } + + @Override + public String getActionPrefix() { + return delegate.getActionPrefix(); + } + + private VerifierException getVerifierException() { + return verifierException; + } + + boolean isFieldInjected(String name) { + return delegate.isFieldInjected(name); + } + + boolean isServiceType(String typeName) { + return delegate.isServiceType(typeName); + } + + void addOnMethod(OnMethod om) { + delegate.addOnMethod(om); + } + + void addOnProbe(OnProbe op) { + delegate.addOnProbe(op); + } + + void addRequiredPermission(Permission permission) { + delegate.addRequiredPermission(permission); + } + + @Override + public Set getRequiredPermissions() { + return delegate.getRequiredPermissions(); + } + + void setTrusted() { + delegate.setTrusted(); + } + + boolean isTrusted() { + return delegate.isTrusted(); + } + + CallGraph getGraph() { + return graph; + } + + @Override + public void notifyTransform(String className) { + if (rt != null && factory.getSettings().isTrackRetransforms()) { + rt.sendCommand(new RetransformClassNotification(className.replace('/', '.'))); + } + } + + @Override + public void checkVerified() { + if (!isVerified()) { + throw getVerifierException(); + } + } + + @Override + public void copyHandlers(ClassVisitor copyingVisitor) { + Set copyNodes = new TreeSet<>(BTraceMethodNode.COMPARATOR); + + for (OnMethod om : onmethods()) { + if (!om.isCalled()) { + continue; + } + + BTraceMethodNode bmn = om.getMethodNode(); + + MethodNode mn = copy(bmn); + + copyNodes.add(mn); + for (BTraceMethodNode c : bmn.getCallees()) { + copyNodes.add(copy(c)); + } + } + copyingVisitor.visit( + Opcodes.V1_7, + Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, + getClassName(true), + null, + "java/lang/Object", + null); + for (MethodNode mn : copyNodes) { + mn.accept(copyingVisitor); + } + copyingVisitor.visitEnd(); + } + + @Override + public void applyArgs(ArgsMap argsMap) { + delegate.applyArgs(argsMap); + } + + /** Maps a list of @OnProbe's to a list @OnMethod's using probe descriptor XML files. */ + private void mapOnProbes() { + ProbeDescriptorLoader pdl = getProbeDescriptorLoader(); + + for (OnProbe op : delegate.onprobes()) { + String ns = op.getNamespace(); + if (log.isDebugEnabled()) { + log.debug("about to load probe descriptor for namespace {}", ns); + } + // load probe descriptor for this namespace + ProbeDescriptor probeDesc = pdl.load(ns); + if (probeDesc == null) { + if (log.isDebugEnabled()) { + log.debug("failed to find probe descriptor for namespace {}", ns); + } + continue; + } + // find particular probe mappings using "local" name + OnProbe foundProbe = probeDesc.findProbe(op.getName()); + if (foundProbe == null) { + if (log.isDebugEnabled()) { + log.debug("no probe mappings for {}", op.getName()); + } + continue; + } + if (log.isDebugEnabled()) { + log.debug("found probe mappings for {}", op.getName()); + } + Collection omColl = foundProbe.getOnMethods(); + for (OnMethod om : omColl) { + // copy the info in a new OnMethod so that + // we can set target method name and descriptor + // Note that the probe descriptor cache is used + // across BTrace sessions. So, we should not update + // cached OnProbes (and their OnMethods). + OnMethod omn = new OnMethod(op.getMethodNode()); + omn.copyFrom(om); + omn.setTargetName(op.getTargetName()); + omn.setTargetDescriptor(op.getTargetDescriptor()); + omn.setClassNameParameter(op.getClassNameParameter()); + omn.setMethodParameter(op.getMethodParameter()); + omn.setDurationParameter(op.getDurationParameter()); + omn.setMethodFqn(op.isMethodFqn()); + omn.setReturnParameter(op.getReturnParameter()); + omn.setSelfParameter(op.getSelfParameter()); + omn.setTargetInstanceParameter(op.getTargetInstanceParameter()); + omn.setTargetMethodOrFieldFqn(op.isTargetMethodOrFieldFqn()); + omn.setTargetMethodOrFieldParameter(op.getTargetMethodOrFieldParameter()); + addOnMethod(omn); + } + } + } + + private ProbeDescriptorLoader getProbeDescriptorLoader() { + String path = factory.getSettings().getProbeDescPath(); + return new ProbeDescriptorLoader(path); + } + + private void initialize(byte[] code) { + ClassReader cr = new ClassReader(code); + if (debug.isDumpClasses()) { + debug.dumpClass(cr.getClassName() + "_orig", code); + } + initialize(cr); + } + + private void initialize(InputStream code) throws IOException { + initialize(readFully(code)); + } + + private void initialize(ClassReader cr) { + try { + Verifier v = new Verifier(this, factory.getSettings().isTrusted()); + log.debug("verifying BTrace class ..."); + cr.accept(v, ClassReader.SKIP_DEBUG); + if (log.isDebugEnabled()) { + String clzName = getClassName(); + log.debug("BTrace class {} verified", clzName); + log.debug("preprocessing BTrace class {} ...", clzName); + } + prep.process(this); + log.debug("... preprocessed"); + try { + Class.forName("javax.xml.bind.JAXBException"); + mapOnProbes(); + } catch (ClassNotFoundException e) { + log.debug("XML bindings are missing. @OnProbe support is disabled."); + } + } catch (VerifierException e) { + verifierException = e; + } finally { + if (debug.isDumpClasses() && name != null) { + debug.dumpClass(name, getBytecode(false)); + } + } + } + + private Set fromIdSet(Set ids) { + Set methods = new HashSet<>(); + for (String id : ids) { + BTraceMethodNode mn = idmap.get(id); + if (mn != null) { + methods.add(mn); + } + } + return methods; + } + + private MethodNode copy(MethodNode n) { + String[] exceptions = n.exceptions != null ? n.exceptions.toArray(new String[0]) : null; + MethodNode mn = new MethodNode(Opcodes.ASM9, n.access, n.name, n.desc, n.signature, exceptions); + n.accept(mn); + mn.access = Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE; + mn.desc = mn.desc.replace(Constants.ANYTYPE_DESC, Constants.OBJECT_DESC); + mn.signature = + mn.signature != null + ? mn.signature.replace(Constants.ANYTYPE_DESC, Constants.OBJECT_DESC) + : null; + mn.name = getActionPrefix() + mn.name; + return mn; + } + + private byte[] readFully(InputStream is) throws IOException { + int bufSize = 512; + int pos = 0; + byte[] finArr = new byte[1024]; + byte[] buff = new byte[bufSize]; + + int read = 0; + while ((read = is.read(buff, 0, bufSize)) > 0) { + int newpos = pos + read; + if (newpos >= finArr.length) { + finArr = Arrays.copyOf(finArr, finArr.length * 2); + } + System.arraycopy(buff, 0, finArr, pos, read); + pos = newpos; + } + return Arrays.copyOfRange(finArr, 0, pos); + } + + @Override + public String toString() { + return "BTraceProbe{" + "delegate=" + delegate + '}'; + } +} diff --git a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceProbePersisted.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbePersisted.java similarity index 79% rename from btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceProbePersisted.java rename to btrace-agent/src/main/java/io/btrace/instr/BTraceProbePersisted.java index d0faaf061..dcf8e99c1 100644 --- a/btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceProbePersisted.java +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbePersisted.java @@ -1,28 +1,59 @@ /* - * Copyright (c) 2017, 2018, Jaroslav Bachorik . + * Copyright (c) 2008, 2024, Jaroslav Bachorik . * All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Copyright owner designates - * this particular file as subject to the "Classpath" exception as provided - * by the owner in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package org.openjdk.btrace.instr; - -import static org.objectweb.asm.Opcodes.*; - +package io.btrace.instr; + +import static org.objectweb.asm.Opcodes.AASTORE; +import static org.objectweb.asm.Opcodes.ACC_ENUM; +import static org.objectweb.asm.Opcodes.ACC_INTERFACE; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED; +import static org.objectweb.asm.Opcodes.ANEWARRAY; +import static org.objectweb.asm.Opcodes.ASM9; +import static org.objectweb.asm.Opcodes.ATHROW; +import static org.objectweb.asm.Opcodes.BASTORE; +import static org.objectweb.asm.Opcodes.CASTORE; +import static org.objectweb.asm.Opcodes.DASTORE; +import static org.objectweb.asm.Opcodes.FASTORE; +import static org.objectweb.asm.Opcodes.IASTORE; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.LASTORE; +import static org.objectweb.asm.Opcodes.MONITORENTER; +import static org.objectweb.asm.Opcodes.MONITOREXIT; +import static org.objectweb.asm.Opcodes.NEW; +import static org.objectweb.asm.Opcodes.NEWARRAY; +import static org.objectweb.asm.Opcodes.PUTFIELD; +import static org.objectweb.asm.Opcodes.PUTSTATIC; +import static org.objectweb.asm.Opcodes.RET; +import static org.objectweb.asm.Opcodes.SASTORE; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.DebugSupport; +import io.btrace.core.VerifierException; +import io.btrace.core.annotations.Kind; +import io.btrace.core.annotations.Sampled; +import io.btrace.core.annotations.Where; +import io.btrace.core.comm.RetransformClassNotification; +import io.btrace.core.extensions.Permission; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -38,19 +69,15 @@ import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.openjdk.btrace.core.ArgsMap; -import org.openjdk.btrace.core.BTraceRuntime; -import org.openjdk.btrace.core.DebugSupport; -import org.openjdk.btrace.core.VerifierException; -import org.openjdk.btrace.core.annotations.Kind; -import org.openjdk.btrace.core.annotations.Sampled; -import org.openjdk.btrace.core.annotations.Where; -import org.openjdk.btrace.core.comm.RetransformClassNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BTraceProbePersisted implements BTraceProbe { + private static final Logger log = LoggerFactory.getLogger(BTraceProbePersisted.class); + static final int MAGIC = 0xbacecaca; - private static final int VERSION = 2; + private static final int VERSION = 3; final BTraceProbeSupport delegate; private final BTraceProbeFactory factory; @@ -68,8 +95,8 @@ public class BTraceProbePersisted implements BTraceProbe { } private BTraceProbePersisted(BTraceProbeFactory f, BTraceProbeSupport delegate) { - debug = new DebugSupport(f.getSettings()); - this.delegate = delegate != null ? delegate : new BTraceProbeSupport(debug); + this.debug = new DebugSupport(f.getSettings()); + this.delegate = delegate != null ? delegate : new BTraceProbeSupport(); factory = f; preverified = false; } @@ -149,6 +176,11 @@ public void read(DataInputStream dis) throws IOException { read_2(dis); break; } + case 3: + { + read_3(dis); + break; + } default: { throw new IOException("Unsupported version for persisted probe: " + version); @@ -189,6 +221,23 @@ private void read_2(DataInputStream dis) throws IOException { readFullData(dis); } + /** + * Read in the structure for version 3 + * + * @param dis data input stream + * @throws IOException + */ + private void read_3(DataInputStream dis) throws IOException { + delegate.setClassName(dis.readUTF()); + readServices(dis); + readOnMethods(dis); + readOnProbes(dis); + readCallees(dis); + readPermissions(dis); + readDataHolderClass(dis); + readFullData(dis); + } + public void write(DataOutputStream dos) { try { dos.writeInt(MAGIC); @@ -198,10 +247,11 @@ public void write(DataOutputStream dos) { writeOnMethods(dos); writeOnProbes(dos); writeCallees(dos); + writePermissions(dos); writeDataHolderClass(dos); writeFullData(dos); } catch (IOException e) { - debug.debug(e); + log.debug("Failed to write probe {}", getClassName(), e); } } @@ -215,7 +265,7 @@ private void readServices(DataInputStream dis) throws IOException { private void readOnMethods(DataInputStream dis) throws IOException { int num = dis.readInt(); for (int i = 0; i < num; i++) { - OnMethod om = new OnMethod(debug); + OnMethod om = new OnMethod(); om.setClazz(dis.readUTF()); om.setMethod(dis.readUTF()); om.setExactTypeMatch(dis.readBoolean()); @@ -272,6 +322,9 @@ private void readFullData(DataInputStream dis) throws IOException { int fullDataLen = dis.readInt(); fullData = new byte[fullDataLen]; dis.readFully(fullData); + if (fullData.length > 0 && isClassRenamed()) { + fullData = ProbeRenameVisitor.rename(getClassName(), fullData); + } } private void readDataHolderClass(DataInputStream dis) throws IOException { @@ -296,6 +349,18 @@ private void readCallees(DataInputStream dis) throws IOException { } } + private void readPermissions(DataInputStream dis) throws IOException { + int cnt = dis.readInt(); + for (int i = 0; i < cnt; i++) { + String permName = dis.readUTF(); + try { + delegate.addRequiredPermission(Permission.valueOf(permName)); + } catch (IllegalArgumentException e) { + log.warn("Unknown permission in probe: {}", permName); + } + } + } + private void writeServices(DataOutputStream dos) throws IOException { Map svcFields = delegate.serviceFields(); dos.writeInt(svcFields.size()); @@ -404,11 +469,24 @@ private void writeCallees(DataOutputStream dos) throws IOException { } } + private void writePermissions(DataOutputStream dos) throws IOException { + Set perms = delegate.getRequiredPermissions(); + dos.writeInt(perms.size()); + for (Permission perm : perms) { + dos.writeUTF(perm.name()); + } + } + @Override public Collection getApplicableHandlers(BTraceClassReader cr) { return delegate.getApplicableHandlers(cr); } + @Override + public Collection getApplicableHandlers(ClassMeta meta) { + return delegate.getApplicableHandlers(meta); + } + @Override public byte[] getFullBytecode() { return fullData; @@ -449,7 +527,10 @@ public boolean isVerified() { verifyBytecode(); return true; } catch (VerifierException e) { - debug.debug(e); + if (Boolean.getBoolean("btrace.verifier.dump")) { + System.err.println("[BTRACE VERIFY] " + e.getMessage()); + } + log.debug("Class '{}' verification failed", getClassName(), e); } } return false; @@ -458,7 +539,7 @@ public boolean isVerified() { @Override public void notifyTransform(String className) { if (rt != null && factory.getSettings().isTrackRetransforms()) { - rt.send(new RetransformClassNotification(className.replace('/', '.'))); + rt.sendCommand(new RetransformClassNotification(className.replace('/', '.'))); } } @@ -483,20 +564,40 @@ public Class register(BTraceRuntime.Impl rt, BTraceTransformer t) { debug.dumpClass(delegate.getClassName(true) + "_bcp", code); } Class clz = delegate.defineClass(rt, code); + HandlerRepositoryImpl.registerProbe(this); t.register(this); transformer = t; this.rt = rt; return clz; } + @Override + public Class getProbeClass() { + return delegate.getProbeClass(); + } + + @Override + public java.lang.invoke.MethodHandle getCachedHandler( + String handlerName, java.lang.invoke.MethodType type) { + return delegate.getCachedHandler(handlerName, type); + } + + @Override + public void cacheHandler( + String handlerName, java.lang.invoke.MethodType type, java.lang.invoke.MethodHandle mh) { + delegate.cacheHandler(handlerName, type, mh); + } + @Override public void unregister() { + HandlerRepositoryImpl.unregisterProbe(this); if (transformer != null && isTransforming()) { - if (debug.isDebug()) { - debug.debug("onExit: removing transformer for " + getClassName()); + if (log.isDebugEnabled()) { + log.debug("onExit: removing transformer for {}", getClassName()); } transformer.unregister(this); } + delegate.clearProbeClass(); rt = null; } @@ -527,7 +628,7 @@ public void copyHandlers(ClassVisitor copyingVisitor) { } } cr.accept( - new ClassVisitor(ASM7) { + new ClassVisitor(ASM9) { @Override public void visit( int version, @@ -546,7 +647,7 @@ public MethodVisitor visitMethod( if (copiedMethods.contains(mid)) { return copyingVisitor.visitMethod( ACC_PRIVATE | ACC_STATIC, - InstrumentUtils.getActionPrefix(getClassName(true)) + name, + getActionPrefix() + name, desc.replace(Constants.ANYTYPE_DESC, Constants.OBJECT_DESC), signature != null ? signature.replace(Constants.ANYTYPE_DESC, Constants.OBJECT_DESC) @@ -569,15 +670,29 @@ public BTraceRuntime.Impl getRuntime() { return rt; } + @Override + public String getActionPrefix() { + return delegate.getActionPrefix(); + } + + @Override + public Set getRequiredPermissions() { + return delegate.getRequiredPermissions(); + } + private void upgradeBytecode() { - fullData = ProbeUpgradeVisitor_1_2.upgrade(new ClassReader(fullData)); - dataHolder = ProbeUpgradeVisitor_1_2.upgrade(new ClassReader(dataHolder)); + // Upgrade com/sun/btrace → org/openjdk/btrace first, then migrate org/openjdk/btrace → + // io/btrace so that probes compiled against either legacy namespace are fully remapped. + fullData = + ProbePackageMigrator.migrate(ProbeUpgradeVisitor_1_2.upgrade(new ClassReader(fullData))); + dataHolder = + ProbePackageMigrator.migrate(ProbeUpgradeVisitor_1_2.upgrade(new ClassReader(dataHolder))); } private void verifyBytecode() throws VerifierException { ClassReader cr = new ClassReader(fullData); cr.accept( - new ClassVisitor(ASM7) { + new ClassVisitor(ASM9) { private String className; @Override @@ -648,7 +763,7 @@ public MethodVisitor visitMethod( } return new MethodVisitor( - ASM7, super.visitMethod(access, methodName, desc, sig, exceptions)) { + ASM9, super.visitMethod(access, methodName, desc, sig, exceptions)) { private final Map labels = new HashMap<>(); @Override @@ -721,7 +836,7 @@ public void visitMethodInsn( // allow ThreadLocal methods } else if (owner.equals(Constants.BTRACERTACCESS_INTERNAL)) { // allow BTraceRuntimeAccess methods - } else if (owner.equals(Constants.BTRACERTBASE_INTERNAL)) { + } else if (owner.equals(Constants.BTRACERTBRIDGE_INTERNAL)) { // allow BTraceRuntimeImplBase methods } else { if (!delegate.isServiceType(owner)) { @@ -730,7 +845,11 @@ public void visitMethodInsn( } break; case INVOKEINTERFACE: - Verifier.reportError("no.method.calls", owner + "." + name + desc); + // allow BTraceRuntimeBridge interface methods (leave(), enter(), etc.) + if (!owner.equals(Constants.BTRACERTBRIDGE_INTERNAL) + && !delegate.isServiceType(owner)) { + Verifier.reportError("no.method.calls", owner + "." + name + desc); + } break; case INVOKESPECIAL: if (owner.equals(Constants.OBJECT_INTERNAL) @@ -747,7 +866,7 @@ public void visitMethodInsn( } break; case INVOKESTATIC: - if (!owner.startsWith("org/openjdk/btrace/") && !owner.equals(className)) { + if (!owner.startsWith("io/btrace/") && !owner.equals(className)) { if ("valueOf".equals(name) && MethodVerifier.isPrimitiveWrapper(owner)) { // allow primitive wrapper boxing methods. // These calls are generated by javac for autoboxing diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeSupport.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeSupport.java new file mode 100644 index 000000000..f1e81284b --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceProbeSupport.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static io.btrace.instr.ClassFilter.isSubTypeOf; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceRuntime; +import io.btrace.core.extensions.Permission; +import io.btrace.runtime.BTraceRuntimeAccess; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class BTraceProbeSupport { + private static final Logger log = LoggerFactory.getLogger(BTraceProbeSupport.class); + + private volatile String actionPrefix = null; + private static final AtomicReferenceFieldUpdater actionPrefixUpdate = + AtomicReferenceFieldUpdater.newUpdater( + BTraceProbeSupport.class, String.class, "actionPrefix"); + private final List onMethods; + private final List onProbes; + private final Map serviceFields; + private final EnumSet requiredPermissions; + + private final Object filterLock = new Object(); + private volatile ClassFilter filter; + private boolean trustedScript = false; + private boolean classRenamed = false; + private String className, origName; + private volatile Class probeClass; + + /** + * Per-probe handler {@link MethodHandle} cache. Holding it on the probe means it dies with the + * probe object — no cross-probe scan on unregister, and no accidental retention of the probe's + * defined {@code Class} / {@code ClassLoader} via a static map outlasting the probe. + * + *

The key omits the probe name (redundant given the cache is per-probe). Sized small on + * purpose: a typical probe has O(10) handlers. + */ + private final Map handlerCache = new ConcurrentHashMap<>(); + + BTraceProbeSupport() { + onMethods = new ArrayList<>(); + onProbes = new ArrayList<>(); + serviceFields = new HashMap<>(); + requiredPermissions = EnumSet.noneOf(Permission.class); + } + + void setClassName(String name) { + origName = name; + String clientName = BTraceRuntimeAccess.getClientName(name); + className = clientName != null ? clientName : name; + classRenamed = !className.equals(name); + } + + String getClassName(boolean internal) { + return internal ? className : className.replace("/", "."); + } + + String getOrigName() { + return origName; + } + + boolean isClassRenamed() { + return classRenamed; + } + + String translateOwner(String owner) { + if (owner.equals(origName)) { + return getClassName(true); + } + return owner; + } + + boolean isTransforming() { + return !onMethods.isEmpty(); + } + + Collection getApplicableHandlers(BTraceClassReader cr) { + return getApplicableHandlers( + new ClassMeta() { + @Override + public String getJavaClassName() { + return cr.getJavaClassName(); + } + + @Override + public String getInternalName() { + return cr.getClassName(); + } + + @Override + public Collection getAnnotationTypes() { + return cr.getAnnotationTypes(); + } + + @Override + public ClassLoader getClassLoader() { + return cr.getClassLoader(); + } + }); + } + + Collection getApplicableHandlers(ClassMeta meta) { + Collection applicables = new ArrayList<>(onMethods.size()); + String targetName = meta.getJavaClassName(); + + outer: + for (OnMethod om : onMethods) { + String probeClass = om.getClazz(); + if (probeClass == null || probeClass.isEmpty()) continue; + + if (probeClass.equals(targetName)) { + applicables.add(om); + continue; + } + if (om.isClassRegexMatcher() && !om.isClassAnnotationMatcher()) { + Pattern p = om.getClassPattern(); + if (p != null && p.matcher(targetName).matches()) { + applicables.add(om); + continue; + } + } + if (om.isClassAnnotationMatcher()) { + Collection annoTypes = meta.getAnnotationTypes(); + if (om.isClassRegexMatcher()) { + Pattern p = om.getClassPattern(); + if (p != null) { + for (String annoType : annoTypes) { + if (p.matcher(annoType).matches()) { + applicables.add(om); + continue outer; + } + } + } + } else { + if (annoTypes.contains(probeClass)) { + applicables.add(om); + continue; + } + } + } + if (om.isSubtypeMatcher()) { + if (isSubTypeOf(meta.getInternalName(), meta.getClassLoader(), probeClass)) { + applicables.add(om); + } + } + } + return applicables; + } + + Collection getOnMethods() { + return Collections.unmodifiableCollection(onMethods); + } + + Collection getOnProbes() { + return Collections.unmodifiableCollection(onProbes); + } + + Iterable onmethods() { + return () -> Collections.unmodifiableCollection(onMethods).iterator(); + } + + Iterable onprobes() { + return () -> onProbes.iterator(); + } + + Map serviceFields() { + return Collections.unmodifiableMap(serviceFields); + } + + boolean isServiceType(String typeName) { + // First check if it's a direct service type (e.g., MetricsService) + if (serviceFields.containsValue(typeName)) { + return true; + } + // Check if it's in a sub-package of any service type's package + // This allows return types from service methods (e.g., HistogramMetric, HistogramSnapshot) + // without hardcoding specific packages - we infer allowed packages from injected services + for (String serviceType : serviceFields.values()) { + // Extract package by removing the class name (everything after the last '/') + int lastSlash = serviceType.lastIndexOf('/'); + if (lastSlash > 0) { + String servicePackage = serviceType.substring(0, lastSlash); + // Allow classes in the same package or sub-packages of the service + // This makes the verifier cooperate with the extension system automatically + if (typeName.startsWith(servicePackage + "/")) { + return true; + } + } + } + return false; + } + + boolean isFieldInjected(String name) { + return serviceFields.containsKey(name); + } + + void addOnMethod(OnMethod om) { + onMethods.add(om); + } + + void addOnProbe(OnProbe op) { + onProbes.add(op); + } + + void addServiceField(String fldName, String svcType) { + serviceFields.put(fldName, svcType); + } + + void addRequiredPermission(Permission permission) { + requiredPermissions.add(permission); + } + + Set getRequiredPermissions() { + return Collections.unmodifiableSet(requiredPermissions); + } + + boolean willInstrument(Class clz) { + return getClassFilter().isCandidate(clz); + } + + private ClassFilter getClassFilter() { + synchronized (filterLock) { + if (filter == null) { + filter = new ClassFilter(onmethods()); + } + return filter; + } + } + + void setTrusted() { + trustedScript = true; + } + + boolean isTrusted() { + return trustedScript; + } + + Class getProbeClass() { + return probeClass; + } + + void clearProbeClass() { + this.probeClass = null; + // Drop cached MethodHandles — they resolve into the now-released probe Class and would + // otherwise keep it (and its ClassLoader) reachable across a detach. + handlerCache.clear(); + } + + MethodHandle getCachedHandler(String handlerName, MethodType type) { + return handlerCache.get(new HandlerSubKey(handlerName, type)); + } + + void cacheHandler(String handlerName, MethodType type, MethodHandle mh) { + handlerCache.put(new HandlerSubKey(handlerName, type), mh); + } + + /** + * Cache key for the per-probe handler cache. The probe component is implicit (the map lives on + * the probe) so we only carry {@code (handlerName, type)}. Hash is precomputed because the key is + * recomputed on every resolve. + */ + private static final class HandlerSubKey { + final String handler; + final MethodType type; + private final int hash; + + HandlerSubKey(String handler, MethodType type) { + this.handler = handler; + this.type = type; + this.hash = handler.hashCode() * 31 + type.hashCode(); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof HandlerSubKey)) return false; + HandlerSubKey k = (HandlerSubKey) o; + return hash == k.hash && handler.equals(k.handler) && type.equals(k.type); + } + } + + Class defineClass(BTraceRuntime.Impl rt, byte[] code) { + // This extra BTraceRuntime.enter is needed to + // check whether we have already entered before. + boolean enteredHere = BTraceRuntime.enter(); + try { + // The trace class static initializer needs to be run + // without BTraceRuntime.enter(). Please look at the + // static initializer code of trace class. + BTraceRuntime.leave(); + if (log.isDebugEnabled()) { + log.debug("about to defineClass {}", getClassName(false)); + } + Class clz = rt.defineClass(code); + if (log.isDebugEnabled()) { + log.debug("defineClass succeeded for {}", getClassName(false)); + } + this.probeClass = clz; + return clz; + } finally { + // leave BTraceRuntime enter state as it was before + // we started executing this method. + if (!enteredHere) BTraceRuntime.enter(); + } + } + + void applyArgs(ArgsMap argsMap) { + for (OnMethod om : onMethods) { + om.applyArgs(argsMap); + } + } + + String getActionPrefix() { + return actionPrefixUpdate.updateAndGet( + this, + prefix -> { + if (prefix == null) { + prefix = Constants.BTRACE_METHOD_PREFIX + getClassName(true).replace('/', '$') + "$"; + } + return prefix; + }); + } + + @Override + public String toString() { + return "BTraceProbeSupport{" + + "onMethods=" + + onMethods + + ", onProbes=" + + onProbes + + ", trustedScript=" + + trustedScript + + ", serviceFields=" + + serviceFields + + '}'; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BTraceTransformer.java b/btrace-agent/src/main/java/io/btrace/instr/BTraceTransformer.java new file mode 100644 index 000000000..bb20ceb37 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BTraceTransformer.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.BTraceRuntime; +import io.btrace.core.DebugSupport; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The single entry point for class transformation. + * + *

When a class is to be transformed all the registered {@linkplain BTraceProbe} instances are + * asked for the appropriate instrumentation. When there are no registered probes or none of the + * registered probes is able to instrument the class it will not be transformed. + * + * @author Jaroslav Bachorik + * @since 1.3.5 + */ +@SuppressWarnings("RedundantThrows") +public final class BTraceTransformer implements ClassFileTransformer { + private static final Logger log = LoggerFactory.getLogger(BTraceTransformer.class); + + private final DebugSupport debug; + private final ReentrantReadWriteLock setupLock = new ReentrantReadWriteLock(); + private final Collection probes = new ArrayList<>(3); + private final Filter filter = new Filter(); + + public BTraceTransformer(DebugSupport d) { + debug = d; + } + + /* + * Certain classes like java.lang.ThreadLocal and it's + * inner classes, java.lang.Object cannot be safely + * instrumented with BTrace. This is because BTrace uses + * ThreadLocal class to check recursive entries due to + * BTrace's own functions. But this leads to infinite recursions + * if BTrace instruments java.lang.ThreadLocal for example. + * For now, we avoid such classes till we find a solution. + */ + private static boolean isSensitiveClass(String name) { + return ClassFilter.isSensitiveClass(name); + } + + // JDK lambda wrapper names from LambdaMetafactory: + // JDK 8: $$Lambda$ (e.g. Main$$Lambda$36) + // JDK 11+: $$Lambda$/0x (hidden-class suffix) + // Both forms are recognised by the "$$Lambda$" infix followed by digits. + static boolean isSyntheticLambda(String internalName) { + if (internalName == null) return false; + int idx = internalName.indexOf("$$Lambda$"); + if (idx < 0) return false; + int digitStart = idx + "$$Lambda$".length(); + if (digitStart >= internalName.length()) return false; + // Next char must be a digit (the Lambda serial number) + return Character.isDigit(internalName.charAt(digitStart)); + } + + public void register(BTraceProbe p) { + try { + setupLock.writeLock().lock(); + probes.add(p); + for (OnMethod om : p.onmethods()) { + filter.add(om); + } + } finally { + setupLock.writeLock().unlock(); + } + } + + public final void unregister(BTraceProbe p) { + try { + setupLock.writeLock().lock(); + probes.remove(p); + for (OnMethod om : p.onmethods()) { + filter.remove(om); + } + } finally { + setupLock.writeLock().unlock(); + } + } + + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) + throws IllegalClassFormatException { + try { + setupLock.readLock().lock(); + + // Skip JVM-synthesized classes that have no binary name (JDK 8 + // Unsafe.defineAnonymousClass host-anonymous classes, JDK 15+ hidden + // classes). These are never a user-intended tracing target: a + // @OnMethod(clazz="") matcher targets classes the user + // authored, not the VM's lambda/LambdaForm scaffolding. Instrumenting + // them also re-enters the invokedynamic machinery that the probe + // dispatch itself uses, leading to unbounded recursion on JDK 8. + if (className == null) { + return null; + } + + if (className.startsWith("io/btrace/")) { + // do not instrument BTrace classes! + return null; + } + + // A special case for patching the Indy linking in order to be able to safely skip + // BTrace probes while linking is still in progress. + if (className.equals("java/lang/invoke/MethodHandleNatives")) { + byte[] transformed = null; + try { + debug.dumpClass(className.replace('.', '/') + "_orig", classfileBuffer); + transformed = LinkerInstrumentor.addGuard(classfileBuffer); + debug.dumpClass(className.replace('.', '/'), transformed); + } catch (Throwable t) { + log.debug("Failed to instrument indy linking", t); + } + return transformed; + } + + // Skip JVM-synthesized classes: reflection accessors (sun/reflect/Generated*, + // jdk/internal/reflect/Generated*) and lambda wrappers (LambdaMetafactory names + // them "$$Lambda$N" on JDK 8 and "$$Lambda$N/0x" on JDK 11+). + // Instrumenting these serves no tracing purpose — they are 1:1 trampolines to a + // target Method or a captured functional method the user can trace directly — + // and it recursively re-enters the invokedynamic/LambdaMetafactory machinery + // that the probe dispatch path relies on. + if (className.startsWith("sun/reflect/Generated") + || className.startsWith("jdk/internal/reflect/Generated") + || isSyntheticLambda(className)) { + return null; + } + + if (probes.isEmpty()) return null; + if ((loader == null || loader.equals(ClassLoader.getSystemClassLoader())) + && isSensitiveClass(className)) { + if (log.isDebugEnabled()) { + log.debug("skipping transform for BTrace class {}", className); // NOI18N + } + return null; + } + + if (filter.matchClass(className) == Filter.Result.FALSE) return null; + + boolean entered = BTraceRuntime.enter(); + try { + if (debug.isDumpClasses()) { + debug.dumpClass(className.replace('.', '/') + "_orig", classfileBuffer); + } + for (BTraceProbe p : probes) { + p.notifyTransform(className); + } + int major = InstrumentUtils.getMajor(classfileBuffer); + InstrumentationBackend backend = BackendSelector.select(major); + byte[] transformed = backend.instrument(loader, classfileBuffer, probes); + if (transformed == null) { + // no instrumentation necessary + if (log.isDebugEnabled()) { + log.debug("skipping class {}", className.replace('/', '.')); + } + return classfileBuffer; + } else { + if (log.isDebugEnabled()) { + log.debug("transformed class {}", className.replace('/', '.')); + } + // Optional: verify transformed class via ASM in tests. + if (Boolean.getBoolean("btrace.verify.transformed") + && !Boolean.TRUE.equals(VerifyGuard.IN_PROGRESS.get())) { + boolean allow; + String filter = System.getProperty("btrace.verify.filter"); + if (filter != null && !filter.isEmpty()) { + allow = className.matches(filter); + } else { + allow = isAppClass(className); + } + if (allow) { + try { + VerifyGuard.IN_PROGRESS.set(Boolean.TRUE); + java.io.StringWriter sw = new java.io.StringWriter(); + org.objectweb.asm.util.CheckClassAdapter.verify( + new org.objectweb.asm.ClassReader(transformed), + false, + new java.io.PrintWriter(sw)); + String report = sw.toString(); + if (!report.isEmpty()) { + log.warn("ASM verification issues for {}:\n{}", className, report); + } else if (log.isDebugEnabled()) { + log.debug("ASM verification OK for {}", className); + } + } catch (Throwable t) { + // Don't break transformation on verifier diagnostics + log.warn("ASM verification failed for {}: {}", className, t.toString()); + } finally { + VerifyGuard.IN_PROGRESS.remove(); + } + } + } + if (debug.isDumpClasses()) { + debug.dumpClass(className.replace('.', '/'), transformed); + } + } + return transformed; + } catch (Throwable th) { + log.warn("Failed to transform class {}", className, th); + throw th; + } finally { + if (entered) { + BTraceRuntime.leave(); + } + } + } finally { + setupLock.readLock().unlock(); + } + } + + // Helper guard and app-class predicate for verifier + static final class VerifyGuard { + static final ThreadLocal IN_PROGRESS = ThreadLocal.withInitial(() -> Boolean.FALSE); + } + + private static boolean isAppClass(String internalName) { + if (internalName == null) return false; + return !(internalName.startsWith("java/") + || internalName.startsWith("javax/") + || internalName.startsWith("jdk/") + || internalName.startsWith("sun/") + || internalName.startsWith("com/sun/") + || internalName.startsWith("org/ietf/") + || internalName.startsWith("org/omg/") + || internalName.startsWith("org/w3c/") + || internalName.startsWith("org/xml/") + || internalName.startsWith("io/btrace/")); + } + + static class Filter { + private final Map nameMap = new HashMap<>(); + private final Map nameRegexMap = new HashMap<>(); + private final Map patternCache = new ConcurrentHashMap<>(); + private boolean isFast = true; + private boolean isRegex = false; + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private static void addToMap(Map map, K name) { + synchronized (map) { + map.merge(name, 1, Integer::sum); + } + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private static void removeFromMap(Map map, K name) { + synchronized (map) { + Integer i = map.get(name); + if (i == null) { + return; + } + int freq = i - 1; + if (freq == 0) { + map.remove(name); + } + } + } + + void add(OnMethod om) { + if (om.isSubtypeMatcher() || om.isClassAnnotationMatcher()) { + isFast = false; + } else { + if (om.isClassRegexMatcher()) { + isRegex = true; + String name = om.getClazz().replace("\\.", "/"); + Pattern pattern = patternCache.computeIfAbsent(name, Pattern::compile); + addToMap(nameRegexMap, pattern); + } else { + String name = om.getClazz().replace('.', '/'); + addToMap(nameMap, name); + } + } + } + + void remove(OnMethod om) { + String name = om.getClazz().replace('.', '/'); + if (!(om.isSubtypeMatcher() || om.isClassAnnotationMatcher())) { + if (om.isClassRegexMatcher()) { + Pattern pattern = patternCache.get(name); + if (pattern != null) { + removeFromMap(nameRegexMap, pattern); + } + } else { + removeFromMap(nameMap, name); + } + } + } + + public Result matchClass(String className) { + if (isFast) { + synchronized (nameMap) { + if (nameMap.containsKey(className)) { + return Result.TRUE; + } + } + if (isRegex) { + synchronized (nameRegexMap) { + for (Pattern p : nameRegexMap.keySet()) { + if (p.matcher(className).matches()) { + return Result.TRUE; + } + } + } + } + return Result.FALSE; + } + return Result.MAYBE; + } + + enum Result { + TRUE, + FALSE, + MAYBE + } + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BackendSelector.java b/btrace-agent/src/main/java/io/btrace/instr/BackendSelector.java new file mode 100644 index 000000000..60e936b97 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BackendSelector.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Chooses an {@link InstrumentationBackend} based on the class file major version. The ASM backend + * handles versions ≤ {@link AsmInstrumentationBackend#MAX_ASM_MAJOR_VERSION}. For higher + * versions the JDK ClassFile API backend is attempted; it is loaded reflectively so there is no + * compile-time dependency on {@code java.lang.classfile} in the main (Java 8-compiled) source set. + */ +final class BackendSelector { + private static final Logger log = LoggerFactory.getLogger(BackendSelector.class); + + private static final InstrumentationBackend ASM = new AsmInstrumentationBackend(); + private static final InstrumentationBackend CLASSFILE_API = loadClassFileApiBackend(); + + private BackendSelector() {} + + private static InstrumentationBackend loadClassFileApiBackend() { + try { + Class cls = + Class.forName( + "io.btrace.instr.ClassFileApiBackend", true, BackendSelector.class.getClassLoader()); + return (InstrumentationBackend) cls.getDeclaredConstructor().newInstance(); + } catch (Throwable t) { + log.debug("ClassFile API backend unavailable (expected on JDK < 24): {}", t.getMessage()); + return null; + } + } + + /** + * Returns the most appropriate backend for the given class file major version. + * + *

Uses the ASM backend for versions ≤ 69 (Java 25). For higher versions uses the ClassFile + * API backend when available, otherwise falls back to ASM. + */ + static InstrumentationBackend select(int classFileMajorVersion) { + if (!ASM.supports(classFileMajorVersion) && CLASSFILE_API != null) { + return CLASSFILE_API; + } + return ASM; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/BailoutException.java b/btrace-agent/src/main/java/io/btrace/instr/BailoutException.java new file mode 100644 index 000000000..82c0ab291 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/BailoutException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +/** + * Dummy, non-stack-collecting runtime exception. It is used for execution control in ClassReader + * instances in order to avoid processing the complete class file when the relevant info is + * available right at the beginning of parsing. + */ +final class BailoutException extends RuntimeException { + /** Shared instance to optimize the cost of throwing */ + static final BailoutException INSTANCE = new BailoutException(); + + private BailoutException() {} + + @Override + public synchronized Throwable fillInStackTrace() { + // we don't need the stack here + return this; + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/CallGraph.java b/btrace-agent/src/main/java/io/btrace/instr/CallGraph.java new file mode 100644 index 000000000..a552cdcc5 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/CallGraph.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * This class allows building an arbitrary graph caller-callee relationship + * + * @author Jaroslav Bachorik + */ +public final class CallGraph { + private static final Pattern MID_SPLIT_PTN = Pattern.compile("::"); + private final Map nodes = new HashMap<>(); // O(1) lookup index + private final Set startingNodes = new HashSet<>(); + + public static String methodId(String name, String desc) { + return name + "::" + desc; + } + + public static String[] method(String methodId) { + if (methodId.contains("::")) { + return MID_SPLIT_PTN.split(methodId); + } + return new String[0]; + } + + public void addEdge(String fromId, String toId) { + // O(1) lookup instead of O(n) + Node fromNode = nodes.computeIfAbsent(fromId, Node::new); + Node toNode = nodes.computeIfAbsent(toId, Node::new); + + fromNode.addEdge(toNode); + } + + public void addStarting(String methodId) { + // O(1) lookup + Node n = nodes.computeIfAbsent(methodId, Node::new); + startingNodes.add(n); + } + + public boolean hasCycle() { + Set looped = findCycles(); + if (looped.isEmpty()) { + return false; + } + + Set checkingSet = new HashSet<>(looped); + + checkingSet.retainAll(startingNodes); + if (!checkingSet.isEmpty()) { + // a starting node is part of the loop + return true; + } + + Deque processingQueue = new ArrayDeque<>(); + for (Node n : startingNodes) { + processingQueue.push(n); + do { + Node current = processingQueue.pop(); + if (looped.contains(current)) { + // there is a path leading from a starting node to the detected loop + return true; + } + for (Edge e : current.outgoing) { + processingQueue.push(e.to); + } + } while (!processingQueue.isEmpty()); + } + return false; + } + + void callees(String name, String desc, Set closure) { + collectOutgoings(methodId(name, desc), closure); + } + + void callers(String name, String desc, Set closure) { + collectIncomings(methodId(name, desc), closure); + } + + private void collectOutgoings(String methodId, Set closure) { + // O(1) lookup instead of O(n) + Node n = nodes.get(methodId); + if (n != null) { + for (Edge e : n.outgoing) { + if (!closure.contains(e.to.id)) { + closure.add(e.to.id); + collectOutgoings(e.to.id, closure); + } + } + } + } + + private void collectIncomings(String methodId, Set closure) { + // O(1) lookup instead of O(n) + Node n = nodes.get(methodId); + if (n != null) { + for (Edge e : n.incoming) { + if (!closure.contains(e.from.id)) { + closure.add(e.from.id); + collectIncomings(e.from.id, closure); + } + } + } + } + + private Set findCycles() { + if (nodes.size() < 2) return Collections.emptySet(); + + Map checkingNodes = new HashMap<>(); + for (Node n : nodes.values()) { + Node newN = checkingNodes.get(n.id); + if (newN == null) { + newN = new Node(n.id); + checkingNodes.put(n.id, newN); + } + for (Edge e : n.incoming) { + Node fromN = checkingNodes.get(e.from.id); + if (fromN == null) { + fromN = new Node(e.from.id); + checkingNodes.put(e.from.id, fromN); + } + Edge ee = new Edge(fromN, newN); + newN.addIncoming(ee); + fromN.addOutgoing(ee); + } + for (Edge e : n.outgoing) { + Node toN = checkingNodes.get(e.to.id); + if (toN == null) { + toN = new Node(e.to.id); + checkingNodes.put(e.to.id, toN); + } + Edge ee = new Edge(newN, toN); + newN.addOutgoing(ee); + toN.addIncoming(ee); + } + } + + Set sortedNodes = new HashSet<>(checkingNodes.values()); + // collect all terminal nodes + Deque terminalNodes = new ArrayDeque<>(); + for (Node node : sortedNodes) { + if ((node.incoming.isEmpty() && !startingNodes.contains(node)) || node.outgoing.isEmpty()) { + terminalNodes.addLast(node); + } + } + + // remove each terminal node from the graph and if the removal creates more terminal nodes + // add them all for processing + while (!terminalNodes.isEmpty()) { + Node n = terminalNodes.removeFirst(); + sortedNodes.remove(n); + for (Edge e : new HashSet<>(n.incoming)) { + e.delete(); + if (e.from.outgoing.isEmpty()) { + terminalNodes.addLast(e.from); + } + } + for (Edge e : new HashSet<>(n.outgoing)) { + e.delete(); + if (e.to.incoming.isEmpty() && !startingNodes.contains(e.to)) { + terminalNodes.addLast(e.to); + } + } + } + return sortedNodes; + } + + public static class Node { + private final String id; + private final Set incoming = new HashSet<>(); + private final Set outgoing = new HashSet<>(); + + public Node(String id) { + this.id = id; + } + + public void addIncoming(Edge e) { + incoming.add(e); + } + + public void addOutgoing(Edge e) { + outgoing.add(e); + } + + public void removeIncoming(Edge e) { + incoming.remove(e); + } + + public void removeOutgoing(Edge e) { + outgoing.remove(e); + } + + public void addEdge(Node to) { + Edge edge = new Edge(this, to); + this.addOutgoing(edge); + to.addIncoming(edge); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Node other = (Node) obj; + return Objects.equals(id, other.id); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 11 * hash + (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Node{id='").append(id).append("'}"); + sb.append("\n"); + sb.append("incomming:\n"); + sb.append("=============================\n"); + for (Edge e : incoming) { + sb.append(e.from.id).append("\n"); + } + sb.append("=============================\n"); + sb.append("outgoing:\n"); + for (Edge e : outgoing) { + sb.append(e.to.id).append("\n"); + } + sb.append("=============================\n"); + + return sb.toString(); + } + } + + public static class Edge { + private final Node from; + private final Node to; + + public Edge(Node from, Node to) { + this.from = from; + this.to = to; + } + + public void delete() { + from.removeOutgoing(this); + to.removeIncoming(this); + } + + @Override + @SuppressWarnings("ReferenceEquality") + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Edge other = (Edge) obj; + if (!Objects.equals(from, other.from)) { + return false; + } + return Objects.equals(to, other.to); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 37 * hash + (from != null ? from.hashCode() : 0); + hash = 37 * hash + (to != null ? to.hashCode() : 0); + return hash; + } + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/CatchInstrumentor.java b/btrace-agent/src/main/java/io/btrace/instr/CatchInstrumentor.java new file mode 100644 index 000000000..12cf62c64 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/CatchInstrumentor.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.HashMap; +import java.util.Map; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +/** + * This visitor helps in inserting code whenever an exception is caught or finally block is reached. + * The code to insert on exception catch or finally may be decided by derived class. By default, + * this class inserts code to print a message. + * + * @author A. Sundararajan + */ +public class CatchInstrumentor extends MethodInstrumentor { + private final Map handlers = new HashMap<>(); + + public CatchInstrumentor( + ClassLoader cl, + MethodVisitor mv, + MethodInstrumentorHelper mHelper, + String parentClz, + String superClz, + int access, + String name, + String desc) { + super(cl, mv, mHelper, parentClz, superClz, access, name, desc); + } + + @Override + public void visitLabel(Label label) { + super.visitLabel(label); + String catchType = handlers.get(label); + if (catchType != null) { + insertFrameReplaceStack(label, Type.getObjectType(catchType)); + onCatch(catchType); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + handlers.put(handler, type); + } + super.visitTryCatchBlock(start, end, handler, type); + } + + protected void onCatch(String type) { + asm.println("catching " + type); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/ClassCache.java b/btrace-agent/src/main/java/io/btrace/instr/ClassCache.java new file mode 100644 index 000000000..5dfbe1b9c --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ClassCache.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.instr.ClassInfo.ClassName; +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.Map; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentMap; +import org.jctools.maps.NonBlockingHashMap; +import org.jctools.maps.NonBlockingIdentityHashMap; + +/** + * A simple class cache holding {@linkplain ClassInfo} instances and being searchable either by + * {@linkplain Class} or a tuple of {@code (className, classLoader)} + * + * @author Jaroslav Bachorik + */ +public final class ClassCache { + private static final class CacheKey { + public final String name; + public final int id; + + private final int hashCode; + + public CacheKey(String name, int id) { + this.name = name; + this.id = id; + this.hashCode = Objects.hash(name, id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return id == cacheKey.id && name.equals(cacheKey.name); + } + + @Override + public int hashCode() { + return hashCode; + } + } + + private static final class ClassLoaderReference extends PhantomReference { + final CacheKey key; + + public ClassLoaderReference(ClassLoader referent, ReferenceQueue q) { + super(referent, q); + this.key = getCacheKey(referent); + } + } + + private final Map loaderRefs = + new NonBlockingIdentityHashMap<>(); + private final ReferenceQueue cleanupQueue = new ReferenceQueue<>(); + private final ConcurrentMap> cacheMap = + new NonBlockingHashMap<>(); + private final ConcurrentMap bootstrapInfos = new NonBlockingHashMap<>(500); + + private final Timer cleanupTimer = new Timer(true); + + public static ClassCache getInstance() { + return Singleton.INSTANCE; + } + + ClassCache(long cleanupPeriod) { + cleanupTimer.schedule( + new TimerTask() { + @Override + public void run() { + ClassLoaderReference ref = null; + while ((ref = (ClassLoaderReference) cleanupQueue.poll()) != null) { + cacheMap.remove(ref.key); + loaderRefs.remove(ref); + } + } + }, + cleanupPeriod, + cleanupPeriod); + } + + public ClassInfo get(Class clz) { + return get(clz.getClassLoader(), clz.getName()); + } + + /** + * Returns a cached {@linkplain ClassInfo} value. If the corresponding value has not been cached + * yet then it is created and put into the cache. + * + * @param cl The associated {@linkplain ClassLoader} + * @param className The Java class name or internal class name + */ + public ClassInfo get(ClassLoader cl, String className) { + return get(cl, new ClassName(className)); + } + + ClassInfo get(ClassLoader cl, ClassName className) { + ConcurrentMap infos = getInfos(cl); + + return infos.computeIfAbsent(className, k -> new ClassInfo(ClassCache.this, cl, k)); + } + + ConcurrentMap getInfos(ClassLoader cl) { + if (cl == null) { + return bootstrapInfos; + } + boolean[] rslt = new boolean[] {false}; + ConcurrentMap infos = + cacheMap.computeIfAbsent( + getCacheKey(cl), + k -> { + rslt[0] = true; + return new NonBlockingHashMap<>(500); + }); + if (rslt[0]) { + ClassLoaderReference ref = new ClassLoaderReference(cl, cleanupQueue); + loaderRefs.put(ref, ref); + } + return infos; + } + + private static CacheKey getCacheKey(ClassLoader cl) { + return new CacheKey(cl.getClass().getName(), System.identityHashCode(cl)); + } + + int getSize() { + return cacheMap.size(); + } + + private static final class Singleton { + private static final ClassCache INSTANCE = new ClassCache(5000); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/ClassFilter.java b/btrace-agent/src/main/java/io/btrace/instr/ClassFilter.java new file mode 100644 index 000000000..bcc25e1b0 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ClassFilter.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.PrefixMap; +import io.btrace.core.annotations.BTrace; +import java.lang.annotation.Annotation; +import java.lang.ref.Reference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + +/** + * This class checks whether a given target class matches at least one probe specified in a BTrace + * class. + * + * @author A. Sundararajan + */ +public class ClassFilter { + private static final Class REFERENCE_CLASS = Reference.class; + private static final PrefixMap SENSITIVE_CLASSES = new PrefixMap(); + // Method-level sensitive filter: internalClassName -> set of method names. + // We filter by name only (not descriptor) because these are specific JDK internal methods + // that don't have overloads. Filtering by name is more conservative - if JDK ever added + // overloads, we'd rather block them all than risk infinite recursion. + private static final Map> SENSITIVE_METHODS = new HashMap<>(); + + static { + ClassReader.class.getClassLoader(); + AnnotationVisitor.class.getClassLoader(); + FieldVisitor.class.getClassLoader(); + MethodVisitor.class.getClassLoader(); + Attribute.class.getClassLoader(); + + SENSITIVE_CLASSES.add("java/lang/Integer"); + SENSITIVE_CLASSES.add("java/lang/Number"); + SENSITIVE_CLASSES.add("java/lang/Object"); + SENSITIVE_CLASSES.add("java/lang/String"); + SENSITIVE_CLASSES.add("java/lang/StringUTF16"); + SENSITIVE_CLASSES.add("java/lang/ThreadLocal"); + SENSITIVE_CLASSES.add("java/lang/ThreadLocal$ThreadLocalMap"); + SENSITIVE_CLASSES.add("java/lang/WeakPairMap"); + SENSITIVE_CLASSES.add("java/lang/WeakPairMap$Pair$Weak"); + SENSITIVE_CLASSES.add("java/lang/instrument/"); + SENSITIVE_CLASSES.add("java/lang/invoke/"); + SENSITIVE_CLASSES.add("java/lang/ref/"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/LockSupport"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/AbstractQueuedSynchronizer"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/AbstractQueuedSynchronizer$ExclusiveNode"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/AbstractQueuedSynchronizer$Node"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/AbstractOwnableSynchronizer"); + SENSITIVE_CLASSES.add("java/util/concurrent/locks/ReentrantLock"); + SENSITIVE_CLASSES.add("java/util/concurrent/ConcurrentHashMap"); + + SENSITIVE_CLASSES.add("jdk/internal/"); + SENSITIVE_CLASSES.add("sun/invoke/"); + SENSITIVE_CLASSES.add("sun/reflect/"); + SENSITIVE_CLASSES.add("io/btrace/"); + + // JDK 25+ added accessor methods for thread-local fields (previously direct field access). + // ThreadLocal.get() calls Thread.threadLocals() which triggers infinite recursion if + // instrumented. + // ThreadLocal.createMap() calls the setter variants when initializing thread-local storage. + addSensitiveMethod("java/lang/Thread", "threadLocals"); + addSensitiveMethod("java/lang/Thread", "setThreadLocals"); + addSensitiveMethod("java/lang/Thread", "inheritableThreadLocals"); + addSensitiveMethod("java/lang/Thread", "setInheritableThreadLocals"); + addSensitiveMethod("java/lang/Thread", "terminatingThreadLocals"); + addSensitiveMethod("java/lang/Thread", "setTerminatingThreadLocals"); + // Thread.interrupted() calls getAndClearInterrupt(); BTrace runtime uses Thread.interrupted() + addSensitiveMethod("java/lang/Thread", "getAndClearInterrupt"); + // Interrupt-related methods used in exception handling and thread coordination + addSensitiveMethod("java/lang/Thread", "setInterrupt"); + addSensitiveMethod("java/lang/Thread", "clearInterrupt"); + // Thread-method exclusions (JDK 25+): + // - dispatchUncaughtException: recursion source — must be excluded to prevent + // probe re-entry during exception dispatch. + // - getUncaughtExceptionHandler: called by dispatchUncaughtException internally. + // Kept in the exclusion list because probes hooking the getter could re-enter + // the dispatcher path even though the getter itself is read-only. + addSensitiveMethod("java/lang/Thread", "dispatchUncaughtException"); + addSensitiveMethod("java/lang/Thread", "getUncaughtExceptionHandler"); + } + + private final List onMethods; + private Set sourceClasses; + private Pattern[] sourceClassPatterns; + private String[] annotationClasses; + private Pattern[] annotationClassPatterns; + // +foo type class pattern in any @OnMethod. + private String[] superTypes; + // same as above but stored in internal name form ('/' instead of '.') + private String[] superTypesInternal; + + public ClassFilter(Iterable onMethods) { + this.onMethods = new ArrayList<>(); + for (OnMethod om : onMethods) { + this.onMethods.add(om); + } + init(); + } + + /* + * return whether given Class is subtype of given type name + * Note that we can not use Class.isAssignableFrom because the other + * type is specified by just name and not by Class object. + */ + public static boolean isSubTypeOf(Class clazz, String typeName) { + if (clazz == null) { + return false; + } else if (clazz.getName().equals(typeName)) { + return true; + } else { + for (Class iface : clazz.getInterfaces()) { + if (isSubTypeOf(iface, typeName)) { + return true; + } + } + return isSubTypeOf(clazz.getSuperclass(), typeName); + } + } + + /** + * Return whether given Class typeA is subtype of any of the given type names. + * + * @param typeA the type to check + * @param loader the classloader for loading the type (my be null) + * @param types any requested supertypes + */ + public static boolean isSubTypeOf(String typeA, ClassLoader loader, String... types) { + if (typeA == null || typeA.equals(Constants.OBJECT_INTERNAL)) { + return false; + } + if (types.length == 0) { + return false; + } + + boolean internal = types[0].contains("/"); + + loader = (loader != null ? loader : ClassLoader.getSystemClassLoader()); + + if (internal) { + typeA = typeA.replace('.', '/'); + } else { + typeA = typeA.replace('/', '.'); + } + + Set typeSet = new HashSet<>(Arrays.asList(types)); + if (typeSet.contains(typeA)) { + return true; + } + ClassInfo ci = ClassCache.getInstance().get(loader, typeA); + Collection sTypesInfo = ci.getSupertypes(false); + if (sTypesInfo != null) { + Collection sTypes = new ArrayList<>(sTypesInfo.size()); + for (ClassInfo sCi : sTypesInfo) { + sTypes.add(internal ? sCi.getClassName() : sCi.getJavaClassName()); + } + sTypes.retainAll(typeSet); + return !sTypes.isEmpty(); + } else { + return false; + } + } + + /* + * Certain classes like java.lang.ThreadLocal and it's + * inner classes, java.lang.Object cannot be safely + * instrumented with BTrace. This is because BTrace uses + * ThreadLocal class to check recursive entries due to + * BTrace's own functions. But this leads to infinite recursions + * if BTrace instruments java.lang.ThreadLocal for example. + * For now, we avoid such classes till we find a solution. + */ + public static boolean isSensitiveClass(String name) { + return SENSITIVE_CLASSES.contains(name); + } + + /** + * Check if a method should be excluded from instrumentation. + * + *

Note: The {@code desc} parameter is accepted for API consistency but currently ignored. + * Filtering is by method name only, which is intentionally conservative - if JDK added overloads + * of sensitive methods, we'd rather block all of them than risk infinite recursion. + * + * @param owner internal class name (e.g., "java/lang/Thread") + * @param name method name + * @param desc method descriptor (currently unused, reserved for future use) + * @return true if the method should not be instrumented + */ + public static boolean isSensitiveMethod(String owner, String name, String desc) { + Set methods = SENSITIVE_METHODS.get(owner); + return methods != null && !methods.isEmpty() && methods.contains(name); + } + + private static void addSensitiveMethod(String owner, String name) { + SENSITIVE_METHODS.computeIfAbsent(owner, k -> new HashSet<>()).add(name); + } + + public boolean isCandidate(Class target) { + if (target.isInterface() || target.isPrimitive() || target.isArray()) { + return false; + } + + if (REFERENCE_CLASS.equals(target)) { + // instrumenting the java.lang.ref.Reference class will lead + // to StackOverflowError in java.lang.ThreadLocal.get() + return false; + } + + try { + // ignore classes annotated with @BTrace - + // We don't want to instrument tracing classes! + if (target.getAnnotation(BTrace.class) != null) { + return false; + } + } catch (Throwable t) { + // thrown from java.lang.Class.initAnnotationsIfNecessary() + // seems to be a case when trying to access non-existing annotations + // on a superclass + // * messed up situation - ignore the class * + return false; + } + + String className = target.getName(); + if (isNameMatching(className)) { + return true; + } + + for (Pattern pat : sourceClassPatterns) { + if (pat.matcher(className).matches()) { + return true; + } + } + + for (String st : superTypes) { + if (isSubTypeOf(target, st)) { + return true; + } + } + + Annotation[] annotations = target.getAnnotations(); + String[] annoTypes = new String[annotations.length]; + for (int i = 0; i < annotations.length; i++) { + annoTypes[i] = annotations[i].annotationType().getName(); + } + + for (String name : annotationClasses) { + for (String annoType : annoTypes) { + if (name.equals(annoType)) { + return true; + } + } + } + + for (Pattern pat : annotationClassPatterns) { + for (String annoType : annoTypes) { + if (pat.matcher(annoType).matches()) { + return true; + } + } + } + + return false; + } + + public boolean isNameMatching(String clzName) { + if (sourceClasses.contains(clzName)) { + return true; + } + + for (Pattern pat : sourceClassPatterns) { + if (pat.matcher(clzName).matches()) { + return true; + } + } + return false; + } + + private void init() { + List strSrcList = new ArrayList<>(); + List patSrcList = new ArrayList<>(); + List superTypesList = new ArrayList<>(); + List superTypesInternalList = new ArrayList<>(); + List strAnoList = new ArrayList<>(); + List patAnoList = new ArrayList<>(); + + for (OnMethod om : onMethods) { + String className = om.getClazz(); + if (className.length() == 0) { + continue; + } + if (om.isClassRegexMatcher()) { + try { + Pattern p = Pattern.compile(className); + if (om.isClassAnnotationMatcher()) { + patAnoList.add(p); + } else { + patSrcList.add(p); + } + } catch (PatternSyntaxException pse) { + System.err.println( + "btrace ERROR: invalid regex pattern - " + + className.substring(1, className.length() - 1)); + } + } else if (om.isClassAnnotationMatcher()) { + strAnoList.add(className); + } else if (om.isSubtypeMatcher()) { + superTypesList.add(className); + superTypesInternalList.add(className.replace('.', '/')); + } else { + strSrcList.add(className); + } + } + + sourceClasses = new HashSet<>(strSrcList.size()); + sourceClasses.addAll(strSrcList); + sourceClassPatterns = new Pattern[patSrcList.size()]; + patSrcList.toArray(sourceClassPatterns); + superTypes = new String[superTypesList.size()]; + superTypesList.toArray(superTypes); + superTypesInternal = new String[superTypesInternalList.size()]; + superTypesInternalList.toArray(superTypesInternal); + annotationClasses = new String[strAnoList.size()]; + strAnoList.toArray(annotationClasses); + annotationClassPatterns = new Pattern[patAnoList.size()]; + patAnoList.toArray(annotationClassPatterns); + } +} diff --git a/btrace-instr/src/main/java/org/openjdk/btrace/instr/ClassInfo.java b/btrace-agent/src/main/java/io/btrace/instr/ClassInfo.java similarity index 80% rename from btrace-instr/src/main/java/org/openjdk/btrace/instr/ClassInfo.java rename to btrace-agent/src/main/java/io/btrace/instr/ClassInfo.java index 80f012e2f..f5349e3d3 100644 --- a/btrace-instr/src/main/java/org/openjdk/btrace/instr/ClassInfo.java +++ b/btrace-agent/src/main/java/io/btrace/instr/ClassInfo.java @@ -1,38 +1,32 @@ /* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the Classpath exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package org.openjdk.btrace.instr; +package io.btrace.instr; +import io.btrace.core.BTraceRuntime; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Method; +import java.lang.invoke.MethodHandle; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; -import org.openjdk.btrace.core.DebugSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Arbitrary class info type allowing access to supertype information also for not-already-loaded @@ -41,8 +35,10 @@ * @author Jaroslav Bachorik */ public final class ClassInfo { + private static final Logger log = LoggerFactory.getLogger(ClassInfo.class); + private static final ClassLoader SYS_CL = ClassLoader.getSystemClassLoader(); - private static volatile Method BSTRP_CHECK_MTD; + private static volatile MethodHandle BSTRP_CHECK_MTD; private final String cLoaderId; private final ClassName classId; @@ -97,7 +93,7 @@ private static ClassLoader inferClassLoader(ClassLoader initiating, ClassName cl } catch (Throwable t) { // some containers can impose additional restrictions on loading resources and error on // unexpected state - DebugSupport.warning(t); + log.warn("Failed to get resource {}", rsrcName, t); } prev = cl; cl = cl.getParent(); @@ -106,31 +102,9 @@ private static ClassLoader inferClassLoader(ClassLoader initiating, ClassName cl } } - private static boolean isBootstrap(String className) { - try { - Method m = getCheckBootstrap(); - if (m != null) { - return m.invoke(SYS_CL, className) != null; - } - } catch (Throwable t) { - DebugSupport.warning(t); - } - return false; - } - - private static Method getCheckBootstrap() { - if (BSTRP_CHECK_MTD != null) { - return BSTRP_CHECK_MTD; - } - Method m = null; - try { - m = ClassLoader.class.getDeclaredMethod("findBootstrapClassOrNull", String.class); - m.setAccessible(true); - } catch (Throwable t) { - DebugSupport.warning(t); - } - BSTRP_CHECK_MTD = m; - return BSTRP_CHECK_MTD; + // package private only for testing purposes + static boolean isBootstrap(String className) { + return BTraceRuntime.isBootstrapClass(className); } /** @@ -212,14 +186,13 @@ private void loadExternalClass(ClassLoader cl, ClassName className) { } isAvailable = true; } catch (IllegalArgumentException | IOException e) { - DebugSupport.warning("Unable to load class: " + className); - DebugSupport.warning(e); + log.warn("Unable to load class: {}", className, e); } } } catch (Throwable t) { // some containers can impose additional restrictions on classloaders throwing exceptions when // not in expected state - DebugSupport.warning(t); + log.warn("Failed to load class {}", className, t); } } diff --git a/btrace-agent/src/main/java/io/btrace/instr/ClassMeta.java b/btrace-agent/src/main/java/io/btrace/instr/ClassMeta.java new file mode 100644 index 000000000..be4f00514 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ClassMeta.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.Collection; + +/** + * Minimal class metadata needed for probe-to-class matching. This interface decouples the matching + * logic in {@link BTraceProbeSupport} from ASM's {@code ClassReader} so that alternative backends + * (e.g. the JDK ClassFile API backend) can perform matching without constructing an ASM object. + */ +interface ClassMeta { + /** Class name in Java (dot-separated) format, e.g. {@code com.example.Foo}. */ + String getJavaClassName(); + + /** Class name in internal (slash-separated) format, e.g. {@code com/example/Foo}. */ + String getInternalName(); + + /** Java class names (dot-separated) of all runtime-visible annotations present on the class. */ + Collection getAnnotationTypes(); + + /** The classloader that loaded this class, used for subtype matching. */ + ClassLoader getClassLoader(); +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/Constants.java b/btrace-agent/src/main/java/io/btrace/instr/Constants.java new file mode 100644 index 000000000..3a047b37e --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/Constants.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.ArgsMap; +import io.btrace.core.BTraceUtils; +import io.btrace.core.annotations.BTrace; +import io.btrace.core.annotations.Duration; +import io.btrace.core.annotations.Injected; +import io.btrace.core.annotations.Kind; +import io.btrace.core.annotations.Level; +import io.btrace.core.annotations.Location; +import io.btrace.core.annotations.OnError; +import io.btrace.core.annotations.OnEvent; +import io.btrace.core.annotations.OnExit; +import io.btrace.core.annotations.OnLowMemory; +import io.btrace.core.annotations.OnMethod; +import io.btrace.core.annotations.OnProbe; +import io.btrace.core.annotations.OnTimer; +import io.btrace.core.annotations.PeriodicEvent; +import io.btrace.core.annotations.ProbeClassName; +import io.btrace.core.annotations.ProbeMethodName; +import io.btrace.core.annotations.Return; +import io.btrace.core.annotations.Sampled; +import io.btrace.core.annotations.Self; +import io.btrace.core.annotations.TargetInstance; +import io.btrace.core.annotations.TargetMethodOrField; +import io.btrace.core.annotations.Where; +import io.btrace.core.extensions.Extension; +import io.btrace.core.jfr.JfrEvent; +import io.btrace.runtime.LinkingFlag; +import java.util.regex.Pattern; +import org.objectweb.asm.Type; + +/** + * Constants shared by few classes. + * + * @author A. Sundararajan + */ +public abstract class Constants { + public static final String BTRACE_METHOD_PREFIX = "$btrace$"; + + public static final String CONSTRUCTOR = ""; + public static final String CLASS_INITIALIZER = ""; + + public static final Type NULL_TYPE = Type.getType("L$$null"); + public static final Type TOP_TYPE = Type.getType("L$$top"); + + public static final Type VOIDREF_TYPE = Type.getType("Ljava/lang/Void;"); + + public static final String OBJECT_INTERNAL = "java/lang/Object"; + public static final String OBJECT_DESC = "L" + OBJECT_INTERNAL + ";"; + public static final Type OBJECT_TYPE = Type.getType(OBJECT_DESC); + + public static final String ANYTYPE_INTERNAL = "io/btrace/core/types/AnyType"; + public static final String ANYTYPE_DESC = "L" + ANYTYPE_INTERNAL + ";"; + public static final Type ANYTYPE_TYPE = Type.getType(ANYTYPE_DESC); + + public static final String CLASS_DESC = "Ljava/lang/Class;"; + public static final Type CLASS_TYPE = Type.getType(CLASS_DESC); + + public static final String STRING_INTERNAL = "java/lang/String"; + public static final String STRING_DESC = "L" + STRING_INTERNAL + ";"; + public static final Type STRING_TYPE = Type.getType(STRING_DESC); + + public static final String STRING_BUILDER_INTERNAL = "java/lang/StringBuilder"; + public static final String STRING_BUILDER_DESC = "L" + STRING_BUILDER_INTERNAL + ";"; + public static final Type STRING_BUILDER_TYPE = Type.getType(STRING_BUILDER_DESC); + + public static final String VOID_DESC = "V"; + public static final String BOOLEAN_DESC = "Z"; + public static final String INT_DESC = "I"; + + public static final String THROWABLE_INTERNAL = "java/lang/Throwable"; + public static final String THROWABLE_DESC = "L" + THROWABLE_INTERNAL + ";"; + public static final Type THROWABLE_TYPE = Type.getType(THROWABLE_DESC); + + public static final String BTRACERTACCESS_INTERNAL = "io/btrace/runtime/BTraceRuntimeAccess"; + public static final String BTRACERTACCESS_DESC = "L" + BTRACERTACCESS_INTERNAL + ";"; + public static final String BTRACERT_INTERNAL = "io/btrace/core/BTraceRuntime"; + public static final String BTRACERT_DESC = "L" + BTRACERT_INTERNAL + ";"; + public static final String BTRACERTIMPL_INTERNAL = "io/btrace/core/BTraceRuntime$Impl"; + public static final String BTRACERTIMPL_DESC = "L" + BTRACERTIMPL_INTERNAL + ";"; + public static final String BTRACERTBRIDGE_INTERNAL = "io/btrace/core/BTraceRuntimeBridge"; + public static final String BTRACERTBRIDGE_DESC = "L" + BTRACERTBRIDGE_INTERNAL + ";"; + public static final Type BTRACERT_TYPE = Type.getType(BTRACERT_DESC); + + public static final String THREAD_LOCAL_INTERNAL = "java/lang/ThreadLocal"; + public static final String THREAD_LOCAL_DESC = "L" + THREAD_LOCAL_INTERNAL + ";"; + public static final Type THREAD_LOCAL_TYPE = Type.getType(ThreadLocal.class); + + public static final Type LINKING_FLAG_TYPE = Type.getType(LinkingFlag.class); + public static final String LINKING_FLAG_INTERNAL = LINKING_FLAG_TYPE.getInternalName(); + + // BTrace specific stuff + public static final String BTRACE_UTILS = Type.getInternalName(BTraceUtils.class); + public static final String BTRACE_DSL = "io/btrace/BTrace"; + public static final String BTRACE_BOOTSTRAP = "io/btrace/runtime/BTraceBootstrap"; + public static final String INDY_DISPATCHER = "io/btrace/runtime/IndyDispatcher"; + + public static final String EXTENSION = Type.getInternalName(Extension.class); + + public static final String EXTENSION_DESC = Type.getDescriptor(Extension.class); + + public static final String BTRACE_DESC = Type.getDescriptor(BTrace.class); + + public static final String ONMETHOD_DESC = Type.getDescriptor(OnMethod.class); + + public static final String JFRPERIODIC_DESC = Type.getDescriptor(PeriodicEvent.class); + + public static final String JFREVENTFACTORY_DESC = Type.getDescriptor(JfrEvent.Factory.class); + + public static final String BTRACE_PROBECLASSNAME_DESC = Type.getDescriptor(ProbeClassName.class); + + public static final String BTRACE_PROBEMETHODNAME_DESC = + Type.getDescriptor(ProbeMethodName.class); + + public static final String ONTIMER_DESC = Type.getDescriptor(OnTimer.class); + public static final String ONEVENT_DESC = Type.getDescriptor(OnEvent.class); + public static final String ONEXIT_DESC = Type.getDescriptor(OnExit.class); + public static final String ONERROR_DESC = Type.getDescriptor(OnError.class); + public static final String ONLOWMEMORY_DESC = Type.getDescriptor(OnLowMemory.class); + + public static final String SAMPLED_DESC = Type.getDescriptor(Sampled.class); + + public static final String SAMPLER_DESC = Type.getDescriptor(Sampled.Sampler.class); + + public static final String ONPROBE_DESC = Type.getDescriptor(OnProbe.class); + + public static final String LOCATION_DESC = Type.getDescriptor(Location.class); + + public static final String LEVEL_DESC = Type.getDescriptor(Level.class); + + public static final String WHERE_DESC = Type.getDescriptor(Where.class); + + public static final String KIND_DESC = Type.getDescriptor(Kind.class); + + public static final String INJECTED_DESC = Type.getDescriptor(Injected.class); + + // RequestPermission annotations removed; permissions are managed via descriptors and manifest. + + public static final String RETURN_DESC = Type.getDescriptor(Return.class); + + public static final String SELF_DESC = Type.getDescriptor(Self.class); + + public static final String TARGETMETHOD_DESC = Type.getDescriptor(TargetMethodOrField.class); + + public static final String TARGETINSTANCE_DESC = Type.getDescriptor(TargetInstance.class); + + public static final String DURATION_DESC = Type.getDescriptor(Duration.class); + + public static final String ARGSMAP_DESC = Type.getDescriptor(ArgsMap.class); + + // class name pattern is specified with this pattern + public static final Pattern REGEX_SPECIFIER = Pattern.compile("/.+/"); + + public static final String JAVA_LANG_THREAD_LOCAL = Type.getInternalName(ThreadLocal.class); + public static final String JAVA_LANG_THREAD_LOCAL_GET = "get"; + public static final String JAVA_LANG_THREAD_LOCAL_GET_DESC = "()Ljava/lang/Object;"; + public static final String JAVA_LANG_THREAD_LOCAL_SET = "set"; + public static final String JAVA_LANG_THREAD_LOCAL_SET_DESC = "(Ljava/lang/Object;)V"; + + public static final String NUMBER_INTERNAL = "java/lang/Number"; + public static final String INTEGER_BOXED_INTERNAL = "java/lang/Integer"; + public static final String INTEGER_BOXED_DESC = "L" + INTEGER_BOXED_INTERNAL + ";"; + public static final String SHORT_BOXED_INTERNAL = "java/lang/Short"; + public static final String SHORT_BOXED_DESC = "L" + SHORT_BOXED_INTERNAL + ";"; + public static final String LONG_BOXED_INTERNAL = "java/lang/Long"; + public static final String LONG_BOXED_DESC = "L" + LONG_BOXED_INTERNAL + ";"; + public static final String FLOAT_BOXED_INTERNAL = "java/lang/Float"; + public static final String FLOAT_BOXED_DESC = "L" + FLOAT_BOXED_INTERNAL + ";"; + public static final String DOUBLE_BOXED_INTERNAL = "java/lang/Double"; + public static final String DOUBLE_BOXED_DESC = "L" + DOUBLE_BOXED_INTERNAL + ";"; + public static final String BYTE_BOXED_INTERNAL = "java/lang/Byte"; + public static final String BYTE_BOXED_DESC = "L" + BYTE_BOXED_INTERNAL + ";"; + public static final String BOOLEAN_BOXED_INTERNAL = "java/lang/Boolean"; + public static final String BOOLEAN_BOXED_DESC = "L" + BOOLEAN_BOXED_INTERNAL + ";"; + public static final String CHARACTER_BOXED_INTERNAL = "java/lang/Character"; + public static final String CHARACTER_BOXED_DESC = "L" + CHARACTER_BOXED_INTERNAL + ";"; + + public static final String BOX_VALUEOF = "valueOf"; + public static final String BOX_BOOLEAN_DESC = "(" + BOOLEAN_DESC + ")" + BOOLEAN_BOXED_DESC; + public static final String BOX_CHARACTER_DESC = "(C)Ljava/lang/Character;"; + public static final String BOX_BYTE_DESC = "(B)Ljava/lang/Byte;"; + public static final String BOX_SHORT_DESC = "(S)Ljava/lang/Short;"; + public static final String BOX_INTEGER_DESC = "(I)Ljava/lang/Integer;"; + public static final String BOX_LONG_DESC = "(J)Ljava/lang/Long;"; + public static final String BOX_FLOAT_DESC = "(F)Ljava/lang/Float;"; + public static final String BOX_DOUBLE_DESC = "(D)Ljava/lang/Double;"; + + public static final String BOOLEAN_VALUE = "booleanValue"; + public static final String CHAR_VALUE = "charValue"; + public static final String BYTE_VALUE = "byteValue"; + public static final String SHORT_VALUE = "shortValue"; + public static final String INT_VALUE = "intValue"; + public static final String LONG_VALUE = "longValue"; + public static final String FLOAT_VALUE = "floatValue"; + public static final String DOUBLE_VALUE = "doubleValue"; + + public static final String BOOLEAN_VALUE_DESC = "()Z"; + public static final String CHAR_VALUE_DESC = "()C"; + public static final String BYTE_VALUE_DESC = "()B"; + public static final String SHORT_VALUE_DESC = "()S"; + public static final String INT_VALUE_DESC = "()I"; + public static final String LONG_VALUE_DESC = "()J"; + public static final String FLOAT_VALUE_DESC = "()F"; + public static final String DOUBLE_VALUE_DESC = "()D"; + public static final String EMBEDDED_BTRACE_SECTION_HEADER = "META-INF/btrace/"; + + public static final String BTRACE_LEVEL_FLD = "$btrace$$level"; +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/ErrorReturnInstrumentor.java b/btrace-agent/src/main/java/io/btrace/instr/ErrorReturnInstrumentor.java new file mode 100644 index 000000000..931baaa51 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/ErrorReturnInstrumentor.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static io.btrace.instr.Constants.THROWABLE_INTERNAL; +import static io.btrace.instr.Constants.THROWABLE_TYPE; +import static org.objectweb.asm.Opcodes.ATHROW; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; + +/** + * This visitor helps in inserting code whenever a method "returns" because of an exception (i.e., + * no exception handler in the method and so it's frame poped). The code to insert on method error + * return may be decided by derived class. By default, this class inserts code to print message to + * say "no handler here". + * + * @author A. Sundararajan + */ +public class ErrorReturnInstrumentor extends MethodReturnInstrumentor { + private final Label start = new Label(); + private final Label end = new Label(); + + public ErrorReturnInstrumentor( + ClassLoader cl, + MethodVisitor mv, + MethodInstrumentorHelper mHelper, + String parentClz, + String superClz, + int access, + String name, + String desc) { + super(cl, mv, mHelper, parentClz, superClz, access, name, desc); + } + + @Override + protected void visitMethodPrologue() { + addTryCatchHandler(start, end); + visitLabel(start); + super.visitMethodPrologue(); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + visitTryCatchBlock(start, end, end, THROWABLE_INTERNAL); + visitLabel(end); + insertFrameReplaceStack(end, THROWABLE_TYPE); + onErrorReturn(); + visitInsn(ATHROW); + super.visitMaxs(maxStack, maxLocals); + } + + @Override + protected void onMethodEntry() {} + + @Override + protected void onMethodReturn(int opcode) {} + + protected void onErrorReturn() { + asm.println("error return from " + getName() + getDescriptor()); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/FieldAccessInstrumentor.java b/btrace-agent/src/main/java/io/btrace/instr/FieldAccessInstrumentor.java new file mode 100644 index 000000000..6b90ddfa7 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/FieldAccessInstrumentor.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static org.objectweb.asm.Opcodes.*; + +import org.objectweb.asm.MethodVisitor; + +/** + * This visitor helps in inserting code whenever an field access is done. The code to insert on + * field access may be decided by derived class. By default, this class inserts code to print the + * field access. + * + * @author A. Sundararajan + */ +public class FieldAccessInstrumentor extends MethodInstrumentor { + protected boolean isStaticAccess = false; + + public FieldAccessInstrumentor( + ClassLoader cl, + MethodVisitor mv, + MethodInstrumentorHelper mHelper, + String parentClz, + String superClz, + int access, + String name, + String desc) { + super(cl, mv, mHelper, parentClz, superClz, access, name, desc); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + boolean get; + // ignore any internal BTrace fields + if (name.contains("$btrace$")) { + super.visitFieldInsn(opcode, owner, name, desc); + return; + } + + get = opcode == GETFIELD || opcode == GETSTATIC; + isStaticAccess = (opcode == GETSTATIC || opcode == PUTSTATIC); + + if (get) { + onBeforeGetField(opcode, owner, name, desc); + } else { + onBeforePutField(opcode, owner, name, desc); + } + super.visitFieldInsn(opcode, owner, name, desc); + if (get) { + onAfterGetField(opcode, owner, name, desc); + } else { + onAfterPutField(opcode, owner, name, desc); + } + } + + protected void onBeforeGetField(int opcode, String owner, String name, String desc) {} + + protected void onAfterGetField(int opcode, String owner, String name, String desc) {} + + protected void onBeforePutField(int opcode, String owner, String name, String desc) {} + + protected void onAfterPutField(int opcode, String owner, String name, String desc) {} +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/HandlerRepositoryImpl.java b/btrace-agent/src/main/java/io/btrace/instr/HandlerRepositoryImpl.java new file mode 100644 index 000000000..e8d860606 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/HandlerRepositoryImpl.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.HandlerRepository; +import io.btrace.core.SharedSettings; +import io.btrace.runtime.BTraceRuntimes; +import io.btrace.runtime.IndyDispatcher; +import io.btrace.runtime.Interval; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of {@link HandlerRepository} that resolves probe handler {@link MethodHandle}s via + * {@link MethodHandles#publicLookup()}.findStatic() on the probe class. + * + *

Probe handler methods stay in the probe class (per-probe ClassLoader on JDK 8/9-14, or a + * hidden class on JDK 15+). No bytecode copying is performed. + * + *

Caching: only successful resolutions are cached; failures are returned as {@code null} + * without poisoning the cache. IndyDispatcher handles transient failure by installing a {@code + * MutableCallSite} trampoline that retries on each invocation, so a negative cache is not needed — + * and would in fact defeat the self-healing behaviour. + */ +public final class HandlerRepositoryImpl { + private static final Logger log = LoggerFactory.getLogger(HandlerRepositoryImpl.class); + + static { + // Wire up to IndyDispatcher (bootstrap CL) via reflection. + // IndyDispatcher.repository is set to a method-reference calling our resolveHandler(). + try { + Class dispatcherClz = Class.forName("io.btrace.runtime.IndyDispatcher"); + HandlerRepository hook = HandlerRepositoryImpl::resolveHandler; + dispatcherClz.getField("repository").set(null, hook); + } catch (Throwable t) { + log.warn("Unable to initialize BTrace IndyDispatcher support", t); + } + } + + /** Maps probe class name (internal form) → live BTraceProbe instance. */ + private static final Map probeMap = new ConcurrentHashMap<>(); + + /** + * Register a probe after its class has been defined in the JVM. Must be called before any + * instrumented call site targeting this probe is invoked. If invocation arrives first, {@link + * io.btrace.runtime.IndyDispatcher} installs a self-relinking trampoline that will pick up the + * probe on its next invocation. + */ + public static void registerProbe(BTraceProbe probe) { + String probeName = probe.getClassName(true); + probeMap.put(probeName, probe); + } + + /** + * Unregister a probe. Also invalidates every live {@link java.lang.invoke.MutableCallSite} + * targeting this probe via {@link IndyDispatcher#invalidateProbe(String)}, swapping their targets + * to a noop so in-flight and subsequent invocations do not enter probe handler bodies after the + * associated {@code BTraceRuntime} state has been torn down. This is the dispatch-level + * equivalent of the older "cushion" approach, which stubbed probe method bodies via bytecode + * redefine on detach — both exist to keep the instrumented application from crashing when a probe + * is undeployed while call sites are still live. + * + *

The handler {@link MethodHandle} cache is per-probe (see {@link BTraceProbe#cacheHandler}) + * so nothing to scan here — the cache dies with the probe object. + */ + public static void unregisterProbe(BTraceProbe probe) { + String probeName = probe.getClassName(true); + probeMap.remove(probeName); + IndyDispatcher.invalidateProbe(probeName); + } + + /** + * Apply level-based guard to a handler MethodHandle. If the handler has a level requirement + * (enableAt annotation), wraps the handler in a guard that checks if the current probe level + * permits execution. This allows the JIT to optimize based on the level and avoids deopt when + * levels change. + * + * @param handler the resolved handler MethodHandle + * @param probe the BTraceProbe instance + * @param simpleHandlerName the unqualified handler method name (e.g. "onMethod") + * @param handlerType the MethodType of the handler + * @return the handler, possibly wrapped with a level guard + */ + private static MethodHandle applyLevelGuard( + MethodHandle handler, BTraceProbe probe, String simpleHandlerName, MethodType handlerType) { + // Find the OnMethod metadata for this handler + Level level = null; + for (OnMethod om : probe.onmethods()) { + if (om.getMethod().equals(simpleHandlerName)) { + level = om.getLevel(); + if (level != null) { + break; + } + } + } + + if (level == null) { + return handler; // No level guard needed + } + + // Compose the handler with a level guard. The guard will: + // 1. Check if current instrumentation level satisfies the requirement + // 2. If yes, invoke the real handler + // 3. If no, invoke a noop that returns the default value for the type + + log.debug("Handler {} requires level check: {}", simpleHandlerName, level.getValue()); + + try { + // Create a test MethodHandle that checks the level requirement. + // It takes the same arguments as the handler but returns boolean. + MethodType testType = handlerType.changeReturnType(boolean.class); + MethodHandle levelTest = createLevelCheckMH(level, testType); + + // Create a noop handler that returns default value for the return type + MethodHandle noop = createNoopMH(handlerType); + + // Compose: if level check passes, invoke handler; otherwise noop + return MethodHandles.guardWithTest(levelTest, handler, noop); + } catch (Throwable e) { + log.warn("Failed to create level guard for handler {}", simpleHandlerName, e); + return handler; // Fall back to unguarded handler on error + } + } + + /** + * Create a MethodHandle that tests if the current instrumentation level satisfies the given level + * requirement. Returns true if level check passes, false otherwise. + */ + private static MethodHandle createLevelCheckMH(Level level, MethodType testType) + throws Throwable { + // Get the Interval requirement (e.g., ">=100" → [100, MAX_VALUE]) + Interval interval = level.getValue(); + + // Create a static helper method that checks: currentLevel >= a && currentLevel <= b + // We need to invoke this helper with the level bounds and call the level getter + // The tricky part: we need access to BTraceRuntime to query the current level, + // but we don't have it as a parameter. We must create a method that can access it. + + // Use a custom MethodHandle that invokes our level-checking logic + MethodHandle levelChecker = createLevelCheckerMH(interval); + + // Drop arguments to match testType (accept same params as handler, return boolean) + return MethodHandles.dropArguments(levelChecker, 0, testType.parameterArray()); + } + + /** + * Helper: create a MethodHandle that queries BTraceRuntime.getInstrumentationLevel() and checks + * if it falls within the given interval. + */ + private static MethodHandle createLevelCheckerMH(Interval interval) throws Throwable { + int minLevel = interval.getA(); + int maxLevel = interval.getB(); + + // Create a MethodHandle that returns true if currentLevel is in [minLevel, maxLevel] + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle checkMH = + lookup.findStatic( + HandlerRepositoryImpl.class, + "checkLevelInRange", + MethodType.methodType(boolean.class, int.class, int.class, int.class)); + + // Bind the interval bounds as arguments at positions 0 and 1 + // After binding: (minLevel=bound, maxLevel=bound, currentLevel=?) -> boolean + MethodHandle bound = MethodHandles.insertArguments(checkMH, 0, minLevel, maxLevel); + // After binding: (currentLevel) -> boolean + + // Compose with a getter that queries the current level + MethodHandle getCurrentLevelMH = + lookup.findStatic( + BTraceRuntimes.class, "getCurrentLevel", MethodType.methodType(int.class)); + // getCurrentLevel: () -> int + + // Fold: getCurrentLevel() returns int, pass result as first arg to bound + // Result: () -> boolean + return MethodHandles.foldArguments(bound, getCurrentLevelMH); + } + + /** Static helper invoked via MethodHandle: check if level is in [min, max] range. */ + @SuppressWarnings("unused") // invoked via MethodHandle + private static boolean checkLevelInRange(int minLevel, int maxLevel, int currentLevel) { + return currentLevel >= minLevel && currentLevel <= maxLevel; + } + + /** Create a noop MethodHandle that returns the default value for the given type. */ + private static MethodHandle createNoopMH(MethodType handlerType) throws Throwable { + Class returnType = handlerType.returnType(); + // Reuse shared default value logic from IndyDispatcher + Object defaultValue = IndyDispatcher.defaultValueFor(returnType); + MethodHandle noop = MethodHandles.constant(returnType, defaultValue); + // Drop arguments so the noop accepts the handler's parameters but ignores them + return MethodHandles.dropArguments(noop, 0, handlerType.parameterArray()); + } + + /** + * Resolve a probe handler MethodHandle. Called from IndyDispatcher.bootstrap() on first execution + * of each instrumented call site, and subsequently from the trampoline on every retry until + * resolution succeeds. + * + * @param probeName internal class name of the probe (e.g. {@code "com/example/MyTrace"}) + * @param handlerName handler method name (probe-prefixed, e.g. {@code "MyTrace$onMethod"}) + * @param handlerType the MethodType of the call site + * @return the resolved MethodHandle, or {@code null} if resolution fails (caller must treat null + * as transient and retry) + */ + public static MethodHandle resolveHandler( + String probeName, String handlerName, MethodType handlerType) { + BTraceProbe probe = probeMap.get(probeName); + if (probe == null) { + // Probe not registered yet. Do not cache — IndyDispatcher's trampoline will retry. + log.debug("[INDY] probe {} not registered, probeMap.size={}", probeName, probeMap.size()); + return null; + } + + MethodHandle cached = probe.getCachedHandler(handlerName, handlerType); + if (cached != null) { + log.debug("[INDY] cached handler {} for {}", cached, handlerName); + return cached; + } + + Class probeClass = probe.getProbeClass(); + if (probeClass == null) { + // defineClass has not populated probeClass yet (race with register()). + // Do not cache — IndyDispatcher's trampoline will retry. + log.debug("[INDY] probeClass null for {}", probeName); + return null; + } + + log.debug("[INDY] resolving {}.{} in {}", probeName, handlerName, probeClass.getClassLoader()); + + try { + // Strip probe-name prefix from handler name (e.g. "MyTrace$onMethod" → "onMethod") + int dollarIdx = handlerName.lastIndexOf('$'); + String simpleHandlerName = + dollarIdx >= 0 ? handlerName.substring(dollarIdx + 1) : handlerName; + + MethodHandle mh; + try { + // Try public lookup first (works for normal, accessible classes). + mh = MethodHandles.publicLookup().findStatic(probeClass, simpleHandlerName, handlerType); + } catch (IllegalAccessException publicLookupFailed) { + // publicLookup has limited access across classloader boundaries. For probes defined + // in isolated/unnamed ClassLoaders (per-probe isolation on JDK 8/9-14, or hidden + // classes on JDK 15+), publicLookup cannot see the handler methods. Fall back to + // a reflection-based approach: get the method via getDeclaredMethod, then convert + // it to a MethodHandle using unreflect. This works on all JDK versions. + log.debug( + "publicLookup denied access to {}.{}, falling back to reflection-based resolution", + probeName, + simpleHandlerName); + try { + // Extract parameter types from the handler MethodType. + Class[] paramTypes = new Class[handlerType.parameterCount()]; + for (int i = 0; i < paramTypes.length; i++) { + paramTypes[i] = handlerType.parameterType(i); + } + java.lang.reflect.Method method = + probeClass.getDeclaredMethod(simpleHandlerName, paramTypes); + method.setAccessible(true); + mh = MethodHandles.lookup().unreflect(method); + } catch (Throwable fallbackEx) { + // Reflection-based approach also failed. Re-throw the original exception from + // publicLookup so it gets logged with proper context in the outer catch. + throw publicLookupFailed; + } + } + + // Apply level guard if this handler has a level check + mh = applyLevelGuard(mh, probe, simpleHandlerName, handlerType); + + probe.cacheHandler(handlerName, handlerType, mh); + + if (SharedSettings.GLOBAL.isDumpClasses()) { + log.debug("BTrace INDY handler resolved: {}.{}", probeName, simpleHandlerName); + } + + return mh; + } catch (Throwable e) { + // Log loudly: unlike transient null-repository or unregistered-probe failures, + // findStatic exceptions usually mean a real problem (signature mismatch, module + // access). Don't cache — the trampoline will retry, but the same failure is likely + // to recur until the probe class/bytecode is fixed. + log.warn("Failed to resolve handler '{}' in probe '{}'", handlerName, probeName, e); + return null; + } + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/InstrPackGenerator.java b/btrace-agent/src/main/java/io/btrace/instr/InstrPackGenerator.java new file mode 100644 index 000000000..93bf472e4 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/InstrPackGenerator.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import io.btrace.core.PackGenerator; +import io.btrace.core.SharedSettings; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public final class InstrPackGenerator implements PackGenerator { + @Override + public byte[] generateProbePack(byte[] classData) throws IOException { + BTraceProbeNode bpn = + (BTraceProbeNode) new BTraceProbeFactory(SharedSettings.GLOBAL).createProbe(classData); + // force bytecode verification before creating the persisted representation + bpn.checkVerified(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(bos)) { + BTraceProbePersisted bpp = BTraceProbePersisted.from(bpn); + if (!bpp.isVerified()) { + throw new Error(); + } + bpp.write(dos); + } + + return bos.toByteArray(); + } +} diff --git a/btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentUtils.java b/btrace-agent/src/main/java/io/btrace/instr/InstrumentUtils.java similarity index 79% rename from btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentUtils.java rename to btrace-agent/src/main/java/io/btrace/instr/InstrumentUtils.java index ead6df88e..bae805786 100644 --- a/btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentUtils.java +++ b/btrace-agent/src/main/java/io/btrace/instr/InstrumentUtils.java @@ -1,33 +1,24 @@ /* - * Copyright (c) 2008, 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). + * https://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +package io.btrace.instr; -package org.openjdk.btrace.instr; - +import static io.btrace.instr.TypeUtils.isAnyType; +import static io.btrace.instr.TypeUtils.isPrimitive; import static org.objectweb.asm.Opcodes.*; -import static org.openjdk.btrace.instr.TypeUtils.isAnyType; -import static org.openjdk.btrace.instr.TypeUtils.isPrimitive; import java.io.IOException; import java.io.InputStream; @@ -100,18 +91,14 @@ public static boolean isAssignable( return true; } - if (TypeUtils.isObjectOrAnyType(left)) { - return true; - } - - if (isPrimitive(left)) { - return left.equals(right); - } - if (TypeUtils.isVoid(left)) { return TypeUtils.isVoid(right); } + if (TypeUtils.isAnyType(left)) { + return true; + } + if (exactTypeCheck) { return false; } @@ -141,7 +128,9 @@ public static boolean isAssignable( */ if (!(isAnyType(args1[i]) && (sort2 == Type.OBJECT || sort2 == Type.ARRAY || isPrimitive(args2[i])))) { - return isAssignable(args1[i], args2[i], cl, exactTypeCheck); + if (!isAssignable(args1[i], args2[i], cl, exactTypeCheck)) { + return false; + } } } } @@ -197,7 +186,7 @@ private static int getMajor(BTraceClassReader cr) { return cr.getClassVersion(); } - private static int getMajor(byte[] code) { + static int getMajor(byte[] code) { // skip 0xCAFEBABE magic and minor version int majorOffset = 4 + 2; return (((code[majorOffset] << 8) & 0xFF00) | ((code[majorOffset + 1]) & 0xFF)); @@ -227,7 +216,7 @@ static BTraceClassWriter newClassWriter(BTraceClassReader cr) { } static BTraceClassWriter newClassWriter(BTraceClassReader reader, int flags) { - BTraceClassWriter cw = null; + BTraceClassWriter cw; cw = reader != null ? new BTraceClassWriter(reader.getClassLoader(), reader, flags) @@ -252,7 +241,7 @@ static BTraceClassReader newClassReader(ClassLoader cl, InputStream is) throws I return new BTraceClassReader(cl, is); } - static final String getActionPrefix(String className) { + static String getActionPrefix(String className) { return Constants.BTRACE_METHOD_PREFIX + className.replace('/', '$') + "$"; } } diff --git a/btrace-agent/src/main/java/io/btrace/instr/InstrumentationBackend.java b/btrace-agent/src/main/java/io/btrace/instr/InstrumentationBackend.java new file mode 100644 index 000000000..5b4bf07c5 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/InstrumentationBackend.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import java.util.Collection; + +/** + * SPI for bytecode instrumentation backends. BTrace ships two implementations: {@link + * AsmInstrumentationBackend} (default, class file versions ≤ 69) and {@code ClassFileApiBackend} + * (JDK 24+, used for versions > 69 that ASM cannot parse). + */ +interface InstrumentationBackend { + + /** Returns {@code true} when this backend can process the given class file major version. */ + boolean supports(int classFileMajorVersion); + + /** + * Instruments {@code classfileBuffer} by applying all applicable probes. + * + * @param loader the classloader loading the target class (may be {@code null}) + * @param classfileBuffer raw class file bytes + * @param probes all currently registered probes + * @return transformed class bytes if at least one probe matched, {@code null} otherwise + */ + byte[] instrument(ClassLoader loader, byte[] classfileBuffer, Collection probes); +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/InstrumentationException.java b/btrace-agent/src/main/java/io/btrace/instr/InstrumentationException.java new file mode 100644 index 000000000..05e60ba62 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/InstrumentationException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +/** + * Thrown when bytecode instrumentation fails due to invalid variable mapping, corrupted state, or + * other instrumentation logic errors. + * + *

This exception indicates a bug in the instrumentation logic and provides detailed debug + * information to help diagnose the issue. When thrown, instrumentation will fail gracefully, + * returning the original unmodified class to prevent crashes in the instrumented application. + * + * @author BTrace Team + * @since 2.3 + */ +public class InstrumentationException extends RuntimeException { + public InstrumentationException(String message) { + super(message); + } + + public InstrumentationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/btrace-agent/src/main/java/io/btrace/instr/InstrumentingMethodVisitor.java b/btrace-agent/src/main/java/io/btrace/instr/InstrumentingMethodVisitor.java new file mode 100644 index 000000000..0f835c3c2 --- /dev/null +++ b/btrace-agent/src/main/java/io/btrace/instr/InstrumentingMethodVisitor.java @@ -0,0 +1,1884 @@ +/* + * Copyright (c) 2008, 2024, Jaroslav Bachorik . + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.btrace.instr; + +import static org.objectweb.asm.Opcodes.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A method visitor providing support for introducing new local variables in bytecode recomputing + * stackmap frames as necessary. It also provides an API for downstream visitors to hint insertion + * of stackmap frames at required locations. + */ +public final class InstrumentingMethodVisitor extends MethodVisitor + implements MethodInstrumentorHelper { + private static final Logger log = LoggerFactory.getLogger(InstrumentingMethodVisitor.class); + + interface FrameDiagnosticListener { + void onFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack); + } + + static final Object TOP_EXT = -2; + private final VariableMapper variableMapper; + private final SimulatedStack stack = new SimulatedStack(); + private final List locals = new ArrayList<>(); + private final Set newLocals = new HashSet<>(3); + private final LocalVarTypes localTypes = new LocalVarTypes(); + private final Set frameOffsets = new HashSet<>(); + private final Map jumpTargetStates = new HashMap<>(); + private final Map> tryCatchHandlerMap = new HashMap<>(); + private final Map trackingContexts = new HashMap<>(); + private final String owner; + private final String desc; + private int argsSize = 0; + private boolean shouldCacheLevelVar = false; + private int localsTailPtr = 0; + private int pc = 0, lastFramePc = Integer.MIN_VALUE; + private final FrameDiagnosticListener frameDiagnosticListener; + + public InstrumentingMethodVisitor( + int access, String owner, String name, String desc, MethodVisitor mv) { + this(access, owner, name, desc, mv, (type, nLocal, local, nStack, stack) -> {}); + } + + InstrumentingMethodVisitor( + int access, + String owner, + String name, + String desc, + MethodVisitor mv, + FrameDiagnosticListener frameDiagnosticListener) { + super(ASM9, mv); + this.owner = owner; + this.desc = desc; + + initLocals((access & ACC_STATIC) == 0, "".equals(name)); + variableMapper = new VariableMapper(argsSize); + this.frameDiagnosticListener = frameDiagnosticListener; + } + + private static Object toSlotType(Type t) { + if (t == null) { + return null; + } + switch (t.getSort()) { + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + { + return INTEGER; + } + case Type.FLOAT: + { + return FLOAT; + } + case Type.LONG: + { + return LONG; + } + case Type.DOUBLE: + { + return DOUBLE; + } + default: + { + return t == Constants.NULL_TYPE + ? NULL + : (t == Constants.TOP_TYPE ? TOP : t.getInternalName()); + } + } + } + + /** + * Normalizes a frame array by replacing BTrace-specific TOP_EXT markers with ASM's TOP constant. + * TOP_EXT is used internally to mark the second slot of long/double values, but ASM expects TOP + * instead. + */ + private static Object[] normalizeFrameArray(Object[] arr) { + if (arr == null || arr.length == 0) { + return arr; + } + Object[] result = new Object[arr.length]; + for (int i = 0; i < arr.length; i++) { + result[i] = arr[i] == TOP_EXT ? TOP : arr[i]; + } + return result; + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (lastFramePc == pc) { + return; + } + lastFramePc = pc; + + switch (type) { + case F_NEW: // fallthrough + case F_FULL: + { + locals.clear(); + this.stack.reset(); + + if (nLocal > 0 && locals != null) { + locals.addAll(Arrays.asList(local).subList(0, nLocal)); + localsTailPtr = nLocal; + } else { + localsTailPtr = 0; + } + + if (nStack > 0 && stack != null) { + for (int i = 0; i < nStack; i++) { + Object e = stack[i]; + this.stack.push(e); + } + } + break; + } + case F_SAME: + { + this.stack.reset(); + break; + } + case F_SAME1: + { + this.stack.reset(); + Object e = stack[0]; + this.stack.push(e); + break; + } + case F_APPEND: + { + this.stack.reset(); + int top = locals.size(); + for (int i = 0; i < nLocal; i++) { + Object e = local[i]; + if (localsTailPtr < top) { + locals.set(localsTailPtr, e); + } else { + locals.add(e); + } + localsTailPtr++; + } + break; + } + case F_CHOP: + { + this.stack.reset(); + for (int i = 0; i < nLocal; i++) { + locals.remove(--localsTailPtr); + } + break; + } + } + + Object[] localsArr = computeFrameLocals(); + localTypes.replaceWith(localsArr); + + int off = 0; + for (int i = 0; i < localsArr.length; i++) { + Object val = localsArr[i]; + if (val == TOP_EXT) { + off++; + continue; + } + if (off > 0) { + localsArr[i - off] = localsArr[i]; + } + } + localsArr = Arrays.copyOf(localsArr, localsArr.length - off); + Object[] tmpStack = this.stack.toArray(true); + + superVisitFrame(F_NEW, localsArr.length, localsArr, tmpStack.length, tmpStack); + } + + void superVisitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + frameDiagnosticListener.onFrame(type, nLocal, local, nStack, stack); + super.visitFrame(type, nLocal, local, nStack, stack); + } + + @Override + public void visitMultiANewArrayInsn(String arrayTypeName, int dims) { + Type arrayType = Type.getObjectType(arrayTypeName); + for (int i = 0; i < dims; i++) { + stack.pop(); + } + stack.push(arrayType.getDescriptor()); + super.visitMultiANewArrayInsn(arrayTypeName, dims); + pc++; + } + + @Override + public void visitLookupSwitchInsn(Label label, int[] ints, Label[] labels) { + stack.pop(); + SavedState state = new SavedState(localTypes, stack, newLocals, SavedState.CONDITIONAL); + jumpTargetStates.put(label, state); + for (Label l : labels) { + jumpTargetStates.put(l, state); + } + super.visitLookupSwitchInsn(label, ints, labels); + pc++; + } + + @Override + public void visitTableSwitchInsn(int i, int i1, Label label, Label... labels) { + stack.pop(); + SavedState state = new SavedState(localTypes, stack, newLocals, SavedState.CONDITIONAL); + jumpTargetStates.put(label, state); + for (Label l : labels) { + jumpTargetStates.put(l, state); + } + super.visitTableSwitchInsn(i, i1, label, labels); + pc++; + } + + @Override + public void visitLdcInsn(Object o) { + Type t = Type.getType(o.getClass()); + switch (t.getInternalName()) { + case "java/lang/Integer": + { + pushToStack(Type.INT_TYPE); + break; + } + case "java/lang/Long": + { + pushToStack(Type.LONG_TYPE); + break; + } + case "java/lang/Byte": + { + pushToStack(Type.BYTE_TYPE); + break; + } + case "java/lang/Short": + { + pushToStack(Type.SHORT_TYPE); + break; + } + case "java/lang/Character": + { + pushToStack(Type.CHAR_TYPE); + break; + } + case "java/lang/Boolean": + { + pushToStack(Type.BOOLEAN_TYPE); + break; + } + case "java/lang/Float": + { + pushToStack(Type.FLOAT_TYPE); + break; + } + case "java/lang/Double": + { + pushToStack(Type.DOUBLE_TYPE); + break; + } + default: + { + pushToStack(t); + } + } + super.visitLdcInsn(o); + pc++; + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + super.visitJumpInsn(opcode, label); + pc++; + switch (opcode) { + case IFEQ: + case IFGE: + case IFGT: + case IFLE: + case IFLT: + case IFNE: + case IFNONNULL: + case IFNULL: + { + stack.pop(); + break; + } + case IF_ACMPEQ: + case IF_ACMPNE: + case IF_ICMPEQ: + case IF_ICMPGE: + case IF_ICMPGT: + case IF_ICMPLE: + case IF_ICMPLT: + case IF_ICMPNE: + { + stack.pop(); + stack.pop(); + break; + } + } + jumpTargetStates.put( + label, + new SavedState( + localTypes, + stack, + newLocals, + opcode == GOTO || opcode == JSR ? SavedState.UNCONDITIONAL : SavedState.CONDITIONAL)); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle handle, Object... bsmArgs) { + Type[] args = Type.getArgumentTypes(desc); + Type ret = Type.getReturnType(desc); + + for (int i = args.length - 1; i >= 0; i--) { + if (!args[i].equals(Type.VOID_TYPE)) { + popFromStack(args[i]); + } + } + super.visitInvokeDynamicInsn(name, desc, handle, bsmArgs); + pc++; + + if (!ret.equals(Type.VOID_TYPE)) { + pushToStack(ret); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itfc) { + Type[] args = Type.getArgumentTypes(desc); + Type ret = Type.getReturnType(desc); + + for (int i = args.length - 1; i >= 0; i--) { + if (!args[i].equals(Type.VOID_TYPE)) { + popFromStack(args[i]); + } + } + + if (opcode != INVOKESTATIC) { + stack.pop(); + } + super.visitMethodInsn(opcode, owner, name, desc, itfc); + pc++; + + if (!ret.equals(Type.VOID_TYPE)) { + pushToStack(ret); + } + if (opcode == INVOKESPECIAL && name.equals("")) { + if (stack.peek() instanceof Label) { + stack.pop(); + pushToStack(Type.getObjectType(owner)); + } else { + Object obj = stack.peek(); + // uninitialized this becomes initialized after call to super + if (!(obj instanceof String)) { + locals.replaceAll(o -> o == UNINITIALIZED_THIS ? this.owner : o); + localTypes.resolveUnitializedThis(this.owner); + } + } + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + Type t = Type.getType(desc); + super.visitFieldInsn(opcode, owner, name, desc); + pc++; + + if (opcode == PUTFIELD || opcode == PUTSTATIC) { + popFromStack(t); + } + if (opcode == GETFIELD || opcode == PUTFIELD) { + stack.pop(); // pop 'this' + } + if (opcode == GETFIELD || opcode == GETSTATIC) { + pushToStack(t); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + super.visitTypeInsn(opcode, type); + pc++; + + switch (opcode) { + case NEW: + { + pushToStack(Type.getObjectType(type)); + break; + } + case ANEWARRAY: + { + stack.pop(); + Type elementType = type.endsWith(";") ? Type.getType(type) : Type.getObjectType(type); + Type arrayType = Type.getType("[" + elementType); + pushToStack(arrayType); + break; + } + case INSTANCEOF: + { + stack.pop(); + pushToStack(Type.BOOLEAN_TYPE); + break; + } + case CHECKCAST: + { + stack.pop(); + pushToStack(Type.getObjectType(type)); + break; + } + } + } + + @Override + public void visitVarInsn(int opcode, int var) { + int size = 1; + + switch (opcode) { + case DLOAD: + case LLOAD: + case DSTORE: + case LSTORE: + { + size++; + break; + } + } + var = variableMapper.remap(var, size); + + boolean isPush = false; + Type opType = null; + switch (opcode) { + case ILOAD: + { + opType = Type.INT_TYPE; + isPush = true; + break; + } + case LLOAD: + { + opType = Type.LONG_TYPE; + isPush = true; + break; + } + case FLOAD: + { + opType = Type.FLOAT_TYPE; + isPush = true; + break; + } + case DLOAD: + { + opType = Type.DOUBLE_TYPE; + isPush = true; + break; + } + case ALOAD: + { + Object o = localTypes.getType(var); + opType = fromSlotType(o); + isPush = true; + break; + } + case ISTORE: + { + opType = Type.INT_TYPE; + break; + } + case LSTORE: + { + opType = Type.LONG_TYPE; + break; + } + case FSTORE: + { + opType = Type.FLOAT_TYPE; + break; + } + case DSTORE: + { + opType = Type.DOUBLE_TYPE; + break; + } + case ASTORE: + { + opType = fromSlotType(stack.peek()); + break; + } + } + + assert opType != null; + + if (isPush) { + pushToStack(opType); + } else { + popFromStack(opType); + localTypes.setType(var, opType); + } + + super.visitVarInsn(opcode, var); + pc++; + } + + @Override + public void visitIntInsn(int opcode, int operand) { + super.visitIntInsn(opcode, operand); + pc++; + + switch (opcode) { + case BIPUSH: + case SIPUSH: + { + stack.push(INTEGER); + break; + } + case NEWARRAY: + { + popFromStack(Type.INT_TYPE); // size + switch (operand) { + case T_BOOLEAN: + { + pushToStack(Type.getObjectType("[Z")); + break; + } + case T_CHAR: + { + pushToStack(Type.getObjectType("[C")); + break; + } + case T_FLOAT: + { + pushToStack(Type.getObjectType("[F")); + break; + } + case T_DOUBLE: + { + pushToStack(Type.getObjectType("[D")); + break; + } + case T_BYTE: + { + pushToStack(Type.getObjectType("[B")); + break; + } + case T_SHORT: + { + pushToStack(Type.getObjectType("[S")); + break; + } + case T_INT: + { + pushToStack(Type.getObjectType("[I")); + break; + } + case T_LONG: + { + pushToStack(Type.getObjectType("[J")); + break; + } + } + break; + } + } + } + + @Override + public void visitInsn(int opcode) { + super.visitInsn(opcode); + pc++; + + switch (opcode) { + case ACONST_NULL: + { + stack.push(NULL); + break; + } + case ICONST_0: + case ICONST_1: + case ICONST_2: + case ICONST_3: + case ICONST_4: + case ICONST_5: + case ICONST_M1: + { + pushToStack(Type.INT_TYPE); + break; + } + case FCONST_0: + case FCONST_1: + case FCONST_2: + { + pushToStack(Type.FLOAT_TYPE); + break; + } + case LCONST_0: + case LCONST_1: + { + pushToStack(Type.LONG_TYPE); + break; + } + case DCONST_0: + case DCONST_1: + { + pushToStack(Type.DOUBLE_TYPE); + break; + } + case AALOAD: + { + stack.pop(); // index + Object target = stack.pop(); + + if (target instanceof String) { + Type t; + String typeStr = (String) target; + if (typeStr.startsWith("[")) { + if (typeStr.contains("/") && !typeStr.endsWith(";")) { + typeStr += ";"; + } + // Type.getElementType() will give the non-array type which we don't want here + // For a multi-dimensional array the element type is the lower dimension array + typeStr = typeStr.substring(1); + t = Type.getType(typeStr); + } else { + t = Type.getObjectType(typeStr); + } + pushToStack(t); + } else if (target == NULL) { + pushToStack(Constants.NULL_TYPE); + } else { + pushToStack(Constants.OBJECT_TYPE); + } + break; + } + case IALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.INT_TYPE); + break; + } + case FALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.FLOAT_TYPE); + break; + } + case BALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.BYTE_TYPE); + break; + } + case CALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.CHAR_TYPE); + break; + } + case SALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.SHORT_TYPE); + break; + } + case LALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.LONG_TYPE); + break; + } + case DALOAD: + { + stack.pop(); + stack.pop(); + + pushToStack(Type.DOUBLE_TYPE); + break; + } + case AASTORE: + case IASTORE: + case FASTORE: + case BASTORE: + case CASTORE: + case SASTORE: + case LASTORE: + case DASTORE: + { + stack.pop(); // val + stack.pop(); // index + stack.pop(); // arrayref + + break; + } + case POP: + { + stack.pop1(); + break; + } + case POP2: + { + stack.pop1(); + stack.pop1(); + break; + } + case DUP: + { + stack.push1(stack.peek()); + break; + } + case DUP_X1: + { + Object x = stack.pop1(); + Object y = stack.pop1(); + stack.push1(x); + stack.push1(y); + stack.push1(x); + break; + } + case DUP_X2: + { + Object x = stack.pop1(); + Object y = stack.pop1(); + Object z = stack.pop1(); + stack.push1(x); + stack.push1(z); + stack.push1(y); + stack.push1(x); + break; + } + case DUP2: + { + Object x = stack.pop1(); + Object y = stack.peek(); + stack.push1(x); + stack.push1(y); + stack.push1(x); + break; + } + case DUP2_X1: + { + Object x2 = stack.pop1(); + Object x1 = stack.pop1(); + Object y = stack.pop1(); + stack.push1(x1); + stack.push1(x2); + stack.push1(y); + stack.push1(x1); + stack.push1(x2); + break; + } + case DUP2_X2: + { + Object x2 = stack.pop1(); + Object x1 = stack.pop1(); + Object y2 = stack.pop1(); + Object y1 = stack.pop1(); + stack.push1(x1); + stack.push1(x2); + stack.push1(y1); + stack.push1(y2); + stack.push1(x1); + stack.push1(x2); + break; + } + case SWAP: + { + Object x = stack.pop1(); + Object y = stack.pop1(); + stack.push1(x); + stack.push1(y); + break; + } + case IADD: + case ISUB: + case IMUL: + case IDIV: + case IREM: + case IAND: + case IOR: + case IXOR: + case ISHR: + case ISHL: + case IUSHR: + { + popFromStack(Type.INT_TYPE); + popFromStack(Type.INT_TYPE); + pushToStack(Type.INT_TYPE); + break; + } + case FADD: + case FSUB: + case FMUL: + case FDIV: + case FREM: + { + popFromStack(Type.FLOAT_TYPE); + popFromStack(Type.FLOAT_TYPE); + pushToStack(Type.FLOAT_TYPE); + break; + } + case LADD: + case LSUB: + case LMUL: + case LDIV: + case LREM: + case LAND: + case LOR: + case LXOR: + { + popFromStack(Type.LONG_TYPE); + popFromStack(Type.LONG_TYPE); + pushToStack(Type.LONG_TYPE); + break; + } + case LSHR: + case LSHL: + case LUSHR: + { + popFromStack(Type.INT_TYPE); + popFromStack(Type.LONG_TYPE); + pushToStack(Type.LONG_TYPE); + break; + } + case DADD: + case DSUB: + case DMUL: + case DDIV: + case DREM: + { + popFromStack(Type.DOUBLE_TYPE); + popFromStack(Type.DOUBLE_TYPE); + pushToStack(Type.DOUBLE_TYPE); + break; + } + case I2L: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.LONG_TYPE); + break; + } + case I2F: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.FLOAT_TYPE); + break; + } + case I2B: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.BYTE_TYPE); + break; + } + case I2C: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.CHAR_TYPE); + break; + } + case I2S: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.SHORT_TYPE); + break; + } + case I2D: + { + popFromStack(Type.INT_TYPE); + pushToStack(Type.DOUBLE_TYPE); + break; + } + case L2I: + { + popFromStack(Type.LONG_TYPE); + pushToStack(Type.INT_TYPE); + break; + } + case L2F: + { + popFromStack(Type.LONG_TYPE); + pushToStack(Type.FLOAT_TYPE); + break; + } + case L2D: + { + popFromStack(Type.LONG_TYPE); + pushToStack(Type.DOUBLE_TYPE); + break; + } + case F2I: + { + popFromStack(Type.FLOAT_TYPE); + pushToStack(Type.INT_TYPE); + break; + } + case F2L: + { + popFromStack(Type.FLOAT_TYPE); + pushToStack(Type.LONG_TYPE); + break; + } + case F2D: + { + popFromStack(Type.FLOAT_TYPE); + pushToStack(Type.DOUBLE_TYPE); + break; + } + case D2I: + { + popFromStack(Type.DOUBLE_TYPE); + pushToStack(Type.INT_TYPE); + break; + } + case D2F: + { + popFromStack(Type.DOUBLE_TYPE); + pushToStack(Type.FLOAT_TYPE); + break; + } + case D2L: + { + popFromStack(Type.DOUBLE_TYPE); + pushToStack(Type.LONG_TYPE); + break; + } + case LCMP: + { + popFromStack(Type.LONG_TYPE); + popFromStack(Type.LONG_TYPE); + + pushToStack(Type.INT_TYPE); + break; + } + case FCMPL: + case FCMPG: + { + popFromStack(Type.FLOAT_TYPE); + popFromStack(Type.FLOAT_TYPE); + + pushToStack(Type.INT_TYPE); + break; + } + case DCMPL: + case DCMPG: + { + popFromStack(Type.DOUBLE_TYPE); + popFromStack(Type.DOUBLE_TYPE); + + pushToStack(Type.INT_TYPE); + break; + } + case IRETURN: + { + popFromStack(Type.INT_TYPE); + break; + } + case LRETURN: + { + popFromStack(Type.LONG_TYPE); + break; + } + case FRETURN: + { + popFromStack(Type.FLOAT_TYPE); + break; + } + case DRETURN: + { + popFromStack(Type.DOUBLE_TYPE); + break; + } + case ARETURN: + { + popFromStack(Type.getReturnType(desc)); + break; + } + case ATHROW: + { + popFromStack(Constants.THROWABLE_TYPE); + break; + } + case ARRAYLENGTH: + { + stack.pop(); + pushToStack(Type.INT_TYPE); + break; + } + case MONITORENTER: + case MONITOREXIT: + { + stack.pop(); + break; + } + } + } + + @Override + public void visitIincInsn(int var, int increment) { + super.visitIincInsn(variableMapper.remap(var, 1), increment); + pc++; + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + try { + int newIndex = variableMapper.map(index, start); + super.visitLocalVariable( + name, desc, signature, start, end, newIndex == Integer.MIN_VALUE ? 0 : newIndex); + } catch (InstrumentationException e) { + throw new InstrumentationException( + String.format( + "Failed to map local variable '%s' (type=%s, index=%d, scope=%s): %s", + name, desc, index, start, e.getMessage()), + e); + } + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + int typeRef, + TypePath typePath, + Label[] start, + Label[] end, + int[] index, + String desc, + boolean visible) { + try { + int[] newIndex = new int[index.length]; + for (int i = 0; i < index.length; i++) { + newIndex[i] = variableMapper.map(index[i]); + } + return super.visitLocalVariableAnnotation( + typeRef, typePath, start, end, newIndex, desc, visible); + } catch (InstrumentationException e) { + throw new InstrumentationException( + String.format( + "Failed to map local variable annotation (type=%s, indices=%s): %s", + desc, Arrays.toString(index), e.getMessage()), + e); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String exception) { + addTryCatchHandler(start, handler); + super.visitTryCatchBlock(start, end, handler, exception); + } + + @Override + public void visitLabel(Label label) { + variableMapper.noteLabel(label); + SavedState ss = jumpTargetStates.get(label); + if (ss != null) { + if (ss.kind != SavedState.CONDITIONAL) { + reset(); + } + localTypes.mergeWith(ss.lvTypes.toArray()); + stack.replaceWith(ss.sStack.toArray()); + if (ss.kind == SavedState.EXCEPTION) { + stack.push(toSlotType(Constants.THROWABLE_TYPE)); + } + for (LocalVarSlot lvs : newLocals) { + if (!ss.newLocals.contains(lvs)) { + lvs.expire(); + } + } + newLocals.clear(); + newLocals.addAll(ss.newLocals); + } + Set