diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 0000000..f4f846a --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,116 @@ +name: Benchmarks + +on: + workflow_dispatch: + inputs: + suite: + description: "Benchmark suite to run" + required: false + default: "all" + type: choice + options: + - all + - wal_benchmarks + - eventbus_benchmarks + - serialization_benchmarks + - system_benchmarks + - process_collector_benchmarks + +# Restrict permissions to minimum required +permissions: + contents: read + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + CI: true + +jobs: + benchmarks: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 + with: + install: true + cache: true + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Restore baseline benchmarks + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: target/criterion + key: criterion-baseline-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock') }} + + - name: Run benchmarks + env: + BENCH_SUITE: ${{ inputs.suite }} + run: | + set -o pipefail + if [ "$BENCH_SUITE" = "all" ]; then + mise x -- cargo bench --package procmond 2>&1 | tee bench-output.txt + else + mise x -- cargo bench --package procmond --bench "$BENCH_SUITE" 2>&1 | tee bench-output.txt + fi + + - name: Check for performance regression + # CI runners have variable performance; use a generous 20% threshold + # to avoid false positives while still catching significant regressions. + run: | + if grep -q "Performance has regressed" bench-output.txt; then + echo "::warning::Performance regression detected in benchmarks" + grep -A2 "Performance has regressed" bench-output.txt + if grep -oP 'change: \+\K[0-9.]+' bench-output.txt | awk '{if ($1 > 20.0) exit 1}'; then + echo "All regressions within 20% threshold (CI runner noise tolerance)" + else + echo "::error::Benchmark regression exceeds 20% threshold" + exit 1 + fi + else + echo "No performance regressions detected" + fi + + - name: Save baseline benchmarks + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + if: github.ref == 'refs/heads/main' + with: + path: target/criterion + key: criterion-baseline-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock') }} + + - name: Upload benchmark results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: benchmark-results + path: bench-output.txt + retention-days: 30 + + load-tests: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 + with: + install: true + cache: true + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run load tests + run: | + set -o pipefail + NO_COLOR=1 TERM=dumb mise x -- cargo test --package procmond --test load_tests -- --ignored --nocapture 2>&1 | tee load-test-output.txt + + - name: Upload load test results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: load-test-results + path: load-test-output.txt + retention-days: 30 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bde1b75..b9e28e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,63 +129,3 @@ jobs: with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} files: target/lcov.info - - benchmarks: - runs-on: ubuntu-latest - needs: test - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 - with: - install: true - cache: true - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Restore baseline benchmarks - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: target/criterion - key: criterion-baseline-${{ runner.os }} - - - name: Run benchmarks - run: mise x -- cargo bench --package procmond 2>&1 | tee bench-output.txt - - - name: Check for performance regression - run: | - # Criterion reports "regressed" when performance degrades beyond noise threshold. - # Fail CI if any benchmark regresses more than 10%. - if grep -q "Performance has regressed" bench-output.txt; then - echo "::warning::Performance regression detected in benchmarks" - grep -A2 "Performance has regressed" bench-output.txt - if grep -oP 'change: \+\K[0-9.]+' bench-output.txt | awk '{if ($1 > 10.0) exit 1}'; then - echo "All regressions within 10% threshold" - else - echo "::error::Benchmark regression exceeds 10% threshold" - exit 1 - fi - else - echo "No performance regressions detected" - fi - - - name: Save baseline benchmarks - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - if: github.ref == 'refs/heads/main' - with: - path: target/criterion - key: criterion-baseline-${{ runner.os }} - - - name: Run load tests - run: NO_COLOR=1 TERM=dumb mise x -- cargo test --package procmond --test load_tests -- --ignored --nocapture 2>&1 | tee load-test-output.txt - - - name: Upload benchmark results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: always() - with: - name: benchmark-results - path: | - bench-output.txt - load-test-output.txt - retention-days: 30 diff --git a/docs/src/contributing.md b/docs/src/contributing.md index 5b4b5e6..ebdde1b 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -125,6 +125,9 @@ just fmt # Run benchmarks just bench +# Run procmond benchmarks +just bench-procmond + # Generate documentation just docs diff --git a/docs/src/testing.md b/docs/src/testing.md index 41a14d6..1525ad5 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -336,17 +336,18 @@ async fn test_full_system_workflow() { ## Performance Testing -### Automated CI Benchmarks +### Automated Benchmarks -DaemonEye's CI pipeline includes automated performance benchmarking to detect regressions: +DaemonEye provides a dedicated benchmarking workflow for performance testing: -- **Automatic Execution**: Performance benchmarks run on every CI build using Criterion -- **Regression Detection**: Tests automatically detect performance regressions with a 10% threshold +- **Manual Trigger**: Performance benchmarks are triggered manually via workflow_dispatch +- **Configurable Suites**: Select which benchmark suite to run ("all", "performance_benchmarks", or "process_collector_benchmarks") +- **Regression Detection**: Tests detect performance regressions and log warnings for review - **Baseline Comparison**: Benchmark results are cached and compared against baseline from the main branch -- **Load Testing**: Automated load tests validate system behavior under stress -- **Results Archival**: Benchmark results are uploaded as artifacts with 30-day retention +- **Load Testing**: Automated load tests validate system behavior under stress in a separate job +- **Results Archival**: Benchmark and load test results are uploaded as artifacts with 30-day retention -Developers can access benchmark results from the GitHub Actions workflow artifacts. If a performance regression exceeds the 10% threshold, the CI build will fail with a detailed error message showing which benchmarks regressed. +Developers can access benchmark results from the GitHub Actions workflow artifacts. Performance regressions are logged as warnings but do not fail the build, allowing for manual review and assessment. ### Load Testing @@ -664,9 +665,13 @@ impl TestDataManager { ## Continuous Integration -### GitHub Actions Workflow +### GitHub Actions Workflows -The CI pipeline includes multiple jobs that run on every build: +DaemonEye uses two separate GitHub Actions workflows for testing: + +#### Main CI Workflow (`.github/workflows/ci.yml`) + +The main CI pipeline runs on every push and pull request: ```yaml name: Tests @@ -750,10 +755,32 @@ jobs: files: lcov.info fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} +``` + +#### Benchmarks Workflow (`.github/workflows/benchmarks.yml`) + +The benchmarks workflow is triggered manually and runs independently: + +```yaml +name: Benchmarks + +on: + workflow_dispatch: + inputs: + suite: + description: "Benchmark suite to run" + required: false + default: "all" + type: choice + options: + - all + - performance_benchmarks + - process_collector_benchmarks +jobs: benchmarks: runs-on: ubuntu-latest - needs: test + timeout-minutes: 15 steps: - uses: actions/checkout@v6 with: @@ -772,22 +799,20 @@ jobs: key: criterion-baseline-${{ runner.os }} - name: Run benchmarks - run: mise x -- cargo bench --package procmond 2>&1 | tee - bench-output.txt + env: + BENCH_SUITE: ${{ inputs.suite }} + run: | + if [ "$BENCH_SUITE" = "all" ]; then + mise x -- cargo bench --package procmond 2>&1 | tee bench-output.txt + else + mise x -- cargo bench --package procmond --bench "$BENCH_SUITE" 2>&1 | tee bench-output.txt + fi - name: Check for performance regression run: | - # Criterion reports "regressed" when performance degrades beyond noise threshold. - # Fail CI if any benchmark regresses more than 10%. if grep -q "Performance has regressed" bench-output.txt; then echo "::warning::Performance regression detected in benchmarks" grep -A2 "Performance has regressed" bench-output.txt - if grep -oP 'change: \+\K[0-9.]+' bench-output.txt | awk '{if ($1 > 10.0) exit 1}'; then - echo "All regressions within 10% threshold" - else - echo "::error::Benchmark regression exceeds 10% threshold" - exit 1 - fi else echo "No performance regressions detected" fi @@ -799,40 +824,64 @@ jobs: path: target/criterion key: criterion-baseline-${{ runner.os }} + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + if: always() + with: + name: benchmark-results + path: bench-output.txt + retention-days: 30 + + load-tests: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - uses: jdx/mise-action@v3 + with: + install: true + cache: true + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Run load tests run: NO_COLOR=1 TERM=dumb mise x -- cargo test --package procmond --test load_tests -- --ignored --nocapture 2>&1 | tee load-test-output.txt - - name: Upload benchmark results + - name: Upload load test results uses: actions/upload-artifact@v4 if: always() with: - name: benchmark-results - path: | - bench-output.txt - load-test-output.txt + name: load-test-results + path: load-test-output.txt retention-days: 30 ``` ### CI Jobs Overview -The CI pipeline includes the following jobs: +The main CI pipeline includes the following jobs: 1. **quality**: Runs code formatting and linting checks 2. **test**: Executes the full test suite with all features enabled 3. **test-cross-platform**: Tests on Ubuntu, macOS, and Windows 4. **coverage**: Generates and uploads code coverage reports -5. **benchmarks**: Runs performance benchmarks with regression detection + +The benchmarks workflow includes two independent jobs: + +1. **benchmarks**: Runs performance benchmarks with configurable suite selection (15-minute timeout) +2. **load-tests**: Runs load tests under stress conditions (10-minute timeout) ### Accessing Benchmark Results -Benchmark results are available in multiple ways: +Benchmark results are available through the dedicated benchmarks workflow: -- **Workflow Artifacts**: Download `benchmark-results` artifacts from the GitHub Actions workflow summary page -- **CI Logs**: View benchmark output directly in the workflow logs under the "Run benchmarks" step -- **Performance Alerts**: If a regression exceeds 10%, the CI build will fail with a warning annotation showing which benchmarks regressed +- **Manual Trigger**: Navigate to the Actions tab and select the "Benchmarks" workflow, then choose "Run workflow" to trigger manually +- **Suite Selection**: Choose which benchmark suite to run: "all" (default), "performance_benchmarks", or "process_collector_benchmarks" +- **Workflow Artifacts**: Download `benchmark-results` and `load-test-results` artifacts from the workflow summary page +- **CI Logs**: View benchmark output directly in the workflow logs +- **Performance Alerts**: Regressions are logged as warnings for manual review without failing the workflow -The `benchmarks` job stores baseline results from the `main` branch and compares all subsequent runs against this baseline to detect performance regressions. +The benchmarks workflow stores baseline results from the `main` branch and compares all subsequent runs against this baseline to detect performance regressions. ### Test Reporting diff --git a/justfile b/justfile index faf7027..f3e5b1b 100644 --- a/justfile +++ b/justfile @@ -186,6 +186,10 @@ test-security: bench: @{{ mise_exec }} cargo bench --workspace +# Run procmond benchmarks (WAL, EventBus, process collection, serialization) +bench-procmond: + @{{ mise_exec }} cargo bench -p procmond + # Run specific benchmark suites bench-process: @{{ mise_exec }} cargo bench -p daemoneye-lib --bench process_collection diff --git a/mise.lock b/mise.lock index 8f3a917..c715433 100644 --- a/mise.lock +++ b/mise.lock @@ -1,58 +1,195 @@ # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html [[tools.actionlint]] -version = "1.7.10" +version = "1.7.11" backend = "aqua:rhysd/actionlint" -"platforms.linux-arm64" = { checksum = "sha256:cd3dfe5f66887ec6b987752d8d9614e59fd22f39415c5ad9f28374623f41773a", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_arm64.tar.gz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:cd3dfe5f66887ec6b987752d8d9614e59fd22f39415c5ad9f28374623f41773a", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_arm64.tar.gz"} -"platforms.linux-x64" = { checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"} -"platforms.linux-x64-musl" = { checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"} -"platforms.macos-arm64" = { checksum = "sha256:004ca87b367b37f4d75c55ab6cf80f9b8c043adbfbd440f31c604d417939c442", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_arm64.tar.gz"} -"platforms.macos-x64" = { checksum = "sha256:16782c41f2af264db80f855ee5d09164ca98fc78edf3bcd0f46eecff279682ba", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_amd64.tar.gz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:16782c41f2af264db80f855ee5d09164ca98fc78edf3bcd0f46eecff279682ba", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_amd64.tar.gz"} -"platforms.windows-x64" = { checksum = "sha256:283467f9d6202a8cb8c00ad8dd0ee4e685b71fb86a6a56c68fcbb9ae8ed91237", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_windows_amd64.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:283467f9d6202a8cb8c00ad8dd0ee4e685b71fb86a6a56c68fcbb9ae8ed91237", url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_windows_amd64.zip"} + +[tools.actionlint."platforms.linux-arm64"] +checksum = "sha256:21bc0dfb57a913fe175298c2a9e906ee630f747cb66d0a934d0d4b69f4ee1235" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_arm64.tar.gz" + +[tools.actionlint."platforms.linux-arm64-musl"] +checksum = "sha256:21bc0dfb57a913fe175298c2a9e906ee630f747cb66d0a934d0d4b69f4ee1235" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_arm64.tar.gz" + +[tools.actionlint."platforms.linux-x64"] +checksum = "sha256:900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_amd64.tar.gz" + +[tools.actionlint."platforms.linux-x64-baseline"] +checksum = "sha256:900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_amd64.tar.gz" + +[tools.actionlint."platforms.linux-x64-musl"] +checksum = "sha256:900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_amd64.tar.gz" + +[tools.actionlint."platforms.linux-x64-musl-baseline"] +checksum = "sha256:900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_linux_amd64.tar.gz" + +[tools.actionlint."platforms.macos-arm64"] +checksum = "sha256:a21ba7366d8329e7223faee0ed69eb13da27fe8acabb356bb7eb0b7f1e1cb6d8" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_darwin_arm64.tar.gz" + +[tools.actionlint."platforms.macos-x64"] +checksum = "sha256:17ffc17fed8f0258ef6ad4aed932d3272464c7ef7d64e1cb0d65aa97c9752107" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_darwin_amd64.tar.gz" + +[tools.actionlint."platforms.macos-x64-baseline"] +checksum = "sha256:17ffc17fed8f0258ef6ad4aed932d3272464c7ef7d64e1cb0d65aa97c9752107" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_darwin_amd64.tar.gz" + +[tools.actionlint."platforms.windows-x64"] +checksum = "sha256:5414b7124a91f4b5abee62e5c9d84802237734f8d15b9b7032732a32c3ebffa3" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_windows_amd64.zip" + +[tools.actionlint."platforms.windows-x64-baseline"] +checksum = "sha256:5414b7124a91f4b5abee62e5c9d84802237734f8d15b9b7032732a32c3ebffa3" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.11/actionlint_1.7.11_windows_amd64.zip" [[tools.bun]] version = "1.3.10" backend = "core:bun" -"platforms.linux-arm64" = { checksum = "sha256:fa5ecb25cafa8e8f5c87a0f833719d46dd0af0a86c7837d806531212d55636d3", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-aarch64.zip"} -"platforms.linux-arm64-musl" = { checksum = "sha256:d2c81365a2e529b78a42330d3a0056e8dbd7896b4a6782c8e392b6532141e34d", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-aarch64-musl.zip"} -"platforms.linux-x64" = { checksum = "sha256:f57bc0187e39623de716ba3a389fda5486b2d7be7131a980ba54dc7b733d2e08", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64.zip"} -"platforms.linux-x64-baseline" = { checksum = "sha256:41201a8c5ee74a9dcbb1ce25a1104f1f929838b57a845aa78d98379b0ce7cde2", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-baseline.zip"} -"platforms.linux-x64-musl" = { checksum = "sha256:48a6c32277d343db0148ce066336472ffd380358a4d26bb1329714742492d824", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-musl.zip"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:a7bc4cdea1ef255a83adbf39c7aafcd30e09f2b8f74deec4b10ee318bc024d1f", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-musl-baseline.zip"} -"platforms.macos-arm64" = { checksum = "sha256:82034e87c9d9b4398ea619aee2eed5d2a68c8157e9a6ae2d1052d84d533ccd8d", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-aarch64.zip"} -"platforms.macos-x64" = { checksum = "sha256:c1d90bf6140f20e572c473065dc6b37a4b036349b5e9e4133779cc642ad94323", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-x64.zip"} -"platforms.macos-x64-baseline" = { checksum = "sha256:f9686c4e4e760db4cde77a0f1fad05e552648b9c9cbfa4f7fc9a7ec26b9f3267", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-x64-baseline.zip"} -"platforms.windows-x64" = { checksum = "sha256:7a77b3e245e2e26965c93089a4a1332e8a326d3364c89fae1d1fd99cdd3cd73d", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-windows-x64.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:715709c69b176e20994533d3292bd0b7c32de9c0c5575b916746ec6b2aa38346", url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-windows-x64-baseline.zip"} + +[tools.bun."platforms.linux-arm64"] +checksum = "sha256:fa5ecb25cafa8e8f5c87a0f833719d46dd0af0a86c7837d806531212d55636d3" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-aarch64.zip" + +[tools.bun."platforms.linux-arm64-musl"] +checksum = "sha256:d2c81365a2e529b78a42330d3a0056e8dbd7896b4a6782c8e392b6532141e34d" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-aarch64-musl.zip" + +[tools.bun."platforms.linux-x64"] +checksum = "sha256:f57bc0187e39623de716ba3a389fda5486b2d7be7131a980ba54dc7b733d2e08" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64.zip" + +[tools.bun."platforms.linux-x64-baseline"] +checksum = "sha256:41201a8c5ee74a9dcbb1ce25a1104f1f929838b57a845aa78d98379b0ce7cde2" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-baseline.zip" + +[tools.bun."platforms.linux-x64-musl"] +checksum = "sha256:48a6c32277d343db0148ce066336472ffd380358a4d26bb1329714742492d824" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-musl.zip" + +[tools.bun."platforms.linux-x64-musl-baseline"] +checksum = "sha256:a7bc4cdea1ef255a83adbf39c7aafcd30e09f2b8f74deec4b10ee318bc024d1f" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-musl-baseline.zip" + +[tools.bun."platforms.macos-arm64"] +checksum = "sha256:82034e87c9d9b4398ea619aee2eed5d2a68c8157e9a6ae2d1052d84d533ccd8d" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-aarch64.zip" + +[tools.bun."platforms.macos-x64"] +checksum = "sha256:c1d90bf6140f20e572c473065dc6b37a4b036349b5e9e4133779cc642ad94323" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-x64.zip" + +[tools.bun."platforms.macos-x64-baseline"] +checksum = "sha256:f9686c4e4e760db4cde77a0f1fad05e552648b9c9cbfa4f7fc9a7ec26b9f3267" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-darwin-x64-baseline.zip" + +[tools.bun."platforms.windows-x64"] +checksum = "sha256:7a77b3e245e2e26965c93089a4a1332e8a326d3364c89fae1d1fd99cdd3cd73d" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-windows-x64.zip" + +[tools.bun."platforms.windows-x64-baseline"] +checksum = "sha256:715709c69b176e20994533d3292bd0b7c32de9c0c5575b916746ec6b2aa38346" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-windows-x64-baseline.zip" [[tools.cargo-binstall]] -version = "1.17.3" +version = "1.17.7" backend = "aqua:cargo-bins/cargo-binstall" +[tools.cargo-binstall."platforms.linux-arm64"] +checksum = "sha256:b0658b0a7f0959bc1dbb4ab665931c31c7dd1109ff01cb8772af17dfdc52a9af" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-aarch64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.linux-arm64-musl"] +checksum = "sha256:b0658b0a7f0959bc1dbb4ab665931c31c7dd1109ff01cb8772af17dfdc52a9af" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-aarch64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.linux-x64"] +checksum = "sha256:29b5ecfb6e03c2511a617c77d312b06df0c54717644fbfda3d465ec8240532f0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.linux-x64-baseline"] +checksum = "sha256:29b5ecfb6e03c2511a617c77d312b06df0c54717644fbfda3d465ec8240532f0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.linux-x64-musl"] +checksum = "sha256:29b5ecfb6e03c2511a617c77d312b06df0c54717644fbfda3d465ec8240532f0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.linux-x64-musl-baseline"] +checksum = "sha256:29b5ecfb6e03c2511a617c77d312b06df0c54717644fbfda3d465ec8240532f0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-unknown-linux-musl.tgz" + +[tools.cargo-binstall."platforms.macos-arm64"] +checksum = "sha256:1ad3c0c56fa3970634cce5009ed0ce61b943515f9115f8e480fd0e41d8d89085" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-aarch64-apple-darwin.zip" + +[tools.cargo-binstall."platforms.macos-x64"] +checksum = "sha256:aa7174fb938e668dea4b4c3d22fe6cefed97642cc3a7a419ba96d63d63fd729b" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-apple-darwin.zip" + +[tools.cargo-binstall."platforms.macos-x64-baseline"] +checksum = "sha256:aa7174fb938e668dea4b4c3d22fe6cefed97642cc3a7a419ba96d63d63fd729b" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-apple-darwin.zip" + +[tools.cargo-binstall."platforms.windows-x64"] +checksum = "sha256:c5cb2444ee04480502a8ac73d96abd9f97af8300ec04ea1c1f2a9e959c02e4d6" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-pc-windows-msvc.zip" + +[tools.cargo-binstall."platforms.windows-x64-baseline"] +checksum = "sha256:c5cb2444ee04480502a8ac73d96abd9f97af8300ec04ea1c1f2a9e959c02e4d6" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.17.7/cargo-binstall-x86_64-pc-windows-msvc.zip" + [[tools.cargo-insta]] -version = "1.46.1" +version = "1.46.3" backend = "aqua:mitsuhiko/insta" -"platforms.linux-x64" = { checksum = "sha256:fffdd3a5af4e7a67c132390b7622107c72039fe38b495f2e64a5f0a089559196", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-unknown-linux-musl.tar.xz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:fffdd3a5af4e7a67c132390b7622107c72039fe38b495f2e64a5f0a089559196", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-unknown-linux-musl.tar.xz"} -"platforms.linux-x64-musl" = { checksum = "sha256:fffdd3a5af4e7a67c132390b7622107c72039fe38b495f2e64a5f0a089559196", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-unknown-linux-musl.tar.xz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:fffdd3a5af4e7a67c132390b7622107c72039fe38b495f2e64a5f0a089559196", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-unknown-linux-musl.tar.xz"} -"platforms.macos-arm64" = { checksum = "sha256:5e3300ae49633be5fd12e5790f21eaf2350c28d8a97c0405d45a8b7dc23d15de", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-aarch64-apple-darwin.tar.xz"} -"platforms.macos-x64" = { checksum = "sha256:31d379dd9a3d263e3932f4acc254aedc4a9ae20394cb3f1d0d7ecc86308c1bcb", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-apple-darwin.tar.xz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:31d379dd9a3d263e3932f4acc254aedc4a9ae20394cb3f1d0d7ecc86308c1bcb", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-apple-darwin.tar.xz"} -"platforms.windows-x64" = { checksum = "sha256:f833281ee8ba99540a45fb5ce7c5e18838f6fc58852d742b770822a735177b8a", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-pc-windows-msvc.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:f833281ee8ba99540a45fb5ce7c5e18838f6fc58852d742b770822a735177b8a", url = "https://github.com/mitsuhiko/insta/releases/download/1.46.1/cargo-insta-x86_64-pc-windows-msvc.zip"} + +[tools.cargo-insta."platforms.linux-x64"] +checksum = "sha256:c738c47f8d7e834a0277dddb9410a1f7369d37818738fc6a380f22904a83f6e4" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + +[tools.cargo-insta."platforms.linux-x64-baseline"] +checksum = "sha256:c738c47f8d7e834a0277dddb9410a1f7369d37818738fc6a380f22904a83f6e4" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + +[tools.cargo-insta."platforms.linux-x64-musl"] +checksum = "sha256:c738c47f8d7e834a0277dddb9410a1f7369d37818738fc6a380f22904a83f6e4" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + +[tools.cargo-insta."platforms.linux-x64-musl-baseline"] +checksum = "sha256:c738c47f8d7e834a0277dddb9410a1f7369d37818738fc6a380f22904a83f6e4" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + +[tools.cargo-insta."platforms.macos-arm64"] +checksum = "sha256:1e620252db7964d876da6b4956872ad84d099ee281753ac7c850ae24413947df" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-aarch64-apple-darwin.tar.xz" + +[tools.cargo-insta."platforms.macos-x64"] +checksum = "sha256:d55ff42a08ad0fc6deed64bb9ab700c069da9c6da40947d9b658cc33fda3dcda" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-apple-darwin.tar.xz" + +[tools.cargo-insta."platforms.macos-x64-baseline"] +checksum = "sha256:d55ff42a08ad0fc6deed64bb9ab700c069da9c6da40947d9b658cc33fda3dcda" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-apple-darwin.tar.xz" + +[tools.cargo-insta."platforms.windows-x64"] +checksum = "sha256:fa0cd6810e393392cf347decacd8a710de9ac95b6747a753f037c46b649209aa" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-pc-windows-msvc.zip" + +[tools.cargo-insta."platforms.windows-x64-baseline"] +checksum = "sha256:fa0cd6810e393392cf347decacd8a710de9ac95b6747a753f037c46b649209aa" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.3/cargo-insta-x86_64-pc-windows-msvc.zip" [[tools."cargo:cargo-audit"]] -version = "0.22.0" +version = "0.22.1" backend = "cargo:cargo-audit" [[tools."cargo:cargo-auditable"]] -version = "0.7.2" +version = "0.7.4" backend = "cargo:cargo-auditable" [[tools."cargo:cargo-cyclonedx"]] @@ -64,11 +201,11 @@ version = "0.19.0" backend = "cargo:cargo-deny" [[tools."cargo:cargo-llvm-cov"]] -version = "0.6.24" +version = "0.8.4" backend = "cargo:cargo-llvm-cov" [[tools."cargo:cargo-nextest"]] -version = "0.9.123-b.4" +version = "0.9.129" backend = "cargo:cargo-nextest" [[tools."cargo:cargo-outdated"]] @@ -76,7 +213,7 @@ version = "0.17.0" backend = "cargo:cargo-outdated" [[tools."cargo:cargo-release"]] -version = "0.25.22" +version = "1.1.1" backend = "cargo:cargo-release" [[tools."cargo:cargo-zigbuild"]] @@ -118,48 +255,145 @@ backend = "cargo:mdbook-toc" [[tools.goreleaser]] version = "2.14.1" backend = "aqua:goreleaser/goreleaser" -"platforms.linux-arm64" = { checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz"} -"platforms.linux-x64" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"} -"platforms.linux-x64-musl" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"} -"platforms.macos-arm64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"} -"platforms.macos-x64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"} -"platforms.windows-x64" = { checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip"} + +[tools.goreleaser."platforms.linux-arm64"] +checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz" + +[tools.goreleaser."platforms.linux-arm64-musl"] +checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz" + +[tools.goreleaser."platforms.linux-x64"] +checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz" +provenance = "cosign" + +[tools.goreleaser."platforms.linux-x64-baseline"] +checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz" + +[tools.goreleaser."platforms.linux-x64-musl"] +checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz" + +[tools.goreleaser."platforms.linux-x64-musl-baseline"] +checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz" + +[tools.goreleaser."platforms.macos-arm64"] +checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz" +provenance = "cosign" + +[tools.goreleaser."platforms.macos-x64"] +checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz" +provenance = "cosign" + +[tools.goreleaser."platforms.macos-x64-baseline"] +checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz" + +[tools.goreleaser."platforms.windows-x64"] +checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip" +provenance = "cosign" + +[tools.goreleaser."platforms.windows-x64-baseline"] +checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137" +url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip" [[tools.just]] version = "1.46.0" backend = "aqua:casey/just" -"platforms.linux-arm64" = { checksum = "sha256:b81970c8247fa64cfb30d2a3da0e487e4253f9f2d01865ed5e7d53cdc7b02188", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-unknown-linux-musl.tar.gz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:b81970c8247fa64cfb30d2a3da0e487e4253f9f2d01865ed5e7d53cdc7b02188", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64" = { checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-musl" = { checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz"} -"platforms.macos-arm64" = { checksum = "sha256:438eaf6468a115aa7db93e501cc7e3272f453f6b7083be3863adfab546b23358", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-apple-darwin.tar.gz"} -"platforms.macos-x64" = { checksum = "sha256:ec54dd60ac876261b7318f1852ef9c0319fede1e5a73c14f56d908a8edf595b8", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-apple-darwin.tar.gz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:ec54dd60ac876261b7318f1852ef9c0319fede1e5a73c14f56d908a8edf595b8", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-apple-darwin.tar.gz"} -"platforms.windows-x64" = { checksum = "sha256:f0acf3f8ccbcf360b481baae9cae4c921774c89d5d932012481d3e0bda78ab39", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-pc-windows-msvc.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:f0acf3f8ccbcf360b481baae9cae4c921774c89d5d932012481d3e0bda78ab39", url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-pc-windows-msvc.zip"} + +[tools.just."platforms.linux-arm64"] +checksum = "sha256:b81970c8247fa64cfb30d2a3da0e487e4253f9f2d01865ed5e7d53cdc7b02188" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.linux-arm64-musl"] +checksum = "sha256:b81970c8247fa64cfb30d2a3da0e487e4253f9f2d01865ed5e7d53cdc7b02188" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.linux-x64"] +checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.linux-x64-baseline"] +checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.linux-x64-musl"] +checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.linux-x64-musl-baseline"] +checksum = "sha256:79966e6e353f535ee7d1c6221641bcc8e3381c55b0d0a6dc6e54b34f9db36eaa" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-unknown-linux-musl.tar.gz" + +[tools.just."platforms.macos-arm64"] +checksum = "sha256:438eaf6468a115aa7db93e501cc7e3272f453f6b7083be3863adfab546b23358" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-aarch64-apple-darwin.tar.gz" + +[tools.just."platforms.macos-x64"] +checksum = "sha256:ec54dd60ac876261b7318f1852ef9c0319fede1e5a73c14f56d908a8edf595b8" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-apple-darwin.tar.gz" + +[tools.just."platforms.macos-x64-baseline"] +checksum = "sha256:ec54dd60ac876261b7318f1852ef9c0319fede1e5a73c14f56d908a8edf595b8" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-apple-darwin.tar.gz" + +[tools.just."platforms.windows-x64"] +checksum = "sha256:f0acf3f8ccbcf360b481baae9cae4c921774c89d5d932012481d3e0bda78ab39" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-pc-windows-msvc.zip" + +[tools.just."platforms.windows-x64-baseline"] +checksum = "sha256:f0acf3f8ccbcf360b481baae9cae4c921774c89d5d932012481d3e0bda78ab39" +url = "https://github.com/casey/just/releases/download/1.46.0/just-1.46.0-x86_64-pc-windows-msvc.zip" [[tools.lychee]] -version = "0.22.0" +version = "0.23.0" backend = "aqua:lycheeverse/lychee" -"platforms.linux-arm64" = { checksum = "sha256:29901e5df983fe2ff961ee9660c4ef3b4a9eae85d4776d0dc89581332e82d6db", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-aarch64-unknown-linux-gnu.tar.gz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:29901e5df983fe2ff961ee9660c4ef3b4a9eae85d4776d0dc89581332e82d6db", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-aarch64-unknown-linux-gnu.tar.gz"} -"platforms.linux-x64" = { checksum = "sha256:fbd422346dc5cd3afde08c9b7dbc032db139f17a25892b1a87f3b8b1222678c5", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:fbd422346dc5cd3afde08c9b7dbc032db139f17a25892b1a87f3b8b1222678c5", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-musl" = { checksum = "sha256:fbd422346dc5cd3afde08c9b7dbc032db139f17a25892b1a87f3b8b1222678c5", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-unknown-linux-musl.tar.gz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:fbd422346dc5cd3afde08c9b7dbc032db139f17a25892b1a87f3b8b1222678c5", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-unknown-linux-musl.tar.gz"} -"platforms.macos-arm64" = { checksum = "sha256:197bf8b7aafd190e38ec8e931a16e38a2d1d2b1e9fa13aa4874d004ba09c4d05", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-arm64-macos.dmg"} -"platforms.windows-x64" = { checksum = "sha256:3f416d1243e7b65e23547e5995b0e3c3388c1bc6451d36764acfd3ee0e5c8968", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-windows.exe"} -"platforms.windows-x64-baseline" = { checksum = "sha256:3f416d1243e7b65e23547e5995b0e3c3388c1bc6451d36764acfd3ee0e5c8968", url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.22.0/lychee-x86_64-windows.exe"} + +[tools.lychee."platforms.linux-arm64"] +checksum = "sha256:97eb93b02a7d78a752fc33e5b0983439ccaadbf3db952b68a0a4401acd92e6e0" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-aarch64-unknown-linux-gnu.tar.gz" + +[tools.lychee."platforms.linux-arm64-musl"] +checksum = "sha256:97eb93b02a7d78a752fc33e5b0983439ccaadbf3db952b68a0a4401acd92e6e0" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-aarch64-unknown-linux-gnu.tar.gz" + +[tools.lychee."platforms.linux-x64"] +checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" + +[tools.lychee."platforms.linux-x64-baseline"] +checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" + +[tools.lychee."platforms.linux-x64-musl"] +checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" + +[tools.lychee."platforms.linux-x64-musl-baseline"] +checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" + +[tools.lychee."platforms.macos-arm64"] +checksum = "sha256:4c8034900e11083b68ac6f6582c377ff1f704e268991999e09d717973e493e7f" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-arm64-macos.dmg" + +[tools.lychee."platforms.windows-x64"] +checksum = "sha256:0fda7ff0a60c0250939fc25361c2d4e6e7853c31c996733fdd5a1dd760bcb824" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-windows.exe" + +[tools.lychee."platforms.windows-x64-baseline"] +checksum = "sha256:0fda7ff0a60c0250939fc25361c2d4e6e7853c31c996733fdd5a1dd760bcb824" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-windows.exe" [[tools.markdownlint-cli2]] -version = "0.20.0" +version = "0.21.0" backend = "npm:markdownlint-cli2" [[tools."pipx:mdformat"]] @@ -178,49 +412,148 @@ version = "3.8.1" backend = "npm:prettier" [[tools.protobuf]] -version = "33.4" +version = "34.0" backend = "aqua:protocolbuffers/protobuf/protoc" -"platforms.linux-arm64" = { checksum = "sha256:15aa988f4a6090636525ec236a8e4b3aab41eef402751bd5bb2df6afd9b7b5a5", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-aarch_64.zip"} -"platforms.linux-arm64-musl" = { checksum = "sha256:15aa988f4a6090636525ec236a8e4b3aab41eef402751bd5bb2df6afd9b7b5a5", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-aarch_64.zip"} -"platforms.linux-x64" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-baseline" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-musl" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.macos-arm64" = { checksum = "sha256:726297dcfed58592fd35620a5a6246ae020c39e88f3fd4cb1827df7bcf3dfcf1", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-aarch_64.zip"} -"platforms.macos-x64" = { checksum = "sha256:a49bec10d039e902d3b43e49938c42526f90011467609864fa6386ac4014da58", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-x86_64.zip"} -"platforms.macos-x64-baseline" = { checksum = "sha256:a49bec10d039e902d3b43e49938c42526f90011467609864fa6386ac4014da58", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-x86_64.zip"} -"platforms.windows-x64" = { checksum = "sha256:0b31be019b9fe45a388e93bf1f16d70afdf9ce5caf958ea47892fbc868b5a1b3", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-win64.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:0b31be019b9fe45a388e93bf1f16d70afdf9ce5caf958ea47892fbc868b5a1b3", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-win64.zip"} + +[tools.protobuf."platforms.linux-arm64"] +checksum = "sha256:f0b8aad28be5ea6150c082f96ac57e028154afb9ee29f4ce092b5a39df8ae6c8" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-aarch_64.zip" + +[tools.protobuf."platforms.linux-arm64-musl"] +checksum = "sha256:f0b8aad28be5ea6150c082f96ac57e028154afb9ee29f4ce092b5a39df8ae6c8" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-aarch_64.zip" + +[tools.protobuf."platforms.linux-x64"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protobuf."platforms.linux-x64-baseline"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protobuf."platforms.linux-x64-musl"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protobuf."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protobuf."platforms.macos-arm64"] +checksum = "sha256:3ef35187a3c8aed81ee57e792227e483e558fa56c93fce525e569bff55794c1a" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-aarch_64.zip" + +[tools.protobuf."platforms.macos-x64"] +checksum = "sha256:d58fcd413a9ed458283d54023e409fd5cf767da4ed225d1ffaffd83cf2764f53" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-x86_64.zip" + +[tools.protobuf."platforms.macos-x64-baseline"] +checksum = "sha256:d58fcd413a9ed458283d54023e409fd5cf767da4ed225d1ffaffd83cf2764f53" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-x86_64.zip" + +[tools.protobuf."platforms.windows-x64"] +checksum = "sha256:76ddeb5ae7a31c8f9f7759d3b843a4cadda2150ac037ad0c1794665d6cf31fce" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-win64.zip" + +[tools.protobuf."platforms.windows-x64-baseline"] +checksum = "sha256:76ddeb5ae7a31c8f9f7759d3b843a4cadda2150ac037ad0c1794665d6cf31fce" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-win64.zip" [[tools.protoc]] -version = "33.4" +version = "34.0" backend = "aqua:protocolbuffers/protobuf/protoc" -"platforms.linux-arm64" = { checksum = "sha256:15aa988f4a6090636525ec236a8e4b3aab41eef402751bd5bb2df6afd9b7b5a5", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-aarch_64.zip"} -"platforms.linux-arm64-musl" = { checksum = "sha256:15aa988f4a6090636525ec236a8e4b3aab41eef402751bd5bb2df6afd9b7b5a5", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-aarch_64.zip"} -"platforms.linux-x64" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-baseline" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-musl" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:c0040ea9aef08fdeb2c74ca609b18d5fdbfc44ea0042fcfbfb38860d35f7dd66", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip"} -"platforms.macos-arm64" = { checksum = "sha256:726297dcfed58592fd35620a5a6246ae020c39e88f3fd4cb1827df7bcf3dfcf1", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-aarch_64.zip"} -"platforms.macos-x64" = { checksum = "sha256:a49bec10d039e902d3b43e49938c42526f90011467609864fa6386ac4014da58", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-x86_64.zip"} -"platforms.macos-x64-baseline" = { checksum = "sha256:a49bec10d039e902d3b43e49938c42526f90011467609864fa6386ac4014da58", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-osx-x86_64.zip"} -"platforms.windows-x64" = { checksum = "sha256:0b31be019b9fe45a388e93bf1f16d70afdf9ce5caf958ea47892fbc868b5a1b3", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-win64.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:0b31be019b9fe45a388e93bf1f16d70afdf9ce5caf958ea47892fbc868b5a1b3", url = "https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-win64.zip"} + +[tools.protoc."platforms.linux-arm64"] +checksum = "sha256:f0b8aad28be5ea6150c082f96ac57e028154afb9ee29f4ce092b5a39df8ae6c8" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-aarch_64.zip" + +[tools.protoc."platforms.linux-arm64-musl"] +checksum = "sha256:f0b8aad28be5ea6150c082f96ac57e028154afb9ee29f4ce092b5a39df8ae6c8" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-aarch_64.zip" + +[tools.protoc."platforms.linux-x64"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protoc."platforms.linux-x64-baseline"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protoc."platforms.linux-x64-musl"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protoc."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e9a91b6fcfe4177ec2cd35fc8f15c1e811fa0ecdef9372755cd6d3513d5faaab" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-linux-x86_64.zip" + +[tools.protoc."platforms.macos-arm64"] +checksum = "sha256:3ef35187a3c8aed81ee57e792227e483e558fa56c93fce525e569bff55794c1a" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-aarch_64.zip" + +[tools.protoc."platforms.macos-x64"] +checksum = "sha256:d58fcd413a9ed458283d54023e409fd5cf767da4ed225d1ffaffd83cf2764f53" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-x86_64.zip" + +[tools.protoc."platforms.macos-x64-baseline"] +checksum = "sha256:d58fcd413a9ed458283d54023e409fd5cf767da4ed225d1ffaffd83cf2764f53" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-osx-x86_64.zip" + +[tools.protoc."platforms.windows-x64"] +checksum = "sha256:76ddeb5ae7a31c8f9f7759d3b843a4cadda2150ac037ad0c1794665d6cf31fce" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-win64.zip" + +[tools.protoc."platforms.windows-x64-baseline"] +checksum = "sha256:76ddeb5ae7a31c8f9f7759d3b843a4cadda2150ac037ad0c1794665d6cf31fce" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v34.0/protoc-34.0-win64.zip" [[tools.python]] version = "3.14.3" backend = "core:python" -"platforms.linux-arm64" = { checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.linux-x64" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.linux-x64-musl" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"} -"platforms.macos-arm64" = { checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"} -"platforms.macos-x64" = { checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"} -"platforms.windows-x64" = { checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"} -"platforms.windows-x64-baseline" = { checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"} + +[tools.python."platforms.linux-arm64"] +checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-arm64-musl"] +checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-x64"] +checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-x64-baseline"] +checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-x64-musl"] +checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-x64-musl-baseline"] +checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.macos-arm64"] +checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz" + +[tools.python."platforms.macos-x64"] +checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz" + +[tools.python."platforms.macos-x64-baseline"] +checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz" + +[tools.python."platforms.windows-x64"] +checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" + +[tools.python."platforms.windows-x64-baseline"] +checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" [[tools.rust]] version = "1.94.0" @@ -229,14 +562,47 @@ backend = "core:rust" [[tools.zig]] version = "0.15.2" backend = "core:zig" -"platforms.linux-arm64" = { checksum = "sha256:958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f", url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz"} -"platforms.linux-arm64-musl" = { checksum = "sha256:958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f", url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz"} -"platforms.linux-x64" = { checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239", url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz"} -"platforms.linux-x64-baseline" = { checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239", url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz"} -"platforms.linux-x64-musl" = { checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239", url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz"} -"platforms.linux-x64-musl-baseline" = { checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239", url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz"} -"platforms.macos-arm64" = { checksum = "sha256:3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b", url = "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz"} -"platforms.macos-x64" = { checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f", url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz"} -"platforms.macos-x64-baseline" = { checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f", url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz"} -"platforms.windows-x64" = { checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c", url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip"} -"platforms.windows-x64-baseline" = { checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c", url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip"} + +[tools.zig."platforms.linux-arm64"] +checksum = "sha256:958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f" +url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.linux-arm64-musl"] +checksum = "sha256:958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f" +url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.linux-x64"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.linux-x64-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.linux-x64-musl"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.linux-x64-musl-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + +[tools.zig."platforms.macos-arm64"] +checksum = "sha256:3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b" +url = "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz" + +[tools.zig."platforms.macos-x64"] +checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" + +[tools.zig."platforms.macos-x64-baseline"] +checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" + +[tools.zig."platforms.windows-x64"] +checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" + +[tools.zig."platforms.windows-x64-baseline"] +checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" diff --git a/mise.toml b/mise.toml index 0ea4a31..d9dfef9 100644 --- a/mise.toml +++ b/mise.toml @@ -1,11 +1,11 @@ # Several tools are set to "latest" to ensure they are using the idiomatic version file. [tools] -cargo-binstall = "1.17.3" -cargo-insta = "1.46.1" -"cargo:cargo-audit" = "0.22.0" +cargo-binstall = "1.17.7" +cargo-insta = "1.46.3" +"cargo:cargo-audit" = "0.22.1" "cargo:cargo-deny" = "0.19.0" -"cargo:cargo-llvm-cov" = "0.6.24" -"cargo:cargo-nextest" = "0.9.123-b.4" +"cargo:cargo-llvm-cov" = "0.8.4" +"cargo:cargo-nextest" = "0.9.129" "cargo:mdbook" = "0.5.2" "cargo:mdbook-linkcheck" = "0.7.7" "cargo:mdbook-tabs" = "0.3.4" @@ -18,16 +18,16 @@ just = "latest" python = "latest" rust = { version = "latest", components = "llvm-tools,cargo,rustfmt,clippy", profile = "default" } "cargo:cargo-outdated" = "0.17.0" -"cargo:cargo-release" = "0.25.22" -"cargo:cargo-auditable" = "0.7.2" +"cargo:cargo-release" = "1.1.1" +"cargo:cargo-auditable" = "0.7.4" "cargo:cargo-cyclonedx" = "0.5.7" "pipx:mdformat" = { version = "0.7.22", uvx_args = "--with mdformat-gfm --with mdformat-frontmatter --with mdformat-footnote --with mdformat-simple-breaks --with mdformat-gfm-alerts --with mdformat-toc --with mdformat-wikilink --with mdformat-tables" } prettier = "3.8.1" -actionlint = "1.7.10" -lychee = "0.22.0" -markdownlint-cli2 = "0.20.0" -protobuf = "33.4" -protoc = "33.4" +actionlint = "1.7.11" +lychee = "0.23.0" +markdownlint-cli2 = "0.21.0" +protobuf = "34.0" +protoc = "34.0" zig = "latest" "cargo:cargo-zigbuild" = "latest" bun = "latest" diff --git a/procmond/Cargo.toml b/procmond/Cargo.toml index a1d7c15..02050cd 100644 --- a/procmond/Cargo.toml +++ b/procmond/Cargo.toml @@ -98,7 +98,19 @@ name = "process_collector_benchmarks" harness = false [[bench]] -name = "performance_benchmarks" +name = "wal_benchmarks" +harness = false + +[[bench]] +name = "eventbus_benchmarks" +harness = false + +[[bench]] +name = "serialization_benchmarks" +harness = false + +[[bench]] +name = "system_benchmarks" harness = false [lints] diff --git a/procmond/benches/bench_helpers.rs b/procmond/benches/bench_helpers.rs new file mode 100644 index 0000000..d3c6050 --- /dev/null +++ b/procmond/benches/bench_helpers.rs @@ -0,0 +1,98 @@ +//! Shared helper functions for benchmark suites. +//! +//! This module provides common test event constructors used across +//! all benchmark files. Include it with: +//! +//! ```ignore +//! #[path = "bench_helpers.rs"] +//! mod bench_helpers; +//! use bench_helpers::*; +//! ``` + +#![allow( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_lossless, + clippy::modulo_arithmetic, + clippy::uninlined_format_args, + clippy::str_to_string +)] + +use collector_core::ProcessEvent; +use std::time::SystemTime; + +/// Create a test process event with the given PID. +pub fn create_test_event(pid: u32) -> ProcessEvent { + let now = SystemTime::now(); + ProcessEvent { + pid, + ppid: Some(1), + name: format!("benchmark_process_{}", pid), + executable_path: Some(format!("/usr/bin/benchmark_{}", pid)), + command_line: vec![ + format!("benchmark_{}", pid), + "--test".to_owned(), + format!("--id={}", pid), + ], + start_time: Some(now), + cpu_usage: Some(1.5 + (pid as f64 * 0.1) % 10.0), + memory_usage: Some(1_048_576_u64.saturating_add((pid as u64).saturating_mul(4096))), + executable_hash: Some(format!("hash_{:08x}", pid)), + user_id: Some("1000".to_owned()), + accessible: true, + file_exists: true, + timestamp: now, + platform_metadata: None, + } +} + +/// Create a minimal test process event (smaller serialization size). +pub fn create_minimal_event(pid: u32) -> ProcessEvent { + ProcessEvent { + pid, + ppid: None, + name: "min".to_owned(), + executable_path: None, + command_line: vec![], + start_time: None, + cpu_usage: None, + memory_usage: None, + executable_hash: None, + user_id: None, + accessible: true, + file_exists: true, + timestamp: SystemTime::now(), + platform_metadata: None, + } +} + +/// Create a large test process event (larger serialization size). +pub fn create_large_event(pid: u32) -> ProcessEvent { + let now = SystemTime::now(); + let long_args: Vec = (0..50).map(|i| format!("--arg{}=value{}", i, i)).collect(); + + ProcessEvent { + pid, + ppid: Some(1), + name: format!("large_process_with_very_long_name_{}", pid), + executable_path: Some(format!( + "/usr/local/bin/very/deep/nested/path/benchmark_{}", + pid + )), + command_line: long_args, + start_time: Some(now), + cpu_usage: Some(99.9), + memory_usage: Some(1_073_741_824), // 1 GB + executable_hash: Some(format!( + "sha256:{}", + "a".repeat(64) // Realistic SHA-256 length + )), + user_id: Some("root".to_owned()), + accessible: true, + file_exists: true, + timestamp: now, + // Note: platform_metadata with serde_json::Value is not directly serializable + // with postcard (WontImplement error), so we leave it as None for benchmarks + platform_metadata: None, + } +} diff --git a/procmond/benches/eventbus_benchmarks.rs b/procmond/benches/eventbus_benchmarks.rs new file mode 100644 index 0000000..e5865ea --- /dev/null +++ b/procmond/benches/eventbus_benchmarks.rs @@ -0,0 +1,176 @@ +//! EventBus connector performance benchmarks using Criterion. +//! +//! This benchmark suite measures event buffering latency, throughput, +//! and WAL replay through the `EventBusConnector`. +//! +//! # Running benchmarks +//! +//! ```bash +//! cargo bench --package procmond --bench eventbus_benchmarks +//! ``` + +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::print_stdout, + clippy::use_debug, + clippy::uninlined_format_args, + clippy::semicolon_if_nothing_returned, + clippy::doc_markdown, + clippy::explicit_iter_loop, + clippy::shadow_reuse, + clippy::cast_lossless +)] + +#[allow(dead_code)] +#[path = "bench_helpers.rs"] +mod bench_helpers; + +use bench_helpers::create_test_event; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; +use std::hint::black_box; +use std::time::Duration; +use tempfile::TempDir; +use tokio::runtime::Runtime; + +/// Benchmark event buffering latency (disconnected mode). +fn bench_eventbus_buffer_operations(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("eventbus_buffer"); + + // Buffer single event + group.bench_function("buffer_single_event", |b| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) + .await + .expect("Failed to create connector"); + + let event = create_test_event(1); + let start = std::time::Instant::now(); + let sequence = connector + .publish(event, ProcessEventType::Start) + .await + .expect("Failed to publish"); + let duration = start.elapsed(); + + black_box((sequence, duration)) + }) + }); + }); + + group.finish(); +} + +/// Benchmark event buffering throughput. +fn bench_eventbus_buffer_throughput(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("eventbus_buffer_throughput"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + let batch_sizes = [100, 500, 1000]; + + for batch_size in batch_sizes.iter() { + group.throughput(Throughput::Elements(*batch_size as u64)); + group.bench_with_input( + BenchmarkId::new("buffer_batch", batch_size), + batch_size, + |b, &batch_size| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) + .await + .expect("Failed to create connector"); + + let start = std::time::Instant::now(); + for i in 0..batch_size { + let event = create_test_event(i as u32); + connector + .publish(event, ProcessEventType::Start) + .await + .expect("Failed to publish event in benchmark"); + } + let duration = start.elapsed(); + + let rate = batch_size as f64 / duration.as_secs_f64(); + if batch_size >= 500 { + eprintln!( + "EventBus buffer throughput: {} events in {:.2}ms, rate: {:.1} events/sec", + batch_size, + duration.as_millis(), + rate + ); + } + + black_box((duration, rate)) + }) + }); + }, + ); + } + + group.finish(); +} + +/// Benchmark WAL replay through EventBusConnector. +fn bench_eventbus_wal_replay(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("eventbus_wal_replay"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + let event_counts = [100, 500, 1000]; + + for event_count in event_counts.iter() { + group.throughput(Throughput::Elements(*event_count as u64)); + group.bench_with_input( + BenchmarkId::new("wal_replay_through_connector", event_count), + event_count, + |b, &event_count| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) + .await + .expect("Failed to create connector"); + + // Publish events (they go to WAL and buffer) + for i in 0..event_count { + let event = create_test_event(i as u32); + connector + .publish(event, ProcessEventType::Start) + .await + .expect("Failed to publish"); + } + + // Measure replay time (while disconnected, events go to buffer) + let start = std::time::Instant::now(); + let replayed = connector.replay_wal().await.expect("Failed to replay"); + let duration = start.elapsed(); + + black_box((duration, replayed)) + }) + }); + }, + ); + } + + group.finish(); +} + +criterion_group!( + eventbus_benchmarks, + bench_eventbus_buffer_operations, + bench_eventbus_buffer_throughput, + bench_eventbus_wal_replay +); + +criterion_main!(eventbus_benchmarks); diff --git a/procmond/benches/performance_benchmarks.rs b/procmond/benches/performance_benchmarks.rs deleted file mode 100644 index 05a784f..0000000 --- a/procmond/benches/performance_benchmarks.rs +++ /dev/null @@ -1,1045 +0,0 @@ -//! Performance baseline benchmarks using Criterion. -//! -//! This benchmark suite establishes performance baselines for critical operations -//! in procmond. The results help ensure the system meets its performance budgets: -//! -//! - Process enumeration: < 5s for 10,000+ processes -//! - DB Writes: > 1,000 records/sec -//! - Alert latency: < 100ms per rule -//! - CPU Usage: < 5% sustained -//! - Memory: < 100 MB resident -//! -//! # Running benchmarks -//! -//! ```bash -//! cargo bench --package procmond --bench performance_benchmarks -//! ``` - -#![allow( - clippy::doc_markdown, - clippy::unreadable_literal, - clippy::expect_used, - clippy::unwrap_used, - clippy::str_to_string, - clippy::arithmetic_side_effects, - clippy::missing_const_for_fn, - clippy::uninlined_format_args, - clippy::print_stdout, - clippy::map_unwrap_or, - clippy::non_ascii_literal, - clippy::use_debug, - clippy::shadow_reuse, - clippy::shadow_unrelated, - clippy::needless_pass_by_value, - clippy::redundant_clone, - clippy::as_conversions, - clippy::panic, - clippy::option_if_let_else, - clippy::wildcard_enum_match_arm, - clippy::large_enum_variant, - clippy::integer_division, - clippy::clone_on_ref_ptr, - clippy::unused_self, - clippy::modulo_arithmetic, - clippy::explicit_iter_loop, - clippy::semicolon_if_nothing_returned, - clippy::missing_assert_message, - clippy::pattern_type_mismatch, - clippy::significant_drop_tightening, - clippy::significant_drop_in_scrutinee, - clippy::if_not_else, - clippy::indexing_slicing, - clippy::cast_lossless, - clippy::items_after_statements, - clippy::redundant_closure_for_method_calls -)] - -use collector_core::ProcessEvent; -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use procmond::wal::{WalEntry, WriteAheadLog}; -use std::hint::black_box; -use std::time::{Duration, SystemTime}; -use tempfile::TempDir; -use tokio::runtime::Runtime; - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/// Create a test process event with the given PID. -fn create_test_event(pid: u32) -> ProcessEvent { - let now = SystemTime::now(); - ProcessEvent { - pid, - ppid: Some(1), - name: format!("benchmark_process_{}", pid), - executable_path: Some(format!("/usr/bin/benchmark_{}", pid)), - command_line: vec![ - format!("benchmark_{}", pid), - "--test".to_owned(), - format!("--id={}", pid), - ], - start_time: Some(now), - cpu_usage: Some(1.5 + (pid as f64 * 0.1) % 10.0), - memory_usage: Some(1_048_576_u64.saturating_add((pid as u64).saturating_mul(4096))), - executable_hash: Some(format!("hash_{:08x}", pid)), - user_id: Some("1000".to_owned()), - accessible: true, - file_exists: true, - timestamp: now, - platform_metadata: None, - } -} - -/// Create a minimal test process event (smaller serialization size). -fn create_minimal_event(pid: u32) -> ProcessEvent { - ProcessEvent { - pid, - ppid: None, - name: "min".to_owned(), - executable_path: None, - command_line: vec![], - start_time: None, - cpu_usage: None, - memory_usage: None, - executable_hash: None, - user_id: None, - accessible: true, - file_exists: true, - timestamp: SystemTime::now(), - platform_metadata: None, - } -} - -/// Create a large test process event (larger serialization size). -fn create_large_event(pid: u32) -> ProcessEvent { - let now = SystemTime::now(); - let long_args: Vec = (0..50).map(|i| format!("--arg{}=value{}", i, i)).collect(); - - ProcessEvent { - pid, - ppid: Some(1), - name: format!("large_process_with_very_long_name_{}", pid), - executable_path: Some(format!( - "/usr/local/bin/very/deep/nested/path/benchmark_{}", - pid - )), - command_line: long_args, - start_time: Some(now), - cpu_usage: Some(99.9), - memory_usage: Some(1_073_741_824), // 1 GB - executable_hash: Some(format!( - "sha256:{}", - "a".repeat(64) // Realistic SHA-256 length - )), - user_id: Some("root".to_owned()), - accessible: true, - file_exists: true, - timestamp: now, - // Note: platform_metadata with serde_json::Value is not directly serializable - // with postcard (WontImplement error), so we leave it as None for benchmarks - platform_metadata: None, - } -} - -// ============================================================================ -// WAL Operations Benchmarks -// ============================================================================ - -/// Benchmark WAL single write latency. -fn bench_wal_write_single(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("wal_write_single"); - - // Different event sizes - let event_types = [("minimal", 0), ("standard", 1), ("large", 2)]; - - for (event_type, type_id) in event_types.iter() { - group.bench_function(BenchmarkId::new("write_latency", event_type), |b| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - // Use 10MB rotation threshold for benchmarks - let wal = WriteAheadLog::with_rotation_threshold( - temp_dir.path().to_path_buf(), - 10 * 1024 * 1024, - ) - .await - .expect("Failed to create WAL"); - - let event = match type_id { - 0 => create_minimal_event(1), - 1 => create_test_event(1), - _ => create_large_event(1), - }; - - let sequence = wal.write(event).await.expect("Failed to write"); - black_box(sequence) - }) - }); - }); - } - - group.finish(); -} - -/// Benchmark WAL batch write throughput. -fn bench_wal_write_throughput(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("wal_write_throughput"); - - // Set longer measurement time for throughput tests - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); - - // Test different batch sizes - let batch_sizes = [100, 500, 1000, 5000]; - - for batch_size in batch_sizes.iter() { - group.throughput(Throughput::Elements(*batch_size as u64)); - group.bench_with_input( - BenchmarkId::new("batch_write", batch_size), - batch_size, - |b, &batch_size| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - // Use larger rotation threshold for batch tests - let wal = WriteAheadLog::with_rotation_threshold( - temp_dir.path().to_path_buf(), - 50 * 1024 * 1024, - ) - .await - .expect("Failed to create WAL"); - - let start = std::time::Instant::now(); - for i in 0..batch_size { - let event = create_test_event(i as u32); - wal.write(event).await.expect("Failed to write"); - } - let duration = start.elapsed(); - - let rate = batch_size as f64 / duration.as_secs_f64(); - if batch_size >= 1000 { - eprintln!( - "WAL write throughput: {} events in {:.2}ms, rate: {:.1} events/sec", - batch_size, - duration.as_millis(), - rate - ); - } - - black_box((duration, rate)) - }) - }); - }, - ); - } - - group.finish(); -} - -/// Benchmark WAL replay latency. -fn bench_wal_replay(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("wal_replay"); - - group.measurement_time(Duration::from_secs(20)); - group.sample_size(10); - - let event_counts = [100, 500, 1000, 2500]; - - for event_count in event_counts.iter() { - group.throughput(Throughput::Elements(*event_count as u64)); - group.bench_with_input( - BenchmarkId::new("replay_latency", event_count), - event_count, - |b, &event_count| { - // Pre-populate WAL with events before each benchmark iteration - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let wal = WriteAheadLog::with_rotation_threshold( - temp_dir.path().to_path_buf(), - 50 * 1024 * 1024, - ) - .await - .expect("Failed to create WAL"); - - // Write events - for i in 0..event_count { - let event = create_test_event(i as u32); - wal.write(event).await.expect("Failed to write"); - } - - // Measure replay - let start = std::time::Instant::now(); - let events = wal.replay().await.expect("Failed to replay"); - let duration = start.elapsed(); - - assert_eq!(events.len(), event_count); - - let rate = events.len() as f64 / duration.as_secs_f64(); - if event_count >= 1000 { - eprintln!( - "WAL replay: {} events in {:.2}ms, rate: {:.1} events/sec", - events.len(), - duration.as_millis(), - rate - ); - } - - black_box((duration, events.len())) - }) - }); - }, - ); - } - - group.finish(); -} - -/// Benchmark WAL file rotation time. -fn bench_wal_rotation(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("wal_rotation"); - - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); - - // Use a very small rotation threshold to trigger rotation - let rotation_threshold = 10 * 1024; // 10KB - will rotate frequently - - group.bench_function("rotation_triggered", |b| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let wal = WriteAheadLog::with_rotation_threshold( - temp_dir.path().to_path_buf(), - rotation_threshold, - ) - .await - .expect("Failed to create WAL"); - - // Write events until rotation happens multiple times - // Track rotation by counting .wal files in directory - let initial_file_count = std::fs::read_dir(temp_dir.path()) - .map(|entries| { - entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "wal")) - .count() - }) - .unwrap_or(0); - - for i in 0..500 { - let event = create_large_event(i); - wal.write(event).await.expect("Failed to write"); - } - - // Count rotations by checking final file count - let final_file_count = std::fs::read_dir(temp_dir.path()) - .map(|entries| { - entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "wal")) - .count() - }) - .unwrap_or(0); - - let rotations_triggered = final_file_count.saturating_sub(initial_file_count); - black_box(rotations_triggered) - }) - }); - }); - - group.finish(); -} - -// ============================================================================ -// EventBusConnector Benchmarks (simulated, without actual broker connection) -// ============================================================================ - -/// Benchmark event buffering latency (disconnected mode). -fn bench_eventbus_buffer_operations(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("eventbus_buffer"); - - use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; - - // Buffer single event - group.bench_function("buffer_single_event", |b| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) - .await - .expect("Failed to create connector"); - - let event = create_test_event(1); - let start = std::time::Instant::now(); - let sequence = connector - .publish(event, ProcessEventType::Start) - .await - .expect("Failed to publish"); - let duration = start.elapsed(); - - black_box((sequence, duration)) - }) - }); - }); - - group.finish(); -} - -/// Benchmark event buffering throughput. -fn bench_eventbus_buffer_throughput(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("eventbus_buffer_throughput"); - - use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; - - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); - - let batch_sizes = [100, 500, 1000]; - - for batch_size in batch_sizes.iter() { - group.throughput(Throughput::Elements(*batch_size as u64)); - group.bench_with_input( - BenchmarkId::new("buffer_batch", batch_size), - batch_size, - |b, &batch_size| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) - .await - .expect("Failed to create connector"); - - let start = std::time::Instant::now(); - for i in 0..batch_size { - let event = create_test_event(i as u32); - // Benchmark: Measuring throughput, result not relevant for timing - drop(connector.publish(event, ProcessEventType::Start).await); - } - let duration = start.elapsed(); - - let rate = batch_size as f64 / duration.as_secs_f64(); - if batch_size >= 500 { - eprintln!( - "EventBus buffer throughput: {} events in {:.2}ms, rate: {:.1} events/sec", - batch_size, - duration.as_millis(), - rate - ); - } - - black_box((duration, rate)) - }) - }); - }, - ); - } - - group.finish(); -} - -/// Benchmark WAL replay through EventBusConnector. -fn bench_eventbus_wal_replay(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("eventbus_wal_replay"); - - use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; - - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); - - let event_counts = [100, 500, 1000]; - - for event_count in event_counts.iter() { - group.throughput(Throughput::Elements(*event_count as u64)); - group.bench_with_input( - BenchmarkId::new("wal_replay_through_connector", event_count), - event_count, - |b, &event_count| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) - .await - .expect("Failed to create connector"); - - // Publish events (they go to WAL and buffer) - for i in 0..event_count { - let event = create_test_event(i as u32); - connector - .publish(event, ProcessEventType::Start) - .await - .expect("Failed to publish"); - } - - // Measure replay time (while disconnected, events go to buffer) - let start = std::time::Instant::now(); - let replayed = connector.replay_wal().await.expect("Failed to replay"); - let duration = start.elapsed(); - - black_box((duration, replayed)) - }) - }); - }, - ); - } - - group.finish(); -} - -// ============================================================================ -// Process Collection Benchmarks -// ============================================================================ - -/// Benchmark process collection using real system processes. -fn bench_process_collection_real(c: &mut Criterion) { - use procmond::process_collector::{ - ProcessCollectionConfig, ProcessCollector, SysinfoProcessCollector, - }; - - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("process_collection_real"); - - // Allow longer measurement for real system collection - group.measurement_time(Duration::from_secs(30)); - group.sample_size(10); - - let configs = [ - ( - "basic", - ProcessCollectionConfig { - collect_enhanced_metadata: false, - compute_executable_hashes: false, - skip_system_processes: false, - skip_kernel_threads: false, - max_processes: 10000, - }, - ), - ( - "enhanced", - ProcessCollectionConfig { - collect_enhanced_metadata: true, - compute_executable_hashes: false, - skip_system_processes: false, - skip_kernel_threads: false, - max_processes: 10000, - }, - ), - ]; - - for (config_name, config) in configs { - group.bench_function(BenchmarkId::new("sysinfo_collector", config_name), |b| { - b.iter(|| { - rt.block_on(async { - let collector = SysinfoProcessCollector::new(config.clone()); - - let start = std::time::Instant::now(); - let result = collector.collect_processes().await; - let duration = start.elapsed(); - - if let Ok((events, stats)) = result { - let rate = events.len() as f64 / duration.as_secs_f64(); - eprintln!( - "Process collection ({}): {} processes in {:.2}ms, rate: {:.1} proc/sec", - config_name, - events.len(), - duration.as_millis(), - rate - ); - black_box((duration, events.len(), stats)); - } else { - eprintln!("Process collection failed: {:?}", result.err()); - black_box(duration); - } - }) - }); - }); - } - - group.finish(); -} - -/// Benchmark single process collection. -fn bench_process_collection_single(c: &mut Criterion) { - use procmond::process_collector::{ - ProcessCollectionConfig, ProcessCollector, SysinfoProcessCollector, - }; - - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("process_collection_single"); - - let config = ProcessCollectionConfig::default(); - - // Get current process PID for benchmark - let current_pid = std::process::id(); - - group.bench_function("collect_single_process", |b| { - b.iter(|| { - rt.block_on(async { - let collector = SysinfoProcessCollector::new(config.clone()); - - let start = std::time::Instant::now(); - let result = collector.collect_process(current_pid).await; - let duration = start.elapsed(); - - if let Ok(event) = result { - black_box((duration, event.pid)); - } else { - black_box(duration); - } - }) - }); - }); - - group.finish(); -} - -// ============================================================================ -// Serialization Benchmarks -// ============================================================================ - -/// Benchmark event serialization using postcard (as used by WAL). -fn bench_serialization_postcard(c: &mut Criterion) { - let mut group = c.benchmark_group("serialization_postcard"); - - let event_types = [ - ("minimal", create_minimal_event(1)), - ("standard", create_test_event(1)), - ("large", create_large_event(1)), - ]; - - for (event_type, event) in event_types.iter() { - // Serialize benchmark - group.bench_function(BenchmarkId::new("serialize", event_type), |b| { - let entry = WalEntry::new(1, event.clone()); - b.iter(|| { - let serialized = postcard::to_allocvec(&entry).expect("Failed to serialize"); - black_box(serialized) - }); - }); - - // Deserialize benchmark - let entry = WalEntry::new(1, event.clone()); - let serialized = postcard::to_allocvec(&entry).expect("Failed to serialize"); - - group.bench_function(BenchmarkId::new("deserialize", event_type), |b| { - b.iter(|| { - let deserialized: WalEntry = - postcard::from_bytes(&serialized).expect("Failed to deserialize"); - black_box(deserialized) - }); - }); - } - - group.finish(); -} - -/// Benchmark event serialization using serde_json (for comparison and debugging). -fn bench_serialization_json(c: &mut Criterion) { - let mut group = c.benchmark_group("serialization_json"); - - let event_types = [ - ("minimal", create_minimal_event(1)), - ("standard", create_test_event(1)), - ("large", create_large_event(1)), - ]; - - for (event_type, event) in event_types.iter() { - // Serialize benchmark - group.bench_function(BenchmarkId::new("serialize", event_type), |b| { - b.iter(|| { - let serialized = serde_json::to_string(&event).expect("Failed to serialize"); - black_box(serialized) - }); - }); - - // Deserialize benchmark - let serialized = serde_json::to_string(&event).expect("Failed to serialize"); - - group.bench_function(BenchmarkId::new("deserialize", event_type), |b| { - b.iter(|| { - let deserialized: ProcessEvent = - serde_json::from_str(&serialized).expect("Failed to deserialize"); - black_box(deserialized) - }); - }); - } - - group.finish(); -} - -/// Benchmark CRC32 checksum computation (used by WAL entries). -fn bench_checksum_computation(c: &mut Criterion) { - use std::hash::Hasher; - - let mut group = c.benchmark_group("checksum_crc32c"); - - let event_types = [ - ("minimal", create_minimal_event(1)), - ("standard", create_test_event(1)), - ("large", create_large_event(1)), - ]; - - for (event_type, event) in event_types.iter() { - let serialized = postcard::to_allocvec(&event).expect("Failed to serialize"); - let data_size = serialized.len(); - - group.throughput(Throughput::Bytes(data_size as u64)); - group.bench_function(BenchmarkId::new("compute", event_type), |b| { - b.iter(|| { - let mut crc = crc32c::Crc32cHasher::new(0); - crc.write(&serialized); - let checksum = crc.finish() as u32; - black_box(checksum) - }); - }); - } - - group.finish(); -} - -/// Benchmark batch serialization throughput. -fn bench_serialization_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("serialization_throughput"); - - group.measurement_time(Duration::from_secs(10)); - group.sample_size(10); - - let batch_sizes = [100, 1000, 5000]; - - for batch_size in batch_sizes.iter() { - group.throughput(Throughput::Elements(*batch_size as u64)); - - // Postcard batch serialization - group.bench_with_input( - BenchmarkId::new("postcard_batch", batch_size), - batch_size, - |b, &batch_size| { - let events: Vec = (0..batch_size) - .map(|i| create_test_event(i as u32)) - .collect(); - - b.iter(|| { - let mut total_bytes = 0_usize; - for event in &events { - let entry = WalEntry::new(1, event.clone()); - let serialized = - postcard::to_allocvec(&entry).expect("Failed to serialize"); - total_bytes = total_bytes.saturating_add(serialized.len()); - } - black_box(total_bytes) - }); - }, - ); - } - - group.finish(); -} - -// ============================================================================ -// RPC Service Benchmarks -// ============================================================================ - -/// Benchmark RPC health check request/response latency. -/// -/// Uses a lightweight actor channel (no full collector) to measure the RPC -/// handler's request processing overhead in isolation. -fn bench_rpc_health_check_latency(c: &mut Criterion) { - use daemoneye_eventbus::rpc::{ - CollectorOperation, RpcCorrelationMetadata, RpcPayload, RpcRequest, - }; - use procmond::event_bus_connector::EventBusConnector; - use procmond::monitor_collector::{ActorHandle, ActorMessage}; - use procmond::rpc_service::RpcServiceHandler; - use std::sync::Arc; - use tokio::sync::{RwLock, mpsc}; - - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("rpc_latency"); - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); - - group.bench_function("health_check", |b| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - - // Create a lightweight actor channel — no full collector needed - let (tx, mut rx) = mpsc::channel::(32); - let actor_handle = ActorHandle::new(tx); - - // Drain actor messages in background so sends don't block - let drain_task = tokio::spawn(async move { while rx.recv().await.is_some() {} }); - - let connector = EventBusConnector::new(temp_dir.path().to_path_buf()) - .await - .expect("connector"); - let event_bus = Arc::new(RwLock::new(connector)); - - let handler = RpcServiceHandler::with_defaults(actor_handle.clone(), event_bus); - - let request = RpcRequest { - request_id: "bench-1".to_owned(), - client_id: "bench-client".to_owned(), - target: "control.collector.procmond".to_owned(), - operation: CollectorOperation::HealthCheck, - payload: RpcPayload::Empty, - timestamp: SystemTime::now(), - deadline: SystemTime::now() + Duration::from_secs(30), - correlation_metadata: RpcCorrelationMetadata::new("bench-corr".to_owned()), - }; - - let start = std::time::Instant::now(); - let response = handler.handle_request(request).await; - let latency = start.elapsed(); - - // Drop handler and actor_handle before awaiting drain_task - // so all senders are dropped and the drain task can terminate. - drop(handler); - drop(actor_handle); - drain_task.abort(); - - black_box((response, latency)) - }) - }); - }); - - group.finish(); -} - -/// Benchmark data sanitization throughput. -fn bench_sanitization(c: &mut Criterion) { - use procmond::security::{sanitize_command_line, sanitize_file_path}; - - let mut group = c.benchmark_group("sanitization"); - - let test_cases = [ - ( - "clean_cmd", - "app --verbose --output file.txt --debug --workers 4", - ), - ( - "sensitive_cmd", - "app --password secret123 --token ghp_xxx --api-key abc --verbose", - ), - ("long_cmd", &"arg ".repeat(200)), - ]; - - for (name, cmd) in &test_cases { - group.bench_function(BenchmarkId::new("command_line", name), |b| { - b.iter(|| black_box(sanitize_command_line(cmd))); - }); - } - - let path_cases = [ - ("safe_path", "/usr/bin/bash"), - ("sensitive_ssh", "/home/user/.ssh/id_rsa"), - ("sensitive_aws", "/home/user/.aws/credentials"), - ("windows_path", "C:\\Users\\admin\\.ssh\\id_rsa"), - ]; - - for (name, path) in &path_cases { - group.bench_function(BenchmarkId::new("file_path", name), |b| { - b.iter(|| black_box(sanitize_file_path(path))); - }); - } - - group.finish(); -} - -// ============================================================================ -// Combined Workload Benchmarks -// ============================================================================ - -/// Benchmark a realistic workload combining collection, serialization, and WAL writes. -fn bench_combined_workload(c: &mut Criterion) { - use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; - - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("combined_workload"); - - group.measurement_time(Duration::from_secs(20)); - group.sample_size(10); - - let event_counts = [100, 500, 1000]; - - for event_count in event_counts.iter() { - group.throughput(Throughput::Elements(*event_count as u64)); - group.bench_with_input( - BenchmarkId::new("collect_serialize_write", event_count), - event_count, - |b, &event_count| { - b.iter(|| { - rt.block_on(async { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) - .await - .expect("Failed to create connector"); - - let start = std::time::Instant::now(); - - // Simulate collection + publish workflow - for i in 0..event_count { - let event = create_test_event(i as u32); - connector - .publish(event, ProcessEventType::Start) - .await - .expect("Failed to publish"); - } - - let duration = start.elapsed(); - let rate = event_count as f64 / duration.as_secs_f64(); - - if event_count >= 500 { - eprintln!( - "Combined workload: {} events in {:.2}ms, rate: {:.1} events/sec", - event_count, - duration.as_millis(), - rate - ); - - // Performance budget check: > 1000 records/sec - if rate < 1000.0 { - eprintln!( - "WARNING: Combined workload rate {:.1}/sec is below 1000/sec budget", - rate - ); - } - } - - black_box((duration, rate)) - }) - }); - }, - ); - } - - group.finish(); -} - -/// Benchmark memory usage patterns during batch operations. -fn bench_memory_efficiency(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("memory_efficiency"); - - use sysinfo::{ProcessRefreshKind, RefreshKind, System}; - - group.measurement_time(Duration::from_secs(15)); - group.sample_size(10); // Criterion requires at least 10 samples - - let batch_sizes = [1000, 5000, 10000]; - - for batch_size in batch_sizes.iter() { - group.throughput(Throughput::Elements(*batch_size as u64)); - group.bench_with_input( - BenchmarkId::new("event_batch_memory", batch_size), - batch_size, - |b, &batch_size| { - b.iter(|| { - rt.block_on(async { - // Measure memory before - let system_before = System::new_with_specifics( - RefreshKind::nothing() - .with_processes(ProcessRefreshKind::nothing().with_memory()), - ); - let pid = sysinfo::Pid::from_u32(std::process::id()); - let memory_before = - system_before.process(pid).map(|p| p.memory()).unwrap_or(0); - - // Create batch of events - let events: Vec = (0..batch_size) - .map(|i| create_test_event(i as u32)) - .collect(); - - // Measure memory after - let system_after = System::new_with_specifics( - RefreshKind::nothing() - .with_processes(ProcessRefreshKind::nothing().with_memory()), - ); - let memory_after = - system_after.process(pid).map(|p| p.memory()).unwrap_or(0); - - let memory_delta = memory_after.saturating_sub(memory_before); - let memory_per_event = if !events.is_empty() { - memory_delta / events.len() as u64 - } else { - 0 - }; - - eprintln!( - "Memory efficiency: {} events, {}KB total delta, {}B per event", - batch_size, - memory_delta / 1024, - memory_per_event - ); - - black_box((events.len(), memory_delta)) - }) - }); - }, - ); - } - - group.finish(); -} - -// ============================================================================ -// Criterion Groups and Main -// ============================================================================ - -criterion_group!( - wal_benchmarks, - bench_wal_write_single, - bench_wal_write_throughput, - bench_wal_replay, - bench_wal_rotation -); - -criterion_group!( - eventbus_benchmarks, - bench_eventbus_buffer_operations, - bench_eventbus_buffer_throughput, - bench_eventbus_wal_replay -); - -criterion_group!( - process_benchmarks, - bench_process_collection_real, - bench_process_collection_single -); - -criterion_group!( - serialization_benchmarks, - bench_serialization_postcard, - bench_serialization_json, - bench_checksum_computation, - bench_serialization_throughput -); - -criterion_group!( - rpc_benchmarks, - bench_rpc_health_check_latency, - bench_sanitization -); - -criterion_group!( - combined_benchmarks, - bench_combined_workload, - bench_memory_efficiency -); - -criterion_main!( - wal_benchmarks, - eventbus_benchmarks, - process_benchmarks, - serialization_benchmarks, - rpc_benchmarks, - combined_benchmarks -); diff --git a/procmond/benches/process_collector_benchmarks.rs b/procmond/benches/process_collector_benchmarks.rs index b91247b..c250426 100644 --- a/procmond/benches/process_collector_benchmarks.rs +++ b/procmond/benches/process_collector_benchmarks.rs @@ -5,38 +5,28 @@ //! (10,000+ processes) to establish baseline performance metrics. #![allow( - clippy::doc_markdown, - clippy::unreadable_literal, clippy::expect_used, clippy::unwrap_used, - clippy::str_to_string, clippy::arithmetic_side_effects, - clippy::missing_const_for_fn, - clippy::uninlined_format_args, + clippy::as_conversions, clippy::print_stdout, - clippy::map_unwrap_or, - clippy::non_ascii_literal, clippy::use_debug, - clippy::shadow_reuse, - clippy::shadow_unrelated, + clippy::uninlined_format_args, + clippy::missing_assert_message, + clippy::semicolon_if_nothing_returned, + clippy::clone_on_ref_ptr, clippy::needless_pass_by_value, - clippy::redundant_clone, - clippy::as_conversions, - clippy::panic, - clippy::option_if_let_else, - clippy::wildcard_enum_match_arm, - clippy::large_enum_variant, clippy::integer_division, - clippy::clone_on_ref_ptr, + clippy::doc_markdown, + clippy::missing_const_for_fn, clippy::unused_self, clippy::modulo_arithmetic, clippy::explicit_iter_loop, - clippy::semicolon_if_nothing_returned, - clippy::missing_assert_message, + clippy::shadow_reuse, clippy::pattern_type_mismatch, - clippy::significant_drop_tightening, - clippy::significant_drop_in_scrutinee, - clippy::if_not_else + clippy::if_not_else, + clippy::option_if_let_else, + clippy::cast_lossless )] use async_trait::async_trait; @@ -68,7 +58,6 @@ use tokio::runtime::Runtime; struct BenchmarkProcessCollector { name: &'static str, process_count: usize, - delay_per_process: Duration, capabilities: ProcessCollectorCapabilities, } @@ -78,7 +67,6 @@ impl BenchmarkProcessCollector { Self { name, process_count, - delay_per_process: Duration::ZERO, // No artificial delay for accurate benchmarks capabilities: ProcessCollectorCapabilities { basic_info: true, enhanced_metadata: true, @@ -90,12 +78,6 @@ impl BenchmarkProcessCollector { } } - /// Configures a delay per process to simulate system load. - fn with_delay(mut self, delay: Duration) -> Self { - self.delay_per_process = delay; - self - } - /// Generates a synthetic process event for benchmarking purposes. fn generate_process_event(&self, index: usize) -> ProcessEvent { ProcessEvent { @@ -138,9 +120,6 @@ impl ProcessCollector for BenchmarkProcessCollector { let mut events = Vec::with_capacity(self.process_count); for i in 0..self.process_count { - if !self.delay_per_process.is_zero() { - tokio::time::sleep(self.delay_per_process).await; - } events.push(self.generate_process_event(i)); } @@ -173,12 +152,11 @@ fn bench_process_enumeration_scale(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("process_enumeration_scale"); - // Set longer measurement time for high process counts - group.measurement_time(Duration::from_secs(30)); + group.measurement_time(Duration::from_secs(10)); group.sample_size(10); - // Test with various process counts including very high counts (10,000+) - for process_count in [100, 500, 1000, 5000, 10000, 25000, 50000, 100000].iter() { + // Test with representative process counts up to 10,000+ + for process_count in [100, 1000, 5000, 10000].iter() { group.throughput(Throughput::Elements(*process_count as u64)); group.bench_with_input( BenchmarkId::new("enumerate_processes", process_count), @@ -219,41 +197,6 @@ fn bench_process_enumeration_scale(c: &mut Criterion) { group.finish(); } -/// Benchmark process enumeration with simulated system load. -fn bench_process_enumeration_with_load(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - let mut group = c.benchmark_group("process_enumeration_load"); - - // Simulate different system loads with delays - let load_scenarios = [ - ("no_load", Duration::ZERO), - ("light_load", Duration::from_millis(1)), - ("medium_load", Duration::from_millis(5)), - ("heavy_load", Duration::from_millis(10)), - ]; - - for (load_name, delay) in load_scenarios.iter() { - group.bench_function(BenchmarkId::new("system_load", load_name), |b| { - b.iter(|| { - rt.block_on(async { - let collector = - BenchmarkProcessCollector::new("load-benchmark", 10000).with_delay(*delay); - - let start = std::time::Instant::now(); - let result = collector.collect_processes().await; - let duration = start.elapsed(); - - assert!(result.is_ok(), "Collection should succeed under load"); - let (events, stats) = result.unwrap(); - - black_box((duration, events.len(), stats)); - }) - }); - }); - } - group.finish(); -} - /// Benchmark single process collection performance. fn bench_single_process_collection(c: &mut Criterion) { let rt = Runtime::new().unwrap(); @@ -323,7 +266,7 @@ fn bench_concurrent_collection(c: &mut Criterion) { // Wait for all tasks to complete let mut results = Vec::with_capacity(concurrent_tasks); for handle in handles { - let result = handle.await.unwrap(); + let result = handle.await.expect("concurrent collection task panicked"); results.push(result); } @@ -420,11 +363,9 @@ fn bench_real_collectors_high_counts(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("real_collectors_high_counts"); - // Set longer measurement time for real collectors - group.measurement_time(Duration::from_secs(60)); + group.measurement_time(Duration::from_secs(10)); group.sample_size(10); - // Test configurations for high process counts let high_count_configs = vec![ ( "basic_10k", @@ -446,26 +387,6 @@ fn bench_real_collectors_high_counts(c: &mut Criterion) { max_processes: 10000, }, ), - ( - "basic_25k", - ProcessCollectionConfig { - collect_enhanced_metadata: false, - compute_executable_hashes: false, - skip_system_processes: false, - skip_kernel_threads: false, - max_processes: 25000, - }, - ), - ( - "enhanced_25k", - ProcessCollectionConfig { - collect_enhanced_metadata: true, - compute_executable_hashes: false, - skip_system_processes: false, - skip_kernel_threads: false, - max_processes: 25000, - }, - ), ]; for (config_name, config) in high_count_configs { @@ -555,8 +476,7 @@ fn bench_cross_platform_collectors(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("cross_platform_collectors"); - // Set measurement parameters for cross-platform comparison - group.measurement_time(Duration::from_secs(30)); + group.measurement_time(Duration::from_secs(10)); group.sample_size(10); let config = ProcessCollectionConfig { @@ -677,12 +597,10 @@ fn bench_memory_efficiency_high_counts(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("memory_efficiency_high_counts"); - // Set parameters for memory efficiency testing - group.measurement_time(Duration::from_secs(45)); + group.measurement_time(Duration::from_secs(10)); group.sample_size(10); - // Test memory efficiency with extremely high process counts - for process_count in [50000, 100000, 200000].iter() { + for process_count in [10000, 50000].iter() { group.throughput(Throughput::Elements(*process_count as u64)); group.bench_with_input( BenchmarkId::new("memory_efficiency", process_count), @@ -745,7 +663,7 @@ fn get_current_memory_usage() -> usize { if let Some(process) = system.process(pid) { process.memory() as usize } else { - // Fallback to 0 if process lookup fails + eprintln!("WARNING: Could not measure memory for PID {pid} - results will be inaccurate"); 0 } } @@ -753,7 +671,6 @@ fn get_current_memory_usage() -> usize { criterion_group!( benches, bench_process_enumeration_scale, - bench_process_enumeration_with_load, bench_single_process_collection, bench_concurrent_collection, bench_memory_usage_patterns, diff --git a/procmond/benches/serialization_benchmarks.rs b/procmond/benches/serialization_benchmarks.rs new file mode 100644 index 0000000..12a6228 --- /dev/null +++ b/procmond/benches/serialization_benchmarks.rs @@ -0,0 +1,183 @@ +//! Serialization and checksum performance benchmarks using Criterion. +//! +//! This benchmark suite measures postcard and JSON serialization performance, +//! CRC32 checksum computation, and batch serialization throughput. +//! +//! # Running benchmarks +//! +//! ```bash +//! cargo bench --package procmond --bench serialization_benchmarks +//! ``` + +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::use_debug, + clippy::uninlined_format_args, + clippy::semicolon_if_nothing_returned, + clippy::doc_markdown, + clippy::explicit_iter_loop, + clippy::pattern_type_mismatch, + clippy::shadow_reuse +)] + +#[allow(dead_code)] +#[path = "bench_helpers.rs"] +mod bench_helpers; + +use bench_helpers::{create_large_event, create_minimal_event, create_test_event}; +use collector_core::ProcessEvent; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use procmond::wal::WalEntry; +use std::hint::black_box; +use std::time::Duration; + +/// Benchmark event serialization using postcard (as used by WAL). +fn bench_serialization_postcard(c: &mut Criterion) { + let mut group = c.benchmark_group("serialization_postcard"); + + let event_types = [ + ("minimal", create_minimal_event(1)), + ("standard", create_test_event(1)), + ("large", create_large_event(1)), + ]; + + for (event_type, event) in event_types.iter() { + // Serialize benchmark + group.bench_function(BenchmarkId::new("serialize", event_type), |b| { + let entry = WalEntry::new(1, event.clone()); + b.iter(|| { + let serialized = postcard::to_allocvec(&entry).expect("Failed to serialize"); + black_box(serialized) + }); + }); + + // Deserialize benchmark + let entry = WalEntry::new(1, event.clone()); + let serialized = postcard::to_allocvec(&entry).expect("Failed to serialize"); + + group.bench_function(BenchmarkId::new("deserialize", event_type), |b| { + b.iter(|| { + let deserialized: WalEntry = + postcard::from_bytes(&serialized).expect("Failed to deserialize"); + black_box(deserialized) + }); + }); + } + + group.finish(); +} + +/// Benchmark event serialization using serde_json (for comparison and debugging). +fn bench_serialization_json(c: &mut Criterion) { + let mut group = c.benchmark_group("serialization_json"); + + let event_types = [ + ("minimal", create_minimal_event(1)), + ("standard", create_test_event(1)), + ("large", create_large_event(1)), + ]; + + for (event_type, event) in event_types.iter() { + // Serialize benchmark + group.bench_function(BenchmarkId::new("serialize", event_type), |b| { + b.iter(|| { + let serialized = serde_json::to_string(&event).expect("Failed to serialize"); + black_box(serialized) + }); + }); + + // Deserialize benchmark + let serialized = serde_json::to_string(&event).expect("Failed to serialize"); + + group.bench_function(BenchmarkId::new("deserialize", event_type), |b| { + b.iter(|| { + let deserialized: ProcessEvent = + serde_json::from_str(&serialized).expect("Failed to deserialize"); + black_box(deserialized) + }); + }); + } + + group.finish(); +} + +/// Benchmark CRC32 checksum computation (used by WAL entries). +fn bench_checksum_computation(c: &mut Criterion) { + use std::hash::Hasher; + + let mut group = c.benchmark_group("checksum_crc32c"); + + let event_types = [ + ("minimal", create_minimal_event(1)), + ("standard", create_test_event(1)), + ("large", create_large_event(1)), + ]; + + for (event_type, event) in event_types.iter() { + let serialized = postcard::to_allocvec(&event).expect("Failed to serialize"); + let data_size = serialized.len(); + + group.throughput(Throughput::Bytes(data_size as u64)); + group.bench_function(BenchmarkId::new("compute", event_type), |b| { + b.iter(|| { + let mut crc = crc32c::Crc32cHasher::new(0); + crc.write(&serialized); + let checksum = crc.finish() as u32; + black_box(checksum) + }); + }); + } + + group.finish(); +} + +/// Benchmark batch serialization throughput. +fn bench_serialization_throughput(c: &mut Criterion) { + let mut group = c.benchmark_group("serialization_throughput"); + + group.measurement_time(Duration::from_secs(10)); + group.sample_size(10); + + let batch_sizes = [100, 1000, 5000]; + + for batch_size in batch_sizes.iter() { + group.throughput(Throughput::Elements(*batch_size as u64)); + + // Postcard batch serialization + group.bench_with_input( + BenchmarkId::new("postcard_batch", batch_size), + batch_size, + |b, &batch_size| { + let events: Vec = (0..batch_size) + .map(|i| create_test_event(i as u32)) + .collect(); + + b.iter(|| { + let mut total_bytes = 0_usize; + for event in &events { + let entry = WalEntry::new(1, event.clone()); + let serialized = + postcard::to_allocvec(&entry).expect("Failed to serialize"); + total_bytes = total_bytes.saturating_add(serialized.len()); + } + black_box(total_bytes) + }); + }, + ); + } + + group.finish(); +} + +criterion_group!( + serialization_benchmarks, + bench_serialization_postcard, + bench_serialization_json, + bench_checksum_computation, + bench_serialization_throughput +); + +criterion_main!(serialization_benchmarks); diff --git a/procmond/benches/system_benchmarks.rs b/procmond/benches/system_benchmarks.rs new file mode 100644 index 0000000..466120b --- /dev/null +++ b/procmond/benches/system_benchmarks.rs @@ -0,0 +1,429 @@ +//! System-level performance benchmarks using Criterion. +//! +//! This benchmark suite covers real process collection, RPC health check +//! latency, data sanitization, combined workloads, and memory efficiency. +//! +//! # Running benchmarks +//! +//! ```bash +//! cargo bench --package procmond --bench system_benchmarks +//! ``` + +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::print_stdout, + clippy::use_debug, + clippy::uninlined_format_args, + clippy::missing_assert_message, + clippy::indexing_slicing, + clippy::semicolon_if_nothing_returned, + clippy::significant_drop_tightening, + clippy::pattern_type_mismatch, + clippy::explicit_iter_loop, + clippy::shadow_reuse, + clippy::cast_lossless, + clippy::map_unwrap_or, + clippy::redundant_closure_for_method_calls, + clippy::if_not_else, + clippy::integer_division, + clippy::items_after_statements +)] + +#[allow(dead_code)] +#[path = "bench_helpers.rs"] +mod bench_helpers; + +use bench_helpers::create_test_event; +use collector_core::ProcessEvent; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; +use std::time::{Duration, SystemTime}; +use tempfile::TempDir; +use tokio::runtime::Runtime; + +// ============================================================================ +// Process Collection Benchmarks +// ============================================================================ + +/// Benchmark process collection using real system processes. +fn bench_process_collection_real(c: &mut Criterion) { + use procmond::process_collector::{ + ProcessCollectionConfig, ProcessCollector, SysinfoProcessCollector, + }; + + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("process_collection_real"); + + // Allow longer measurement for real system collection + group.measurement_time(Duration::from_secs(10)); + group.sample_size(10); + + let configs = [ + ( + "basic", + ProcessCollectionConfig { + collect_enhanced_metadata: false, + compute_executable_hashes: false, + skip_system_processes: false, + skip_kernel_threads: false, + max_processes: 10000, + }, + ), + ( + "enhanced", + ProcessCollectionConfig { + collect_enhanced_metadata: true, + compute_executable_hashes: false, + skip_system_processes: false, + skip_kernel_threads: false, + max_processes: 10000, + }, + ), + ]; + + for (config_name, config) in configs { + group.bench_function(BenchmarkId::new("sysinfo_collector", config_name), |b| { + b.iter(|| { + rt.block_on(async { + let collector = SysinfoProcessCollector::new(config.clone()); + + let start = std::time::Instant::now(); + let result = collector.collect_processes().await; + let duration = start.elapsed(); + + if let Ok((events, stats)) = result { + let rate = events.len() as f64 / duration.as_secs_f64(); + eprintln!( + "Process collection ({}): {} processes in {:.2}ms, rate: {:.1} proc/sec", + config_name, + events.len(), + duration.as_millis(), + rate + ); + black_box((duration, events.len(), stats)); + } else { + eprintln!("Process collection failed: {:?}", result.err()); + black_box(duration); + } + }) + }); + }); + } + + group.finish(); +} + +/// Benchmark single process collection. +fn bench_process_collection_single(c: &mut Criterion) { + use procmond::process_collector::{ + ProcessCollectionConfig, ProcessCollector, SysinfoProcessCollector, + }; + + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("process_collection_single"); + + let config = ProcessCollectionConfig::default(); + + // Get current process PID for benchmark + let current_pid = std::process::id(); + + group.bench_function("collect_single_process", |b| { + b.iter(|| { + rt.block_on(async { + let collector = SysinfoProcessCollector::new(config.clone()); + + let start = std::time::Instant::now(); + let result = collector.collect_process(current_pid).await; + let duration = start.elapsed(); + + if let Ok(event) = result { + black_box((duration, event.pid)); + } else { + black_box(duration); + } + }) + }); + }); + + group.finish(); +} + +// ============================================================================ +// RPC Service Benchmarks +// ============================================================================ + +/// Benchmark RPC health check request/response latency. +/// +/// Uses a lightweight actor channel (no full collector) to measure the RPC +/// handler's request processing overhead in isolation. +fn bench_rpc_health_check_latency(c: &mut Criterion) { + use daemoneye_eventbus::rpc::{ + CollectorOperation, RpcCorrelationMetadata, RpcPayload, RpcRequest, + }; + use procmond::event_bus_connector::EventBusConnector; + use procmond::monitor_collector::{ActorHandle, ActorMessage}; + use procmond::rpc_service::RpcServiceHandler; + use std::sync::Arc; + use tokio::sync::{RwLock, mpsc}; + + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("rpc_latency"); + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + group.bench_function("health_check", |b| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + + // Create a lightweight actor channel — no full collector needed + let (tx, mut rx) = mpsc::channel::(32); + let actor_handle = ActorHandle::new(tx); + + // Drain actor messages in background so sends don't block + let drain_task = tokio::spawn(async move { while rx.recv().await.is_some() {} }); + + let connector = EventBusConnector::new(temp_dir.path().to_path_buf()) + .await + .expect("connector"); + let event_bus = Arc::new(RwLock::new(connector)); + + let handler = RpcServiceHandler::with_defaults(actor_handle.clone(), event_bus); + + let request = RpcRequest { + request_id: "bench-1".to_owned(), + client_id: "bench-client".to_owned(), + target: "control.collector.procmond".to_owned(), + operation: CollectorOperation::HealthCheck, + payload: RpcPayload::Empty, + timestamp: SystemTime::now(), + deadline: SystemTime::now() + Duration::from_secs(30), + correlation_metadata: RpcCorrelationMetadata::new("bench-corr".to_owned()), + }; + + let start = std::time::Instant::now(); + let response = handler.handle_request(request).await; + let latency = start.elapsed(); + + // Drop handler and actor_handle before awaiting drain_task + // so all senders are dropped and the drain task can terminate. + drop(handler); + drop(actor_handle); + drain_task.abort(); + + black_box((response, latency)) + }) + }); + }); + + group.finish(); +} + +/// Benchmark data sanitization throughput. +fn bench_sanitization(c: &mut Criterion) { + use procmond::security::{sanitize_command_line, sanitize_file_path}; + + let mut group = c.benchmark_group("sanitization"); + + let test_cases = [ + ( + "clean_cmd", + "app --verbose --output file.txt --debug --workers 4", + ), + ( + "sensitive_cmd", + "app --password secret123 --token test_token_value --api-key abc --verbose", + ), + ("long_cmd", &"arg ".repeat(200)), + ]; + + for (name, cmd) in &test_cases { + group.bench_function(BenchmarkId::new("command_line", name), |b| { + b.iter(|| black_box(sanitize_command_line(cmd))); + }); + } + + let path_cases = [ + ("safe_path", "/usr/bin/bash"), + ("sensitive_ssh", "/home/user/.ssh/id_rsa"), + ("sensitive_aws", "/home/user/.aws/credentials"), + ("windows_path", "C:\\Users\\admin\\.ssh\\id_rsa"), + ]; + + for (name, path) in &path_cases { + group.bench_function(BenchmarkId::new("file_path", name), |b| { + b.iter(|| black_box(sanitize_file_path(path))); + }); + } + + group.finish(); +} + +// ============================================================================ +// Combined Workload Benchmarks +// ============================================================================ + +/// Benchmark a realistic workload combining collection, serialization, and WAL writes. +fn bench_combined_workload(c: &mut Criterion) { + use procmond::event_bus_connector::{EventBusConnector, ProcessEventType}; + + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("combined_workload"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + let event_counts = [100, 500, 1000]; + + for event_count in event_counts.iter() { + group.throughput(Throughput::Elements(*event_count as u64)); + group.bench_with_input( + BenchmarkId::new("collect_serialize_write", event_count), + event_count, + |b, &event_count| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let mut connector = EventBusConnector::new(temp_dir.path().to_path_buf()) + .await + .expect("Failed to create connector"); + + let start = std::time::Instant::now(); + + // Simulate collection + publish workflow + for i in 0..event_count { + let event = create_test_event(i as u32); + connector + .publish(event, ProcessEventType::Start) + .await + .expect("Failed to publish"); + } + + let duration = start.elapsed(); + let rate = event_count as f64 / duration.as_secs_f64(); + + if event_count >= 500 { + eprintln!( + "Combined workload: {} events in {:.2}ms, rate: {:.1} events/sec", + event_count, + duration.as_millis(), + rate + ); + + // Performance budget check: > 1000 records/sec + if rate < 1000.0 { + eprintln!( + "WARNING: Combined workload rate {:.1}/sec is below 1000/sec budget", + rate + ); + } + } + + black_box((duration, rate)) + }) + }); + }, + ); + } + + group.finish(); +} + +/// Benchmark memory usage patterns during batch operations. +fn bench_memory_efficiency(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("memory_efficiency"); + + use sysinfo::{ProcessRefreshKind, RefreshKind, System}; + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); // Criterion requires at least 10 samples + + let batch_sizes = [1000, 5000]; + + for batch_size in batch_sizes.iter() { + group.throughput(Throughput::Elements(*batch_size as u64)); + group.bench_with_input( + BenchmarkId::new("event_batch_memory", batch_size), + batch_size, + |b, &batch_size| { + b.iter(|| { + rt.block_on(async { + // Measure memory before + let system_before = System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::nothing().with_memory()), + ); + let pid = sysinfo::Pid::from_u32(std::process::id()); + let memory_before = system_before + .process(pid) + .map(|p| p.memory()) + .unwrap_or_else(|| { + eprintln!("WARNING: Could not measure memory for current process"); + 0 + }); + + // Create batch of events + let events: Vec = (0..batch_size) + .map(|i| create_test_event(i as u32)) + .collect(); + + // Measure memory after + let system_after = System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::nothing().with_memory()), + ); + let memory_after = system_after + .process(pid) + .map(|p| p.memory()) + .unwrap_or_else(|| { + eprintln!("WARNING: Could not measure memory for current process"); + 0 + }); + + let memory_delta = memory_after.saturating_sub(memory_before); + let memory_per_event = if !events.is_empty() { + memory_delta / events.len() as u64 + } else { + 0 + }; + + eprintln!( + "Memory efficiency: {} events, {}KB total delta, {}B per event", + batch_size, + memory_delta / 1024, + memory_per_event + ); + + black_box((events.len(), memory_delta)) + }) + }); + }, + ); + } + + group.finish(); +} + +criterion_group!( + process_benchmarks, + bench_process_collection_real, + bench_process_collection_single +); + +criterion_group!( + rpc_benchmarks, + bench_rpc_health_check_latency, + bench_sanitization +); + +criterion_group!( + combined_benchmarks, + bench_combined_workload, + bench_memory_efficiency +); + +criterion_main!(process_benchmarks, rpc_benchmarks, combined_benchmarks); diff --git a/procmond/benches/wal_benchmarks.rs b/procmond/benches/wal_benchmarks.rs new file mode 100644 index 0000000..43b5414 --- /dev/null +++ b/procmond/benches/wal_benchmarks.rs @@ -0,0 +1,264 @@ +//! WAL (Write-Ahead Log) performance benchmarks using Criterion. +//! +//! This benchmark suite measures WAL write latency, batch throughput, +//! replay performance, and file rotation overhead. +//! +//! # Running benchmarks +//! +//! ```bash +//! cargo bench --package procmond --bench wal_benchmarks +//! ``` + +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::print_stdout, + clippy::use_debug, + clippy::uninlined_format_args, + clippy::missing_assert_message, + clippy::semicolon_if_nothing_returned, + clippy::explicit_iter_loop, + clippy::pattern_type_mismatch, + clippy::shadow_reuse, + clippy::cast_lossless, + clippy::map_unwrap_or, + clippy::redundant_closure_for_method_calls +)] + +#[allow(dead_code)] +#[path = "bench_helpers.rs"] +mod bench_helpers; + +use bench_helpers::{create_large_event, create_minimal_event, create_test_event}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use procmond::wal::WriteAheadLog; +use std::hint::black_box; +use std::time::Duration; +use tempfile::TempDir; +use tokio::runtime::Runtime; + +/// Benchmark WAL single write latency. +fn bench_wal_write_single(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("wal_write_single"); + + // Different event sizes + let event_types = [("minimal", 0), ("standard", 1), ("large", 2)]; + + for (event_type, type_id) in event_types.iter() { + group.bench_function(BenchmarkId::new("write_latency", event_type), |b| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + // Use 10MB rotation threshold for benchmarks + let wal = WriteAheadLog::with_rotation_threshold( + temp_dir.path().to_path_buf(), + 10 * 1024 * 1024, + ) + .await + .expect("Failed to create WAL"); + + let event = match type_id { + 0 => create_minimal_event(1), + 1 => create_test_event(1), + _ => create_large_event(1), + }; + + let sequence = wal.write(event).await.expect("Failed to write"); + black_box(sequence) + }) + }); + }); + } + + group.finish(); +} + +/// Benchmark WAL batch write throughput. +fn bench_wal_write_throughput(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("wal_write_throughput"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + let batch_sizes = [100, 500, 1000]; + + for batch_size in batch_sizes.iter() { + group.throughput(Throughput::Elements(*batch_size as u64)); + group.bench_with_input( + BenchmarkId::new("batch_write", batch_size), + batch_size, + |b, &batch_size| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + // Use larger rotation threshold for batch tests + let wal = WriteAheadLog::with_rotation_threshold( + temp_dir.path().to_path_buf(), + 50 * 1024 * 1024, + ) + .await + .expect("Failed to create WAL"); + + let start = std::time::Instant::now(); + for i in 0..batch_size { + let event = create_test_event(i as u32); + wal.write(event).await.expect("Failed to write"); + } + let duration = start.elapsed(); + + let rate = batch_size as f64 / duration.as_secs_f64(); + if batch_size >= 1000 { + eprintln!( + "WAL write throughput: {} events in {:.2}ms, rate: {:.1} events/sec", + batch_size, + duration.as_millis(), + rate + ); + } + + black_box((duration, rate)) + }) + }); + }, + ); + } + + group.finish(); +} + +/// Benchmark WAL replay latency. +fn bench_wal_replay(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("wal_replay"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + let event_counts = [100, 500, 1000]; + + for event_count in event_counts.iter() { + group.throughput(Throughput::Elements(*event_count as u64)); + group.bench_with_input( + BenchmarkId::new("replay_latency", event_count), + event_count, + |b, &event_count| { + // Pre-populate WAL with events before each benchmark iteration + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let wal = WriteAheadLog::with_rotation_threshold( + temp_dir.path().to_path_buf(), + 50 * 1024 * 1024, + ) + .await + .expect("Failed to create WAL"); + + // Write events + for i in 0..event_count { + let event = create_test_event(i as u32); + wal.write(event).await.expect("Failed to write"); + } + + // Measure replay + let start = std::time::Instant::now(); + let events = wal.replay().await.expect("Failed to replay"); + let duration = start.elapsed(); + + assert_eq!(events.len(), event_count); + + let rate = events.len() as f64 / duration.as_secs_f64(); + if event_count >= 1000 { + eprintln!( + "WAL replay: {} events in {:.2}ms, rate: {:.1} events/sec", + events.len(), + duration.as_millis(), + rate + ); + } + + black_box((duration, events.len())) + }) + }); + }, + ); + } + + group.finish(); +} + +/// Benchmark WAL file rotation time. +fn bench_wal_rotation(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let mut group = c.benchmark_group("wal_rotation"); + + group.measurement_time(Duration::from_secs(5)); + group.sample_size(10); + + // Use a very small rotation threshold to trigger rotation + let rotation_threshold = 10 * 1024; // 10KB - will rotate frequently + + group.bench_function("rotation_triggered", |b| { + b.iter(|| { + rt.block_on(async { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let wal = WriteAheadLog::with_rotation_threshold( + temp_dir.path().to_path_buf(), + rotation_threshold, + ) + .await + .expect("Failed to create WAL"); + + // Write events until rotation happens multiple times + // Track rotation by counting .wal files in directory + let initial_file_count = std::fs::read_dir(temp_dir.path()) + .map(|entries| { + entries + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "wal")) + .count() + }) + .unwrap_or_else(|_err| { + eprintln!("WARNING: Could not measure memory for current process"); + 0 + }); + + for i in 0..500 { + let event = create_large_event(i); + wal.write(event).await.expect("Failed to write"); + } + + // Count rotations by checking final file count + let final_file_count = std::fs::read_dir(temp_dir.path()) + .map(|entries| { + entries + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "wal")) + .count() + }) + .unwrap_or_else(|_err| { + eprintln!("WARNING: Could not measure memory for current process"); + 0 + }); + + let rotations_triggered = final_file_count.saturating_sub(initial_file_count); + black_box(rotations_triggered) + }) + }); + }); + + group.finish(); +} + +criterion_group!( + wal_benchmarks, + bench_wal_write_single, + bench_wal_write_throughput, + bench_wal_replay, + bench_wal_rotation +); + +criterion_main!(wal_benchmarks); diff --git a/procmond/src/security.rs b/procmond/src/security.rs index 8c6c6e5..a7cca4d 100644 --- a/procmond/src/security.rs +++ b/procmond/src/security.rs @@ -121,7 +121,8 @@ impl SecurityContext { /// - **Linux**: Parses `CapEff` from `/proc/self/status` and checks for /// `CAP_SYS_PTRACE` (bit 19) and `CAP_DAC_READ_SEARCH` (bit 2). /// - **macOS**: Checks `getuid() == 0` for root access. -/// - **Windows**: Checks for elevated (Administrator) privileges via `Win32_Security`. +/// - **Windows**: Currently unimplemented; always returns degraded mode. +/// See `detect_windows_privileges` and tracking issue #149. /// - **FreeBSD**: Checks `getuid() == 0` for root access (best-effort). pub fn detect_privileges() -> SecurityContext { let platform = detect_platform(); @@ -243,122 +244,19 @@ fn detect_macos_privileges() -> SecurityContext { } } -/// Windows-specific privilege detection via `Win32_Security`. +/// Windows-specific privilege detection. /// -/// Checks whether the current process token has the `SeDebugPrivilege` -/// enabled, which grants full access to other processes. +/// TODO(#149): Implement using the `token-privilege` crate once it is published. +/// See for the safe FFI wrapper +/// that will provide `is_elevated()` and `is_privilege_enabled()` without requiring +/// unsafe code in this workspace. +/// +/// Until then, Windows runs in degraded mode (no privilege detection). +/// Tracking issue: #[cfg(target_os = "windows")] -// Safety: Win32 FFI calls (OpenProcessToken, LookupPrivilegeValueW, PrivilegeCheck, -// CloseHandle) require unsafe blocks. The `windows` crate provides typed wrappers but -// the underlying foreign function invocations remain inherently unsafe. All handles are -// closed on every code path and no aliased mutable state is created. -#[allow(unsafe_code)] fn detect_windows_privileges() -> SecurityContext { - use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID}; - use windows::Win32::Security::{ - LookupPrivilegeValueW, OpenProcessToken, PRIVILEGE_SET, PrivilegeCheck, TOKEN_QUERY, - }; - use windows::Win32::System::Threading::GetCurrentProcess; - - let platform = Platform::Windows; - let mut capabilities = vec![]; - let mut has_full_process_access = false; - - // Open the current process token - let mut token_handle = HANDLE::default(); - let opened = unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle) }; - - if opened.is_err() { - warn!("Failed to open process token for privilege detection"); - return SecurityContext::degraded(platform); - } - - // Look up SeDebugPrivilege LUID - let privilege_name = windows::core::w!("SeDebugPrivilege"); - let mut luid = LUID::default(); - let lookup = unsafe { LookupPrivilegeValueW(None, privilege_name, &mut luid) }; - - if lookup.is_err() { - warn!("Failed to look up SeDebugPrivilege LUID"); - let _ = unsafe { CloseHandle(token_handle) }; - return SecurityContext::degraded(platform); - } - - // Check if the privilege is enabled - let mut privilege_set = PRIVILEGE_SET { - PrivilegeCount: 1, - Control: 1, // PRIVILEGE_SET_ALL_NECESSARY - Privilege: [windows::Win32::Security::LUID_AND_ATTRIBUTES { - Luid: luid, - Attributes: windows::Win32::Security::SE_PRIVILEGE_ENABLED, - }], - }; - - let mut result = windows::Win32::Foundation::BOOL(0); - let check = unsafe { PrivilegeCheck(token_handle, &mut privilege_set, &mut result) }; - - let _ = unsafe { CloseHandle(token_handle) }; - - if check.is_err() { - warn!("Failed to check SeDebugPrivilege"); - return SecurityContext::degraded(platform); - } - - if result.as_bool() { - capabilities.push("SeDebugPrivilege".to_owned()); - has_full_process_access = true; - } - - // Also check if running as Administrator - let is_admin = is_windows_elevated(); - if is_admin { - capabilities.push("Administrator".to_owned()); - } - - SecurityContext { - platform, - has_full_process_access, - capabilities, - degraded_mode: false, - } -} - -/// Check if the current Windows process is running elevated (as Administrator). -#[cfg(target_os = "windows")] -// Safety: Win32 FFI calls (OpenProcessToken, GetTokenInformation, CloseHandle) require -// unsafe blocks. The `windows` crate provides typed wrappers but the underlying foreign -// function invocations remain inherently unsafe. The token handle is closed on every -// code path and the TOKEN_ELEVATION struct is stack-allocated with a known fixed size. -#[allow(unsafe_code)] -fn is_windows_elevated() -> bool { - use windows::Win32::Foundation::HANDLE; - use windows::Win32::Security::{ - GetTokenInformation, OpenProcessToken, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation, - }; - use windows::Win32::System::Threading::GetCurrentProcess; - - let mut token_handle = HANDLE::default(); - let opened = unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle) }; - - if opened.is_err() { - return false; - } - - let mut elevation = TOKEN_ELEVATION::default(); - let mut return_length = 0_u32; - let info = unsafe { - GetTokenInformation( - token_handle, - TokenElevation, - Some(std::ptr::addr_of_mut!(elevation).cast()), - std::mem::size_of::() as u32, - &mut return_length, - ) - }; - - let _ = unsafe { windows::Win32::Foundation::CloseHandle(token_handle) }; - - info.is_ok() && elevation.TokenIsElevated != 0 + warn!("Windows privilege detection not yet implemented; running in degraded mode"); + SecurityContext::degraded(Platform::Windows) } /// FreeBSD-specific privilege detection.