Skip to content

Commit 152606b

Browse files
authored
Merge pull request #136 from dev-five-git/improve-performance-2
Improve performance 2
2 parents 1ee4054 + 100d5e5 commit 152606b

413 files changed

Lines changed: 80883 additions & 40922 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Minor","crates/vespera_core/Cargo.toml":"Minor","crates/vespera_macro/Cargo.toml":"Minor","crates/vespera_inprocess/Cargo.toml":"Minor","crates/vespera_jni/Cargo.toml":"Minor","crates/vespera/Cargo.toml":"Minor","libs/vespera-bridge/build.gradle.kts":"Minor"},"note":"BREAKING (0.x minor): OpenAPI schema output is now strict OpenAPI 3.1 / JSON Schema 2020-12: nullable schemas serialize as type:[...,\"null\"] or nullable $ref anyOf, and schema-level #[schema(example = ...)] serializes as examples:[...] instead of singular example. SecurityScheme now includes OAuth/OpenID fields flows and openIdConnectUrl. vespera-bridge 0.2.0 DecodedResponse.body() returns a read-only ByteBuffer; use bodyBytes() for an owned byte[] copy.","date":"2026-06-20T00:00:00.000Z"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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"}

.changepacks/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
]
1010
},
1111
"publish": {
12-
"java": "./gradlew publishToMavenCentral --stacktrace"
12+
"java": "./gradlew publishToMavenCentral --stacktrace",
13+
"libs/vespera-bridge-gradle-plugin/build.gradle.kts": "./gradlew publishToMavenCentral publishPlugins --stacktrace"
1314
}
1415
}

.github/workflows/CI.yml

Lines changed: 176 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
image: xd009642/tarpaulin:develop-nightly
2525
options: --security-opt seccomp=unconfined
2626
steps:
27-
- uses: actions/checkout@v6
27+
- uses: actions/checkout@v7
2828
- uses: oven-sh/setup-bun@v2
2929
with:
3030
bun-version: latest
@@ -39,6 +39,11 @@ jobs:
3939
run: cargo clippy --all-targets --all-features -- -D warnings
4040
- name: Test Deploy
4141
run: cargo publish --dry-run
42+
- name: Doctest
43+
# tarpaulin's --all-targets / default run never compiles doc
44+
# tests, which let a never-passing doctest land unnoticed —
45+
# run them explicitly before the (slow) coverage step.
46+
run: cargo test --workspace --doc
4247
- name: Test
4348
run: |
4449
# rust coverage issue
@@ -53,18 +58,86 @@ jobs:
5358
cargo fmt
5459
cargo tarpaulin --out Lcov Stdout --engine llvm
5560
- name: Upload to codecov.io
56-
uses: codecov/codecov-action@v6
61+
uses: codecov/codecov-action@v7
5762
with:
5863
token: ${{ secrets.CODECOV_TOKEN }}
5964
fail_ci_if_error: true
6065
files: lcov.info
6166
if: github.ref == 'refs/heads/main'
6267

