Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_release-0-2-0-bridge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"Cargo.toml":"Minor","libs/vespera-bridge/build.gradle.kts":"Minor","libs/vespera-bridge-gradle-plugin/build.gradle.kts":"Minor"},"note":"0.2.0 / 0.3.0 release — BREAKING (0.x minor): DecodedResponse.body() returns read-only ByteBuffer (bodyBytes() copies on demand); SmartDispatchModeResolver is the autoconfigured default (DIRECT ~2.2µs / SYNC ~3.2µs for small requests, opt out via vespera.bridge.dispatch-mode=bidirectional-streaming); Gradle plugin now also publishes to the Plugin Portal. Perf: JMethodID+GlobalRef caching for streaming closures, daemon-attached dispatchAsync completion, lazy bidirectional request-pull (spawn on first body poll), JsonGenerator wire-header encoding, zero-copy get_byte_array_region input conversion. Rust: Validated 422 envelope via derive(Serialize) (byte-identical, snapshot-locked), per-invocation fs::metadata epoch caching in vespera_macro, collector clone elimination. See libs/vespera-bridge/docs/jni-before-after-2026-06-11.md for measured numbers.","date":"2026-06-12T13:00:00.000Z"}
3 changes: 2 additions & 1 deletion .changepacks/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
]
},
"publish": {
"java": "./gradlew publishToMavenCentral --stacktrace"
"java": "./gradlew publishToMavenCentral --stacktrace",
"libs/vespera-bridge-gradle-plugin/build.gradle.kts": "./gradlew publishToMavenCentral publishPlugins --stacktrace"
}
}
80 changes: 78 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Test Deploy
run: cargo publish --dry-run
- name: Doctest
# tarpaulin's --all-targets / default run never compiles doc
# tests, which let a never-passing doctest land unnoticed —
# run them explicitly before the (slow) coverage step.
run: cargo test --workspace --doc
- name: Test
run: |
# rust coverage issue
Expand All @@ -53,7 +58,7 @@ jobs:
cargo fmt
cargo tarpaulin --out Lcov Stdout --engine llvm
- name: Upload to codecov.io
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
Expand All @@ -64,7 +69,9 @@ jobs:
changepacks:
name: changepacks
runs-on: ubuntu-latest
needs: test
# jni-e2e gates publishing: a release must never ship with a broken
# JNI dispatch path on any supported OS.
needs: [test, jni-e2e]
permissions:
# create pull request comments
pull-requests: write
Expand Down Expand Up @@ -101,6 +108,75 @@ jobs:
# GPG signing (in-memory key, no keyring file)
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSWORD }}
# Gradle Plugin Portal credentials (read natively by
# com.gradle.plugin-publish for the `publishPlugins` task)
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
outputs:
changepacks: ${{ steps.changepacks.outputs.changepacks }}
release_assets_urls: ${{ steps.changepacks.outputs.release_assets_urls }}

