From caaad2477be4c9c4b053d1d7fe56932f82b3fc28 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 10:56:41 +0800 Subject: [PATCH 1/9] feat(bench): expose internal API for criterion benchmarks --- src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index f75bb53..3b3e418 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,9 @@ pub mod __test_api { #[cfg(target_arch = "aarch64")] pub use crate::scan::neon::NeonScanner; } + +#[doc(hidden)] +pub mod __bench_api { + pub use crate::doc::Document; + pub use crate::options::{Options, QJSON_MODE_LAZY}; +} From 2bf03221c0353bcea79c7f1e26625e7f2ef9a6bb Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 10:57:55 +0800 Subject: [PATCH 2/9] build: add criterion benchmark dependency --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d11d64b..e8ce7b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,13 @@ once_cell = "1" proptest = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +criterion = { version = "0.5", features = ["html_reports"] } [profile.release] opt-level = 3 lto = "thin" codegen-units = 1 + +[[bench]] +name = "rust_bench" +harness = false From a8ecb12a818d833d1ae042179153c830ed607f1f Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 10:59:35 +0800 Subject: [PATCH 3/9] feat(bench): add criterion benchmarks for parse and field access --- benches/rust_bench.rs | 118 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 benches/rust_bench.rs diff --git a/benches/rust_bench.rs b/benches/rust_bench.rs new file mode 100644 index 0000000..54e62a7 --- /dev/null +++ b/benches/rust_bench.rs @@ -0,0 +1,118 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId}; +use qjson::__bench_api::{Document, Options, QJSON_MODE_LAZY}; +use std::fs; + +fn read_fixture(path: &str) -> Vec { + fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {}", path, e)) +} + +struct Fixture { + name: &'static str, + #[allow(dead_code)] + path: &'static str, + data: Vec, +} + +fn load_fixtures() -> Vec { + vec![ + Fixture { + name: "small_api", + path: "benches/fixtures/small_api.json", + data: read_fixture("benches/fixtures/small_api.json"), + }, + Fixture { + name: "wide_object", + path: "tests/fixtures/data/wide_object.json", + data: read_fixture("tests/fixtures/data/wide_object.json"), + }, + Fixture { + name: "deep_nesting", + path: "tests/fixtures/data/deep_nesting.json", + data: read_fixture("tests/fixtures/data/deep_nesting.json"), + }, + ] +} + +fn bench_parse_eager(c: &mut Criterion) { + let fixtures = load_fixtures(); + let mut group = c.benchmark_group("parse_eager"); + + for f in &fixtures { + group.bench_with_input(BenchmarkId::new("parse", f.name), &f.data, |b, data| { + b.iter(|| Document::parse(black_box(data)).unwrap()) + }); + } + + group.finish(); +} + +fn bench_parse_lazy(c: &mut Criterion) { + let fixtures = load_fixtures(); + let opts = Options { mode: QJSON_MODE_LAZY, max_depth: 0 }; + let mut group = c.benchmark_group("parse_lazy"); + + for f in &fixtures { + group.bench_with_input(BenchmarkId::new("parse", f.name), &f.data, |b, data| { + b.iter(|| Document::parse_with_options(black_box(data), &opts).unwrap()) + }); + } + + group.finish(); +} + +fn bench_field_access(c: &mut Criterion) { + let small = read_fixture("benches/fixtures/small_api.json"); + let doc = Document::parse(&small).unwrap(); + let mut group = c.benchmark_group("field_access"); + + group.bench_function("get_str/model", |b| { + b.iter(|| { + let mut out_ptr = std::ptr::null(); + let mut out_len = 0usize; + unsafe { + qjson::ffi::qjson_get_str( + &doc as *const _ as *mut _, + b"model".as_ptr() as *const _, + 5, + &mut out_ptr, + &mut out_len, + ) + } + }) + }); + + group.bench_function("get_f64/max_tokens", |b| { + b.iter(|| { + let mut out = 0f64; + unsafe { + qjson::ffi::qjson_get_f64( + &doc as *const _ as *mut _, + b"max_tokens".as_ptr() as *const _, + 10, + &mut out, + ) + } + }) + }); + + group.bench_function("get_str/nested", |b| { + b.iter(|| { + let mut out_ptr = std::ptr::null(); + let mut out_len = 0usize; + unsafe { + qjson::ffi::qjson_get_str( + &doc as *const _ as *mut _, + b"messages[0].role".as_ptr() as *const _, + 16, + &mut out_ptr, + &mut out_len, + ) + } + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_parse_eager, bench_parse_lazy, bench_field_access); +criterion_main!(benches); From a813f05ece93b56935577aaa4d623afe39d38d41 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:01:22 +0800 Subject: [PATCH 4/9] ci: add benchmark regression detection workflow --- .github/workflows/bench.yml | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/bench.yml diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..4f458e2 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,58 @@ +name: Benchmark + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: bench-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + benchmark: + name: Performance regression check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Rust (stable) + run: | + rustup toolchain install stable --profile minimal --no-self-update + rustup default stable + + - name: Cache cargo registry & target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: bench-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }} + restore-keys: | + bench-${{ runner.os }}- + + - name: Run benchmarks + run: cargo bench --bench rust_bench -- --output-format bencher | tee output.txt + + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + tool: cargo + output-file-path: output.txt + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.ref == 'refs/heads/main' }} + comment-on-alert: true + alert-threshold: '105%' + fail-on-alert: false + gh-pages-branch: gh-pages + benchmark-data-dir-path: dev/bench From 3ceabde04c8f4d236c9fbad97e4631ac978aab06 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:02:51 +0800 Subject: [PATCH 5/9] build: add bench-rust Makefile target --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b822cca..f8d9fee 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ else LUA_ENV := LD_LIBRARY_PATH=$(LIB_DIR) LUA_PATH='$(LUA_PATH)' LUA_CPATH='$(LUA_CPATH)' endif -.PHONY: help build test lua-property-test lua-mutation-property-test lint lua-lint bench-smoke bench clean +.PHONY: help build test lua-property-test lua-mutation-property-test lint lua-lint bench-smoke bench bench-rust clean help: ## Show this help @# FS uses [^#]* (not .*) so a description containing `##` isn't truncated. @@ -65,6 +65,9 @@ bench: bench-smoke build vendor/lua-cjson/cjson.so ## Run each scenario in a fre $(LUA_ENV) $(RESTY) benches/lua_bench.lua $$s; \ done +bench-rust: build ## Run Rust criterion benchmarks + cargo bench --bench rust_bench + vendor/lua-cjson/cjson.so: | vendor/lua-cjson/Makefile ifeq ($(shell uname),Darwin) $(MAKE) -C vendor/lua-cjson PREFIX=$(LUAJIT_PREFIX) LUA_INCLUDE_DIR=$(LUAJIT_INC) LUA=$(LUAJIT) CJSON_LDFLAGS="-bundle -undefined dynamic_lookup" From 8cc5f56ae70920970cc76de6f06252fe5c5663c5 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:13:50 +0800 Subject: [PATCH 6/9] fix: address code review feedback - Remove unused submodules checkout option - Add comments explaining workflow permissions - Add SAFETY comments for unsafe FFI calls in benchmarks - Add doc comment explaining field_access benchmarks measure FFI overhead --- .github/workflows/bench.yml | 6 ++---- benches/rust_bench.rs | 6 ++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 4f458e2..1025734 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -6,8 +6,8 @@ on: pull_request: permissions: - contents: write - pull-requests: write + contents: write # needed for auto-push to gh-pages on main branch + pull-requests: write # needed for posting benchmark regression comments concurrency: group: bench-${{ github.event.pull_request.number || github.ref }} @@ -22,8 +22,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - submodules: recursive - name: Install Rust (stable) run: | diff --git a/benches/rust_bench.rs b/benches/rust_bench.rs index 54e62a7..2b1e8ef 100644 --- a/benches/rust_bench.rs +++ b/benches/rust_bench.rs @@ -60,6 +60,8 @@ fn bench_parse_lazy(c: &mut Criterion) { group.finish(); } +/// Field access benchmarks measure FFI overhead by calling through the C ABI surface, +/// not just Rust internals. This reflects real-world usage from LuaJIT. fn bench_field_access(c: &mut Criterion) { let small = read_fixture("benches/fixtures/small_api.json"); let doc = Document::parse(&small).unwrap(); @@ -69,6 +71,8 @@ fn bench_field_access(c: &mut Criterion) { b.iter(|| { let mut out_ptr = std::ptr::null(); let mut out_len = 0usize; + // SAFETY: doc ptr is valid (borrowed from stack), path ptr+len are valid + // (static byte string), out-pointers are non-null and writable. unsafe { qjson::ffi::qjson_get_str( &doc as *const _ as *mut _, @@ -84,6 +88,7 @@ fn bench_field_access(c: &mut Criterion) { group.bench_function("get_f64/max_tokens", |b| { b.iter(|| { let mut out = 0f64; + // SAFETY: same as above - doc, path, and out-pointer are all valid. unsafe { qjson::ffi::qjson_get_f64( &doc as *const _ as *mut _, @@ -99,6 +104,7 @@ fn bench_field_access(c: &mut Criterion) { b.iter(|| { let mut out_ptr = std::ptr::null(); let mut out_len = 0usize; + // SAFETY: same as above - doc, path, and out-pointers are all valid. unsafe { qjson::ffi::qjson_get_str( &doc as *const _ as *mut _, From 46a3e74d4a0729236127bfe858eddd5aeffef40b Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:18:03 +0800 Subject: [PATCH 7/9] ci: add fallback to create gh-pages branch if missing --- .github/workflows/bench.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 1025734..aa0ad1f 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -42,6 +42,17 @@ jobs: - name: Run benchmarks run: cargo bench --bench rust_bench -- --output-format bencher | tee output.txt + - name: Ensure gh-pages branch exists + run: | + if ! git ls-remote --exit-code --heads origin gh-pages; then + echo "Creating gh-pages branch..." + git checkout --orphan gh-pages + git reset --hard + git commit --allow-empty -m "Initialize gh-pages branch for benchmark data" + git push origin gh-pages + git checkout - + fi + - name: Store benchmark result uses: benchmark-action/github-action-benchmark@v1 with: From f07ee4ca031c94a8e398f3c2d7f2158cf47eae45 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:19:27 +0800 Subject: [PATCH 8/9] chore: enable CodeRabbit review for draft PRs --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..db6fcbc --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,3 @@ +reviews: + request_changes_workflow: false + drafts: true From c2fd9c11c448917c31aa2f82c051136c8fd50234 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Tue, 2 Jun 2026 11:23:56 +0800 Subject: [PATCH 9/9] ci: reset Cargo.lock after benchmarks to allow gh-pages switch --- .github/workflows/bench.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index aa0ad1f..8f292fc 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -42,6 +42,9 @@ jobs: - name: Run benchmarks run: cargo bench --bench rust_bench -- --output-format bencher | tee output.txt + - name: Reset Cargo.lock changes + run: git checkout -- Cargo.lock + - name: Ensure gh-pages branch exists run: | if ! git ls-remote --exit-code --heads origin gh-pages; then