68+
# OBSERVATIONAL ONLY — this job must never gate, and its percentage must
69+
# never become a threshold. Rust's branch instrumentation is still unstable
70+
# (rust-lang/rust#79649), and rust-lang/rust#124118 lists as NOT yet
71+
# supported: match arms and or-patterns, the `?` operator, `.await`, and
72+
# any branch introduced by macro expansion — "the current implementation
73+
# discards any branch span that isn't directly visible in the function
74+
# body". vespera_macro owns roughly four fifths of the measured branches
75+
# and exists to generate code inside `quote!`, so those generated branches
76+
# are not reported as uncovered — they are dropped from the denominator.
77+
# The number therefore under-counts precisely where this codebase is most
78+
# complex, which is why it is a trend signal and not a pass/fail metric.
79+
#
80+
# vespera_jni is excluded for the same reason tarpaulin excludes it: it
81+
# cannot run without a JVM, and jni-e2e is its real coverage. Including it
82+
# reports ~4% branch and drowns out every other crate.
83+
rust-branch-coverage:
84+
name: Rust branch coverage (observational)
85+
runs-on: ubuntu-latest
86+
continue-on-error: true
87+
timeout-minutes: 30
88+
steps:
89+
- uses: actions/checkout@v7
90+
- uses: dtolnay/rust-toolchain@nightly
91+
with:
92+
components: llvm-tools-preview
93+
- uses: taiki-e/install-action@cargo-llvm-cov
94+
- name: Run instrumented tests
95+
# Allowed to fail. This pins a MOVING nightly, and toolchain drift
96+
# breaks tests that assert compiler output — the trybuild UI suite
97+
# blesses its .stderr files against stable, so a nightly diagnostic
98+
# reword fails it with nothing actually broken. The profraw data is
99+
# still written, so the report step below runs regardless.
100+
continue-on-error: true
101+
run: cargo llvm-cov --branch --workspace --no-fail-fast --no-report
102+
- name: Summarise branch coverage
103+
run: |
104+
# `--branch` belongs on the instrumented RUN above, not here: the
105+
# profdata already carries branch counters, and `report` rejects
106+
# the flag.
107+
cargo llvm-cov report --summary-only \
108+
--ignore-filename-regex '(benches|examples|vespera_jni)' \
109+
| tee branch-coverage.txt
110+
{
111+
echo '### Rust branch coverage (observational)'
112+
echo
113+
echo 'Excludes benches, examples and `vespera_jni` (JVM-only — covered by jni-e2e).'
114+
echo 'Not a gate: see the job comment in CI.yml for why this number under-counts.'
115+
echo
116+
echo '```'
117+
cat branch-coverage.txt
118+
echo '```'
119+
} >> "$GITHUB_STEP_SUMMARY"
120+
- name: Generate lcov
121+
if: always()
122+
run: |
123+
cargo llvm-cov report --lcov --output-path branch-lcov.info \
124+
--ignore-filename-regex '(benches|examples|vespera_jni)'
125+
- name: Upload branch coverage report
126+
if: always()
127+
uses: actions/upload-artifact@v7
128+
with:
129+
name: rust-branch-coverage
130+
path: |
131+
branch-coverage.txt
132+
branch-lcov.info
133+
63134
# publish
64135
changepacks:
65136
name: changepacks
66137
runs-on: ubuntu-latest
67-
needs: test
138+
# jni-e2e gates publishing: a release must never ship with a broken
139+
# JNI dispatch path on any supported OS.
140+
needs: [test, jni-e2e]
68141
permissions:
69142
# create pull request comments
70143
pull-requests: write
@@ -76,7 +149,7 @@ jobs:
76149
# Publish to GitHub Packages
77150
packages: write
78151
steps:
79-
- uses: actions/checkout@v6
152+
- uses: actions/checkout@v7
80153
- uses: actions/setup-java@v5
81154
with:
82155
distribution: 'temurin'
@@ -101,6 +174,105 @@ jobs:
101174
# GPG signing (in-memory key, no keyring file)
102175
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }}
103176
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSWORD }}
177+
# Gradle Plugin Portal credentials (read natively by
178+
# com.gradle.plugin-publish for the `publishPlugins` task)
179+
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
180+
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
104181
outputs:
105182
changepacks: ${{ steps.changepacks.outputs.changepacks }}
106183
release_assets_urls: ${{ steps.changepacks.outputs.release_assets_urls }}
184+
185+
# Java gate — builds the rust-jni-demo cdylib, runs the vespera-bridge unit
186+
# suite, publishes the bridge JAR to mavenLocal (so the demo-app Gradle
187+
# plugin can resolve kr.devfive:vespera-bridge), then runs the full
188+
# :demo-app:test suite (StreamingClosureStressTest + JNI dispatch tests)
189+
# across all three target host OSes.
190+
#
191+
# This is the ONLY thing that exercises Java in this repository, and it is
192+
# also what covers `crates/vespera_jni`: that crate is excluded from
193+
# tarpaulin (`#![cfg(not(tarpaulin_include))]`) because its symbols cannot
194+
# run without a JVM, so this job is its coverage — not the Rust line
195+
# coverage number.
196+
#
197+
# Runs unconditionally on every push/PR (matching the existing CI job's
198+
# style — no per-job paths-filter). The whole workflow already inherits
199+
# the workflow-level `paths-ignore` for docs-only changes.
200+
jni-e2e:
201+
name: JNI E2E (${{ matrix.os }})
202+
runs-on: ${{ matrix.os }}
203+
timeout-minutes: 25
204+
strategy:
205+
fail-fast: false
206+
matrix:
207+
os: [ubuntu-latest, windows-latest, macos-latest]
208+
steps:
209+
- uses: actions/checkout@v7
210+
- uses: actions/setup-java@v5
211+
with:
212+
distribution: 'temurin'
213+
java-version: '17'
214+
cache: 'gradle'
215+
- uses: actions-rust-lang/setup-rust-toolchain@v1
216+
- name: Build rust-jni-demo cdylib (release)
217+
# The vespera-bridge Gradle plugin's bundleNativeLib task copies
218+
# this cdylib from target/release into demo-app's resources, so it
219+
# must exist before `:demo-app:test` (processResources) runs.
220+
run: cargo build -p rust-jni-demo --release
221+
- name: Make gradlew executable (unix)
222+
if: runner.os != 'Windows'
223+
run: |
224+
chmod +x libs/vespera-bridge/gradlew
225+
chmod +x libs/vespera-bridge-gradle-plugin/gradlew
226+
chmod +x examples/rust-jni-demo/java/gradlew
227+
- name: Publish vespera-bridge Gradle plugin to mavenLocal
228+
# demo-app's plugins block resolves kr.devfive.vespera-bridge from
229+
# mavenLocal (settings.gradle.kts pluginManagement) — the plugin is
230+
# not on the Gradle Plugin Portal.
231+
shell: bash
232+
working-directory: libs/vespera-bridge-gradle-plugin
233+
run: ./gradlew publishToMavenLocal --console=plain --no-daemon
234+
- name: Run vespera-bridge unit tests
235+
# The bridge's own suite: wire codec, zero-copy header reader, direct
236+
# buffer pool, hop-by-hop header policy, dispatch-mode resolvers and
237+
# the Spring autoconfiguration. Until now the workflow only PUBLISHED
238+
# this library, so none of these ever ran in CI — the Rust side was
239+
# gated while ~2 kLOC of Java shipped untested. Runs before the publish
240+
# step so a broken bridge fails here rather than as a confusing
241+
# demo-app failure. Bench knobs are NOT propagated, so the
242+
# `vespera.bench`-gated PerfAllocBench cases stay skipped.
243+
shell: bash
244+
working-directory: libs/vespera-bridge
245+
run: ./gradlew test --console=plain --no-daemon
246+
- name: Publish vespera-bridge to mavenLocal
247+
# demo-app resolves kr.devfive:vespera-bridge from mavenLocal at the
248+
# version declared in libs/vespera-bridge/build.gradle.kts (see
249+
# examples/rust-jni-demo/java/demo-app/build.gradle.kts — the version
250+
# is read from that file, never pinned, so bumping the bridge cannot
251+
# silently fall back to the last release on Maven Central).
252+
shell: bash
253+
working-directory: libs/vespera-bridge
254+
run: ./gradlew publishToMavenLocal --console=plain --no-daemon
255+
- name: Run demo-app JNI E2E tests
256+
# Includes StreamingClosureStressTest (1000 × 1 MiB SHA256
257+
# bidirectional round-trip). Bench knobs are NOT propagated —
258+
# gated bench tests stay skipped in CI.
259+
shell: bash
260+
working-directory: examples/rust-jni-demo/java
261+
run: ./gradlew :demo-app:test --console=plain --no-daemon
262+
- name: Merge Java coverage
263+
# Regenerates the bridge's JaCoCo report now that demo-app has run, so
264+
# it also credits the classes only reachable with a loaded cdylib — the
265+
# JNI wrappers and the Spring proxy, which the unit suite cannot drive.
266+
# The report task reads demo-app/build/jacoco/test.exec when present.
267+
shell: bash
268+
working-directory: libs/vespera-bridge
269+
run: ./gradlew jacocoTestReport --console=plain --no-daemon
270+
- name: Upload Java test results and coverage
271+
if: always()
272+
uses: actions/upload-artifact@v7
273+
with:
274+
name: jni-e2e-${{ matrix.os }}-test-results
275+
path: |
276+
examples/rust-jni-demo/java/demo-app/build/test-results/test/*.xml
277+
libs/vespera-bridge/build/test-results/test/*.xml
278+
libs/vespera-bridge/build/reports/jacoco/test/jacocoTestReport.xml

.github/workflows/bench.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Bench
2+
3+
# Criterion regression gate for the in-process dispatch hot path.
4+
#
5+
# - push to main: runs the gated bench groups and saves the results as
6+
# the `main` criterion baseline in the actions cache.
7+
# - pull_request: restores the latest main baseline and compares; the
8+
# job FAILS when any bench regresses by more than 10% mean change
9+
# AND the 95% confidence interval lower bound exceeds +5% (the
10+
# double condition filters shared-runner noise).
11+
#
12+
# Gated groups are the stable per-request paths (wire_path,
13+
# headers_path, request_headers_path, resolve_path, dispatch_path). The
14+
# streaming and contended groups are noisier (spawn_blocking / scheduler
15+
# timing) and the router_path setup micro-bench is low-signal, so those
16+
# are validated locally instead — see PERF_REPORT.md.
17+
#
18+
# This TIMING gate fires only at a loose ±10% (shared-runner drift), so it
19+
# catches BIG regressions. Small, deterministic ALLOCATION regressions are
20+
# caught noise-free by the `alloc_budget` integration test (a counting
21+
# global allocator asserting exact per-dispatch allocation budgets) in the
22+
# normal `cargo test` job — the two gates are complementary.
23+
24+
on:
25+
push:
26+
branches:
27+
- main
28+
paths:
29+
- 'crates/**'
30+
- 'Cargo.toml'
31+
- 'Cargo.lock'
32+
- '.github/workflows/bench.yml'
33+
pull_request:
34+
paths:
35+
- 'crates/**'
36+
- 'Cargo.toml'
37+
- 'Cargo.lock'
38+
- '.github/workflows/bench.yml'
39+
40+
concurrency:
41+
group: bench-${{ github.ref }}
42+
cancel-in-progress: true
43+
44+
env:
45+
BENCH_FILTER: 'wire_path|headers_path|request_headers_path|resolve_path|dispatch_path'
46+
47+
jobs:
48+
bench:
49+
name: Criterion regression gate
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v7
53+
54+
- uses: actions-rust-lang/setup-rust-toolchain@v1
55+
56+
- name: Restore criterion baseline (latest main)
57+
id: restore-baseline
58+
uses: actions/cache/restore@v6
59+
with:
60+
path: target/criterion
61+
key: bench-baseline-${{ runner.os }}-${{ github.sha }}
62+
restore-keys: |
63+
bench-baseline-${{ runner.os }}-
64+
65+
- name: Run benches and save main baseline
66+
if: github.event_name == 'push'
67+
run: |
68+
cargo bench -p vespera_inprocess --bench dispatch -- \
69+
--save-baseline main "${BENCH_FILTER}"
70+
71+
- name: Save criterion baseline cache
72+
if: github.event_name == 'push'
73+
uses: actions/cache/save@v6
74+
with:
75+
path: target/criterion
76+
key: bench-baseline-${{ runner.os }}-${{ github.sha }}
77+
78+
- name: Compare against main baseline
79+
if: github.event_name == 'pull_request'
80+
run: |
81+
if [ ! -d target/criterion ] || ! find target/criterion -maxdepth 4 -type d -name main | grep -q .; then
82+
echo "::notice::No main baseline in cache yet — running benches without a gate."
83+
cargo bench -p vespera_inprocess --bench dispatch -- "${BENCH_FILTER}"
84+
exit 0
85+
fi
86+
cargo bench -p vespera_inprocess --bench dispatch -- \
87+
--baseline main "${BENCH_FILTER}"
88+
89+
- name: Enforce regression gate
90+
if: github.event_name == 'pull_request'
91+
run: |
92+
shopt -s nullglob
93+
fail=0
94+
found=0
95+
while IFS= read -r f; do
96+
found=1
97+
mean=$(jq -r '.mean.point_estimate' "$f")
98+
lower=$(jq -r '.mean.confidence_interval.lower_bound' "$f")
99+
bench=$(dirname "$(dirname "$f")")
100+
bench=${bench#target/criterion/}
101+
printf '%s: mean %+.2f%% (CI lower %+.2f%%)\n' \
102+
"$bench" "$(awk -v v="$mean" 'BEGIN{print v*100}')" \
103+
"$(awk -v v="$lower" 'BEGIN{print v*100}')"
104+
if awk -v m="$mean" -v l="$lower" 'BEGIN{exit !(m > 0.10 && l > 0.05)}'; then
105+
echo "::error::Performance regression: ${bench} mean change exceeds +10% with CI lower bound > +5%"
106+
fail=1
107+
fi
108+
done < <(find target/criterion -path '*/change/estimates.json')
109+
if [ "$found" -eq 0 ]; then
110+
echo "::notice::No change estimates found (first run against this baseline?) — nothing to gate."
111+
fi
112+
exit $fail

.github/workflows/deploy-pages.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,26 @@ jobs:
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: Checkout
26-
uses: actions/checkout@v6
26+
uses: actions/checkout@v7
2727

2828
- name: Install bun
2929
uses: oven-sh/setup-bun@v2
3030

3131
- name: Cache bun dependencies
32-
uses: actions/cache@v5
32+
uses: actions/cache@v6
3333
with:
3434
path: ~/.bun/install/cache
3535
key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
3636
restore-keys: |
3737
bun-${{ runner.os }}-
3838
3939
- name: Install Node.js
40-
uses: actions/setup-node@v6
40+
uses: actions/setup-node@v7
4141
with:
4242
node-version: 22
4343

4444
- name: Cache Next.js build
45-
uses: actions/cache@v5
45+
uses: actions/cache@v6
4646
with:
4747
path: |
4848
apps/landing/.next/cache

0 commit comments

Comments
 (0)