# JNI end-to-end tests — builds the rust-jni-demo cdylib, publishes the
# vespera-bridge JAR to mavenLocal (so the demo-app Gradle plugin can
# resolve kr.devfive:vespera-bridge:0.1.1), then runs the full
# :demo-app:test suite (StreamingClosureStressTest + JNI dispatch tests)
# across all three target host OSes. This is the project's only Java/JNI
# coverage gate — until now the workflow ran zero JNI tests.
#
# Runs unconditionally on every push/PR (matching the existing CI job's
# style — no per-job paths-filter). The whole workflow already inherits
# the workflow-level `paths-ignore` for docs-only changes.
jni-e2e:
name: JNI E2E (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Build rust-jni-demo cdylib (release)
# The vespera-bridge Gradle plugin's bundleNativeLib task copies
# this cdylib from target/release into demo-app's resources, so it
# must exist before `:demo-app:test` (processResources) runs.
run: cargo build -p rust-jni-demo --release
- name: Make gradlew executable (unix)
if: runner.os != 'Windows'
run: |
chmod +x libs/vespera-bridge/gradlew
chmod +x libs/vespera-bridge-gradle-plugin/gradlew
chmod +x examples/rust-jni-demo/java/gradlew
- name: Publish vespera-bridge Gradle plugin to mavenLocal
# demo-app's plugins block resolves kr.devfive.vespera-bridge from
# mavenLocal (settings.gradle.kts pluginManagement) — the plugin is
# not on the Gradle Plugin Portal.
shell: bash
working-directory: libs/vespera-bridge-gradle-plugin
run: ./gradlew publishToMavenLocal --console=plain --no-daemon
- name: Publish vespera-bridge to mavenLocal
# demo-app resolves kr.devfive:vespera-bridge:0.1.1 from mavenLocal
# (see examples/rust-jni-demo/java/demo-app/build.gradle.kts —
# bridgeVersion.set("0.1.1")).
shell: bash
working-directory: libs/vespera-bridge
run: ./gradlew publishToMavenLocal --console=plain --no-daemon
- name: Run demo-app JNI E2E tests
# Includes StreamingClosureStressTest (1000 × 1 MiB SHA256
# bidirectional round-trip). Bench knobs are NOT propagated —
# gated bench tests stay skipped in CI.
shell: bash
working-directory: examples/rust-jni-demo/java
run: ./gradlew :demo-app:test --console=plain --no-daemon
- name: Upload demo-app test results
if: always()
uses: actions/upload-artifact@v7
with:
name: jni-e2e-${{ matrix.os }}-test-results
path: examples/rust-jni-demo/java/demo-app/build/test-results/test/*.xml
105 changes: 105 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Bench

# Criterion regression gate for the in-process dispatch hot path.
#
# - push to main: runs the gated bench groups and saves the results as
# the `main` criterion baseline in the actions cache.
# - pull_request: restores the latest main baseline and compares; the
# job FAILS when any bench regresses by more than 10% mean change
# AND the 95% confidence interval lower bound exceeds +5% (the
# double condition filters shared-runner noise).
#
# Gated groups are the stable per-request paths (wire_path,
# headers_path, resolve_path). The streaming groups are noisier
# (spawn_blocking scheduling) and are validated locally instead — see
# PERF_REPORT.md.

on:
push:
branches:
- main
paths:
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/bench.yml'
pull_request:
paths:
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/bench.yml'

concurrency:
group: bench-${{ github.ref }}
cancel-in-progress: true

env:
BENCH_FILTER: 'wire_path|headers_path|resolve_path'

jobs:
bench:
name: Criterion regression gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions-rust-lang/setup-rust-toolchain@v1

- name: Restore criterion baseline (latest main)
id: restore-baseline
uses: actions/cache/restore@v5
with:
path: target/criterion
key: bench-baseline-${{ runner.os }}-${{ github.sha }}
restore-keys: |
bench-baseline-${{ runner.os }}-

- name: Run benches and save main baseline
if: github.event_name == 'push'
run: |
cargo bench -p vespera_inprocess --bench dispatch -- \
--save-baseline main "${BENCH_FILTER}"

- name: Save criterion baseline cache
if: github.event_name == 'push'
uses: actions/cache/save@v5
with:
path: target/criterion
key: bench-baseline-${{ runner.os }}-${{ github.sha }}

- name: Compare against main baseline
if: github.event_name == 'pull_request'
run: |
if [ ! -d target/criterion ] || ! find target/criterion -maxdepth 4 -type d -name main | grep -q .; then
echo "::notice::No main baseline in cache yet — running benches without a gate."
cargo bench -p vespera_inprocess --bench dispatch -- "${BENCH_FILTER}"
exit 0
fi
cargo bench -p vespera_inprocess --bench dispatch -- \
--baseline main "${BENCH_FILTER}"

- name: Enforce regression gate
if: github.event_name == 'pull_request'
run: |
shopt -s nullglob
fail=0
found=0
while IFS= read -r f; do
found=1
mean=$(jq -r '.mean.point_estimate' "$f")
lower=$(jq -r '.mean.confidence_interval.lower_bound' "$f")
bench=$(dirname "$(dirname "$f")")
bench=${bench#target/criterion/}
printf '%s: mean %+.2f%% (CI lower %+.2f%%)\n' \
"$bench" "$(awk -v v="$mean" 'BEGIN{print v*100}')" \
"$(awk -v v="$lower" 'BEGIN{print v*100}')"
if awk -v m="$mean" -v l="$lower" 'BEGIN{exit !(m > 0.10 && l > 0.05)}'; then
echo "::error::Performance regression: ${bench} mean change exceeds +10% with CI lower bound > +5%"
fail=1
fi
done < <(find target/criterion -path '*/change/estimates.json')
if [ "$found" -eq 0 ]; then
echo "::notice::No change estimates found (first run against this baseline?) — nothing to gate."
fi
exit $fail
Loading
Loading