├── presets ├── stores │ ├── contrie.toml │ ├── dashmap.toml │ ├── flurry.toml │ ├── null.toml │ ├── papaya.toml │ ├── chashmap.toml │ ├── null_async.toml │ ├── scchashmap.toml │ ├── mutex_btreemap.toml │ ├── rwlock_btreemap.toml │ ├── mutex_hashmap.toml │ ├── rwlock_hashmap.toml │ └── remote.toml └── benchmarks │ ├── example_scan.toml │ └── example.toml ├── src ├── main.rs ├── stores │ ├── dashmap.rs │ ├── chashmap.rs │ ├── contrie.rs │ ├── flurry.rs │ ├── papaya.rs │ ├── scc.rs │ ├── remote.rs │ ├── rocksdb.rs │ ├── null.rs │ ├── remotereplicated.rs │ ├── btreemap.rs │ ├── remotesharded.rs │ └── hashmap.rs ├── thread.rs ├── cmdline.rs ├── stores.rs ├── lib.rs ├── workload.rs ├── server.rs └── bench.rs ├── examples ├── your-kv-store │ ├── your-kv-store.toml │ ├── Cargo.toml │ ├── benchmark.toml │ ├── README.md │ └── src │ │ └── main.rs ├── mixed │ ├── mixed.pdf │ ├── mixed.png │ ├── run.sh │ ├── plot.gpl │ ├── mixed.toml │ └── README.md ├── ycsbd │ ├── ycsbd.pdf │ ├── ycsbd.png │ ├── ycsbd.toml │ ├── plot.gpl │ ├── run.sh │ └── README.md ├── writeheavy │ ├── writeheavy.pdf │ ├── writeheavy.png │ ├── writeheavy.toml │ ├── plot.gpl │ ├── run.sh │ └── README.md ├── readpopular │ ├── readpopular.pdf │ ├── readpopular.png │ ├── readpopular.toml │ ├── plot.gpl │ ├── run.sh │ └── README.md └── latency │ ├── latency-writeheavy.pdf │ ├── latency-writeheavy.png │ ├── latency-readpopular.pdf │ ├── latency-readpopular.png │ ├── README.md │ ├── run.sh │ └── plot.gpl ├── .gitignore ├── .github └── workflows │ └── test.yml ├── README.md ├── LICENSE └── Cargo.toml /presets/stores/contrie.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "contrie" 3 | -------------------------------------------------------------------------------- /presets/stores/dashmap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "dashmap" 3 | -------------------------------------------------------------------------------- /presets/stores/flurry.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "flurry" 3 | -------------------------------------------------------------------------------- /presets/stores/null.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "nullmap" 3 | -------------------------------------------------------------------------------- /presets/stores/papaya.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "papaya" 3 | -------------------------------------------------------------------------------- /presets/stores/chashmap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "chashmap" 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | kvbench::cmdline(); 3 | } 4 | -------------------------------------------------------------------------------- /presets/stores/null_async.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "nullmap_async" 3 | -------------------------------------------------------------------------------- /presets/stores/scchashmap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "scchashmap" 3 | -------------------------------------------------------------------------------- /presets/stores/mutex_btreemap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "mutex_btreemap" 3 | -------------------------------------------------------------------------------- /examples/your-kv-store/your-kv-store.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "your_kv_store" 3 | -------------------------------------------------------------------------------- /presets/stores/rwlock_btreemap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "rwlock_btreemap" 3 | -------------------------------------------------------------------------------- /presets/stores/mutex_hashmap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "mutex_hashmap" 3 | shards = 1024 4 | -------------------------------------------------------------------------------- /presets/stores/rwlock_hashmap.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "rwlock_hashmap" 3 | shards = 1024 4 | -------------------------------------------------------------------------------- /examples/mixed/mixed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/mixed/mixed.pdf -------------------------------------------------------------------------------- /examples/mixed/mixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/mixed/mixed.png -------------------------------------------------------------------------------- /examples/ycsbd/ycsbd.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/ycsbd/ycsbd.pdf -------------------------------------------------------------------------------- /examples/ycsbd/ycsbd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/ycsbd/ycsbd.png -------------------------------------------------------------------------------- /presets/stores/remote.toml: -------------------------------------------------------------------------------- 1 | [map] 2 | name = "remotemap" 3 | host = "127.0.0.1" 4 | port = "9000" 5 | -------------------------------------------------------------------------------- /examples/writeheavy/writeheavy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/writeheavy/writeheavy.pdf -------------------------------------------------------------------------------- /examples/writeheavy/writeheavy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/writeheavy/writeheavy.png -------------------------------------------------------------------------------- /examples/readpopular/readpopular.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/readpopular/readpopular.pdf -------------------------------------------------------------------------------- /examples/readpopular/readpopular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/readpopular/readpopular.png -------------------------------------------------------------------------------- /examples/latency/latency-writeheavy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/latency/latency-writeheavy.pdf -------------------------------------------------------------------------------- /examples/latency/latency-writeheavy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/latency/latency-writeheavy.png -------------------------------------------------------------------------------- /examples/latency/latency-readpopular.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/latency/latency-readpopular.pdf -------------------------------------------------------------------------------- /examples/latency/latency-readpopular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdroychan/kvbench/HEAD/examples/latency/latency-readpopular.png -------------------------------------------------------------------------------- /examples/your-kv-store/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "your-kv-store" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kvbench = { path = "../.." } 8 | -------------------------------------------------------------------------------- /examples/ycsbd/ycsbd.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 1 3 | repeat = 1 4 | klen = 8 5 | vlen = 16 6 | kmin = 0 7 | kmax = 1000000 8 | 9 | [[benchmark]] 10 | set_perc = 100 11 | repeat = 1 12 | dist = "shufflep" 13 | report = "hidden" 14 | 15 | [[benchmark]] 16 | timeout = 3 17 | set_perc = 5 18 | get_perc = 95 19 | dist = "latest" 20 | report = "finish" 21 | -------------------------------------------------------------------------------- /examples/writeheavy/writeheavy.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 1 3 | repeat = 1 4 | klen = 8 5 | vlen = 16 6 | kmin = 0 7 | kmax = 1000000 8 | 9 | [[benchmark]] 10 | set_perc = 100 11 | repeat = 1 12 | dist = "incrementp" 13 | report = "hidden" 14 | 15 | [[benchmark]] 16 | timeout = 1 17 | set_perc = 50 18 | get_perc = 50 19 | dist = "uniform" 20 | report = "finish" 21 | -------------------------------------------------------------------------------- /examples/readpopular/readpopular.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 1 3 | repeat = 1 4 | klen = 8 5 | vlen = 16 6 | kmin = 0 7 | kmax = 1000000 8 | 9 | [[benchmark]] 10 | set_perc = 100 11 | repeat = 1 12 | dist = "incrementp" 13 | report = "hidden" 14 | 15 | [[benchmark]] 16 | timeout = 1 17 | get_perc = 100 18 | dist = "zipfian" 19 | zipf_theta = 1.0 20 | report = "finish" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /examples/your-kv-store/benchmark.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 8 3 | repeat = 5 4 | klen = 8 5 | vlen = 16 6 | kmin = 0 7 | kmax = 1000 8 | 9 | [[benchmark]] 10 | set_perc = 100 11 | repeat = 1 12 | dist = "incrementp" 13 | report = "hidden" 14 | 15 | [[benchmark]] 16 | timeout = 1.0 17 | set_perc = 50 18 | get_perc = 50 19 | dist = "zipfian" 20 | report = "repeat" 21 | 22 | [[benchmark]] 23 | timeout = 1.0 24 | set_perc = 50 25 | get_perc = 50 26 | dist = "uniform" 27 | report = "repeat" 28 | -------------------------------------------------------------------------------- /presets/benchmarks/example_scan.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 8 3 | repeat = 5 4 | qd = 100 5 | batch = 10 6 | scan_n = 10 7 | klen = 8 8 | vlen = 16 9 | kmin = 0 10 | kmax = 10000 11 | 12 | [[benchmark]] 13 | set_perc = 100 14 | repeat = 1 15 | dist = "incrementp" 16 | 17 | [[benchmark]] 18 | timeout = 0.2 19 | set_perc = 50 20 | get_perc = 25 21 | scan_perc = 25 22 | dist = "zipfian" 23 | 24 | [[benchmark]] 25 | timeout = 0.2 26 | set_perc = 50 27 | get_perc = 25 28 | scan_perc = 25 29 | dist = "uniform" 30 | -------------------------------------------------------------------------------- /examples/latency/README.md: -------------------------------------------------------------------------------- 1 | This example shows the latency measurement of the [writeheavy](../writeheavy/) 2 | and [readpopular](../readpopular/) examples. Their descriptions can be found in their own 3 | directories. 4 | 5 | AMD Ryzen 9 5950X CPU 0-15 results (writeheavy, [pdf](latency-writeheavy.pdf)): 6 | 7 | ![latency-writeheavy](latency-writeheavy.png) 8 | 9 | 10 | AMD Ryzen 9 5950X CPU 0-15 results (readpopular, [pdf](latency-readpopular.pdf)): 11 | 12 | ![latency-readpopular](latency-readpopular.png) 13 | -------------------------------------------------------------------------------- /examples/your-kv-store/README.md: -------------------------------------------------------------------------------- 1 | This example shows how to integrate `kvbench` into your own key-value store implementations. 2 | 3 | To compile, simply use: 4 | 5 | ``` 6 | cargo build --release 7 | ``` 8 | 9 | Then you can run the benchmark binary with this newly added key-value store: 10 | 11 | ``` 12 | ./target/release/your-kv-store bench -s your-kv-store.toml -b benchmark.toml 13 | ``` 14 | 15 | Or you can also start a server on it: 16 | 17 | ``` 18 | ./target/release/your-kv-store server -s your-kv-store.toml 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/mixed/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | 5 | cargo +stable build --profile release-lto --all-features 6 | 7 | STORE_DIR=$DIR/../../presets/stores 8 | BENCHMARK=$DIR/mixed.toml 9 | 10 | STORES="chashmap contrie dashmap flurry papaya scchashmap mutex_hashmap rwlock_hashmap" 11 | 12 | for s in $STORES; do 13 | echo $s 14 | rm $s.txt 2>/dev/null 15 | env global.threads=32 \ 16 | cargo +stable run --profile release-lto --all-features -- \ 17 | bench -s $STORE_DIR/$s.toml -b $BENCHMARK 2>/dev/null | tee $s.txt 18 | done 19 | 20 | gnuplot $DIR/plot.gpl 21 | -------------------------------------------------------------------------------- /examples/ycsbd/plot.gpl: -------------------------------------------------------------------------------- 1 | set terminal pdf 2 | set ylabel "Throughput (MOP/s)" 3 | set xlabel "Threads" 4 | set key left top 5 | 6 | set output "ycsbd.pdf" 7 | plot [0:17] [0:] \ 8 | "chashmap.txt" using 2:14 with lp ti "chashmap",\ 9 | "contrie.txt" using 2:14 with lp ti "contrie",\ 10 | "dashmap.txt" using 2:14 with lp ti "dashmap",\ 11 | "flurry.txt" using 2:14 with lp ti "flurry",\ 12 | "papaya.txt" using 2:14 with lp ti "papaya",\ 13 | "scchashmap.txt" using 2:14 with lp ti "scchashmap",\ 14 | "mutex_hashmap.txt" using 2:14 with lp ti "mutexhashmap",\ 15 | "rwlock_hashmap.txt" using 2:14 with lp ti "rwlockhashmap" 16 | -------------------------------------------------------------------------------- /examples/readpopular/plot.gpl: -------------------------------------------------------------------------------- 1 | set terminal pdf 2 | set ylabel "Throughput (MOP/s)" 3 | set xlabel "Threads" 4 | set key left top 5 | 6 | set output "readpopular.pdf" 7 | plot [0:17] [0:] \ 8 | "chashmap.txt" using 2:14 with lp ti "chashmap",\ 9 | "contrie.txt" using 2:14 with lp ti "contrie",\ 10 | "dashmap.txt" using 2:14 with lp ti "dashmap",\ 11 | "flurry.txt" using 2:14 with lp ti "flurry",\ 12 | "papaya.txt" using 2:14 with lp ti "papaya",\ 13 | "scchashmap.txt" using 2:14 with lp ti "scchashmap",\ 14 | "mutex_hashmap.txt" using 2:14 with lp ti "mutexhashmap",\ 15 | "rwlock_hashmap.txt" using 2:14 with lp ti "rwlockhashmap" 16 | -------------------------------------------------------------------------------- /examples/writeheavy/plot.gpl: -------------------------------------------------------------------------------- 1 | set terminal pdf 2 | set ylabel "Throughput (MOP/s)" 3 | set xlabel "Threads" 4 | set key left top 5 | 6 | set output "writeheavy.pdf" 7 | plot [0:17] [0:] \ 8 | "chashmap.txt" using 2:14 with lp ti "chashmap",\ 9 | "contrie.txt" using 2:14 with lp ti "contrie",\ 10 | "dashmap.txt" using 2:14 with lp ti "dashmap",\ 11 | "flurry.txt" using 2:14 with lp ti "flurry",\ 12 | "papaya.txt" using 2:14 with lp ti "papaya",\ 13 | "scchashmap.txt" using 2:14 with lp ti "scchashmap",\ 14 | "mutex_hashmap.txt" using 2:14 with lp ti "mutexhashmap",\ 15 | "rwlock_hashmap.txt" using 2:14 with lp ti "rwlockhashmap" 16 | -------------------------------------------------------------------------------- /examples/mixed/plot.gpl: -------------------------------------------------------------------------------- 1 | set terminal pdf 2 | set ylabel "Throughput (MOP/s)" 3 | set xlabel "Time (sec)" 4 | set key left top 5 | 6 | set output "mixed.pdf" 7 | plot [0:21] [0:] \ 8 | "chashmap.txt" using ($0*$6+1):12 with lp ti "chashmap",\ 9 | "contrie.txt" using ($0*$6+1):12 with lp ti "contrie",\ 10 | "dashmap.txt" using ($0*$6+1):12 with lp ti "dashmap",\ 11 | "flurry.txt" using ($0*$6+1):12 with lp ti "flurry",\ 12 | "papaya.txt" using ($0*$6+1):12 with lp ti "papaya",\ 13 | "scchashmap.txt" using ($0*$6+1):12 with lp ti "scchashmap",\ 14 | "mutex_hashmap.txt" using ($0*$6+1):12 with lp ti "mutexhashmap",\ 15 | "rwlock_hashmap.txt" using ($0*$6+1):12 with lp ti "rwlockhashmap" 16 | -------------------------------------------------------------------------------- /examples/ycsbd/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | 5 | cargo +stable build --profile release-lto --all-features 6 | 7 | STORE_DIR=$DIR/../../presets/stores 8 | BENCHMARK=$DIR/ycsbd.toml 9 | 10 | STORES="chashmap contrie dashmap flurry papaya scchashmap mutex_hashmap rwlock_hashmap" 11 | 12 | for s in $STORES; do 13 | echo $s 14 | rm $s.txt 2>/dev/null 15 | for t in `seq 1 16`; do 16 | data="$(env global.threads=$t cargo +stable run --profile release-lto --all-features -- bench -s $STORE_DIR/$s.toml -b $BENCHMARK 2>/dev/null)" 17 | echo "threads $t $data" | tee -a $s.txt 18 | done 19 | done 20 | 21 | gnuplot $DIR/plot.gpl 22 | -------------------------------------------------------------------------------- /examples/readpopular/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | 5 | cargo +stable build --profile release-lto --all-features 6 | 7 | STORE_DIR=$DIR/../../presets/stores 8 | BENCHMARK=$DIR/readpopular.toml 9 | 10 | STORES="chashmap contrie dashmap flurry papaya scchashmap mutex_hashmap rwlock_hashmap" 11 | 12 | for s in $STORES; do 13 | echo $s 14 | rm $s.txt 2>/dev/null 15 | for t in `seq 1 16`; do 16 | data="$(env global.threads=$t cargo +stable run --profile release-lto --all-features -- bench -s $STORE_DIR/$s.toml -b $BENCHMARK 2>/dev/null)" 17 | echo "threads $t $data" | tee -a $s.txt 18 | done 19 | done 20 | 21 | gnuplot $DIR/plot.gpl 22 | -------------------------------------------------------------------------------- /examples/writeheavy/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | 5 | cargo +stable build --profile release-lto --all-features 6 | 7 | STORE_DIR=$DIR/../../presets/stores 8 | BENCHMARK=$DIR/writeheavy.toml 9 | 10 | STORES="chashmap contrie dashmap flurry papaya scchashmap mutex_hashmap rwlock_hashmap" 11 | 12 | for s in $STORES; do 13 | echo $s 14 | rm $s.txt 2>/dev/null 15 | for t in `seq 1 16`; do 16 | data="$(env global.threads=$t cargo +stable run --profile release-lto --all-features -- bench -s $STORE_DIR/$s.toml -b $BENCHMARK 2>/dev/null)" 17 | echo "threads $t $data" | tee -a $s.txt 18 | done 19 | done 20 | 21 | gnuplot $DIR/plot.gpl 22 | -------------------------------------------------------------------------------- /examples/mixed/mixed.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 1 3 | repeat = 5 4 | klen = 8 5 | vlen = 16 6 | kmin = 0 7 | kmax = 1000000 8 | report = "repeat" 9 | 10 | [[benchmark]] 11 | set_perc = 100 12 | repeat = 1 13 | dist = "incrementp" 14 | report = "hidden" 15 | 16 | # write-intensive, zipfian 17 | [[benchmark]] 18 | set_perc = 50 19 | get_perc = 50 20 | timeout = 1.0 21 | dist = "zipfian" 22 | 23 | # write-intensive, zipfian, hotspot in middle 24 | [[benchmark]] 25 | set_perc = 50 26 | get_perc = 50 27 | timeout = 1.0 28 | dist = "zipfian" 29 | zipf_hotspot = 0.5 30 | 31 | # read-intensive, zipfian 32 | [[benchmark]] 33 | set_perc = 5 34 | get_perc = 95 35 | timeout = 1.0 36 | dist = "zipfian" 37 | 38 | # read-only, uniform 39 | [[benchmark]] 40 | get_perc = 100 41 | timeout = 1.0 42 | dist = "uniform" 43 | -------------------------------------------------------------------------------- /examples/latency/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | 5 | cargo +stable build --profile release-lto --all-features 6 | 7 | STORE_DIR=$DIR/../../presets/stores 8 | 9 | STORES="chashmap contrie dashmap flurry papaya scchashmap mutex_hashmap rwlock_hashmap" 10 | 11 | for s in $STORES; do 12 | for b in readpopular writeheavy; do 13 | benchmark=$DIR/../$b/$b.toml 14 | echo $s-$b 15 | rm $s-$b.txt 2>/dev/null 16 | for t in `seq 1 16`; do 17 | data="$(env global.latency=true global.threads=$t cargo +stable run --profile release-lto --all-features -- bench -s $STORE_DIR/$s.toml -b $benchmark 2>/dev/null)" 18 | echo "threads $t $data" | tee -a $s-$b.txt 19 | done 20 | done 21 | done 22 | 23 | gnuplot $DIR/plot.gpl 24 | -------------------------------------------------------------------------------- /presets/benchmarks/example.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | threads = 8 3 | repeat = 5 4 | qd = 100 5 | batch = 10 6 | klen = 8 7 | vlen = 16 8 | kmin = 0 9 | kmax = 10000 10 | 11 | [[benchmark]] 12 | set_perc = 100 13 | repeat = 1 14 | dist = "incrementp" 15 | 16 | [[benchmark]] 17 | timeout = 0.2 18 | set_perc = 50 19 | get_perc = 50 20 | dist = "zipfian" 21 | 22 | [[benchmark]] 23 | timeout = 0.2 24 | set_perc = 50 25 | get_perc = 50 26 | dist = "uniform" 27 | 28 | [[benchmark]] 29 | set_perc = 100 30 | kmin = 10000 31 | kmax = 20000 32 | repeat = 1 33 | dist = "shufflep" 34 | 35 | [[benchmark]] 36 | set_perc = 100 37 | kmin = 20000 38 | kmax = 30000 39 | repeat = 1 40 | dist = "incrementp" 41 | 42 | [[benchmark]] 43 | del_perc = 100 44 | kmin = 5000 45 | kmax = 15000 46 | repeat = 1 47 | dist = "shufflep" 48 | 49 | [[benchmark]] 50 | del_perc = 100 51 | kmin = 15000 52 | kmax = 25000 53 | repeat = 1 54 | dist = "incrementp" 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | check-build-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/cache@v4 16 | with: 17 | path: | 18 | ~/.cargo/bin/ 19 | ~/.cargo/registry/index/ 20 | ~/.cargo/registry/cache/ 21 | ~/.cargo/git/db/ 22 | target/ 23 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} 24 | - uses: baptiste0928/cargo-install@v3 25 | with: 26 | crate: cargo-hack 27 | - name: build 28 | run: | 29 | cargo build --verbose --release 30 | cargo build --verbose --release --all-features 31 | - name: test 32 | run: | 33 | cargo test --release 34 | cargo hack test --each-feature --release 35 | cargo test --release --all-features 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kvbench 2 | 3 | [![Crates.io Version](https://img.shields.io/crates/v/kvbench)](https://crates.io/crates/kvbench/) 4 | [![Docs.rs Status](https://img.shields.io/docsrs/kvbench)](https://docs.rs/kvbench/) 5 | 6 | A benchmark framework designed for testing key-value stores with easily customizable 7 | workloads. 8 | 9 | ## Introduction 10 | 11 | This Rust crate enables the execution of customizable benchmarks on various key-value stores. 12 | Users have the flexibility to adjust benchmark and key-value store parameters and store them 13 | in TOML-formatted files. The built-in command line interface is capable of loading these files and 14 | running the benchmarks as specified. 15 | 16 | In addition to standard single-process benchmarks, it also seamlessly incorporates a key-value 17 | client/server implementation that operates with a dedicated server thread or machine. 18 | 19 | ## Usage 20 | 21 | The [documentation](https://docs.rs/kvbench) provides detailed usage guidelines. 22 | 23 | ## Development 24 | 25 | This project is being actively developed. More built-in stores and benchmark parameters 26 | are expected to be added. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Chen Chen 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /examples/writeheavy/README.md: -------------------------------------------------------------------------------- 1 | This example shows a benchmark that mixes reads and writes at 1:1 ratio accessing a random record 2 | in the store, running with different number of threads. 3 | 4 | The workload file, `writeheavy.toml` is as follows: 5 | 6 | ```toml 7 | [global] 8 | threads = 1 9 | repeat = 1 10 | klen = 8 11 | vlen = 16 12 | kmin = 0 13 | kmax = 1000000 14 | 15 | [[benchmark]] 16 | set_perc = 100 17 | get_perc = 0 18 | del_perc = 0 19 | repeat = 1 20 | dist = "incrementp" 21 | report = "hidden" 22 | 23 | [[benchmark]] 24 | timeout = 1 25 | set_perc = 50 26 | get_perc = 50 27 | del_perc = 0 28 | dist = "uniform" 29 | report = "finish" 30 | ``` 31 | 32 | In the first phase, all worker threads fill the key space of the store, and the metrics are hidden. 33 | In the second phase, worker threads execute the write-heavy workload for 1 second and report once 34 | when finished. 35 | 36 | The script file `run.sh` runs this benchmark against multiple stores with different number of 37 | threads. The number of threads are dynamically adjusted via `global.threads` environment variable. 38 | 39 | AMD Ryzen 9 5950X CPU 0-15 results ([pdf](writeheavy.pdf)): 40 | 41 | ![writeheavy](writeheavy.png) 42 | -------------------------------------------------------------------------------- /examples/readpopular/README.md: -------------------------------------------------------------------------------- 1 | This example shows a benchmark that reads popular records in the store, running with different 2 | number of threads. 3 | 4 | The workload file, `readpopular.toml` is as follows: 5 | 6 | ```toml 7 | [global] 8 | threads = 1 9 | repeat = 1 10 | klen = 8 11 | vlen = 16 12 | kmin = 0 13 | kmax = 1000000 14 | 15 | [[benchmark]] 16 | set_perc = 100 17 | get_perc = 0 18 | del_perc = 0 19 | repeat = 1 20 | dist = "incrementp" 21 | report = "hidden" 22 | 23 | [[benchmark]] 24 | timeout = 1 25 | set_perc = 0 26 | get_perc = 100 27 | del_perc = 0 28 | dist = "zipfian" 29 | zipf_theta = 1.0 30 | report = "finish" 31 | ``` 32 | 33 | In the first phase, all worker threads fill the key space of the store, and the metrics are hidden. 34 | In the second phase, worker threads execute the read-only workload that accesses Zipfian keys for 35 | 1 second and report once when finished. 36 | 37 | The script file `run.sh` runs this benchmark against multiple stores with different number of 38 | threads. The number of threads are dynamically adjusted via `global.threads` environment variable. 39 | 40 | AMD Ryzen 9 5950X CPU 0-15 results ([pdf](readpopular.pdf)): 41 | 42 | ![readpopular](readpopular.png) 43 | -------------------------------------------------------------------------------- /src/stores/dashmap.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`dashmap::DashMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "dashmap" 8 | //! ``` 9 | //! 10 | //! This store is [`KVMap`]. 11 | 12 | use crate::stores::{BenchKVMap, Registry}; 13 | use crate::*; 14 | 15 | #[derive(Clone)] 16 | pub struct DashMap(Arc, Box<[u8]>>>); 17 | 18 | impl DashMap { 19 | pub fn new() -> Self { 20 | Self(Arc::new(dashmap::DashMap::, Box<[u8]>>::new())) 21 | } 22 | 23 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 24 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 25 | } 26 | } 27 | 28 | impl KVMap for DashMap { 29 | fn handle(&self) -> Box { 30 | Box::new(self.clone()) 31 | } 32 | } 33 | 34 | impl KVMapHandle for DashMap { 35 | fn set(&mut self, key: &[u8], value: &[u8]) { 36 | self.0.insert(key.into(), value.into()); 37 | } 38 | 39 | fn get(&mut self, key: &[u8]) -> Option> { 40 | match self.0.get(key) { 41 | Some(v) => Some(v.clone()), 42 | None => None, 43 | } 44 | } 45 | 46 | fn delete(&mut self, key: &[u8]) { 47 | self.0.remove(key); 48 | } 49 | 50 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 51 | unimplemented!("Range query is not supported"); 52 | } 53 | } 54 | 55 | inventory::submit! { 56 | Registry::new("dashmap", DashMap::new_benchkvmap) 57 | } 58 | -------------------------------------------------------------------------------- /src/stores/chashmap.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`chashmap::CHashMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "chashmap" 8 | //! ``` 9 | //! This store is [`KVMap`]. 10 | 11 | use crate::stores::{BenchKVMap, Registry}; 12 | use crate::*; 13 | 14 | #[derive(Clone)] 15 | pub struct CHashMap(Arc, Box<[u8]>>>); 16 | 17 | impl CHashMap { 18 | pub fn new() -> Self { 19 | Self(Arc::new(chashmap::CHashMap::, Box<[u8]>>::new())) 20 | } 21 | 22 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 23 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 24 | } 25 | } 26 | 27 | impl KVMap for CHashMap { 28 | fn handle(&self) -> Box { 29 | Box::new(self.clone()) 30 | } 31 | } 32 | 33 | impl KVMapHandle for CHashMap { 34 | fn set(&mut self, key: &[u8], value: &[u8]) { 35 | self.0.insert(key.into(), value.into()); 36 | } 37 | 38 | fn get(&mut self, key: &[u8]) -> Option> { 39 | match self.0.get(key) { 40 | Some(r) => Some(r.clone()), 41 | None => None, 42 | } 43 | } 44 | 45 | fn delete(&mut self, key: &[u8]) { 46 | self.0.remove(key); 47 | } 48 | 49 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 50 | unimplemented!("Range query is not supported"); 51 | } 52 | } 53 | 54 | inventory::submit! { 55 | Registry::new("chashmap", CHashMap::new_benchkvmap) 56 | } 57 | -------------------------------------------------------------------------------- /src/stores/contrie.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`contrie::ConMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "contrie" 8 | //! ``` 9 | //! 10 | //! This store is [`KVMap`]. 11 | 12 | use crate::stores::{BenchKVMap, Registry}; 13 | use crate::*; 14 | 15 | #[derive(Clone)] 16 | pub struct Contrie(Arc, Box<[u8]>>>); 17 | 18 | impl Contrie { 19 | pub fn new() -> Self { 20 | Self(Arc::new(contrie::ConMap::, Box<[u8]>>::new())) 21 | } 22 | 23 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 24 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 25 | } 26 | } 27 | 28 | impl KVMap for Contrie { 29 | fn handle(&self) -> Box { 30 | Box::new(self.clone()) 31 | } 32 | } 33 | 34 | impl KVMapHandle for Contrie { 35 | fn set(&mut self, key: &[u8], value: &[u8]) { 36 | self.0.insert(key.into(), value.into()); 37 | } 38 | 39 | fn get(&mut self, key: &[u8]) -> Option> { 40 | match self.0.get(key) { 41 | Some(r) => Some(r.value().clone()), 42 | None => None, 43 | } 44 | } 45 | 46 | fn delete(&mut self, key: &[u8]) { 47 | self.0.remove(key); 48 | } 49 | 50 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 51 | unimplemented!("Range query is not supported"); 52 | } 53 | } 54 | 55 | inventory::submit! { 56 | Registry::new("contrie", Contrie::new_benchkvmap) 57 | } 58 | -------------------------------------------------------------------------------- /src/stores/flurry.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`flurry::HashMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "flurry" 8 | //! ``` 9 | //! 10 | //! This store is [`KVMap`]. 11 | 12 | use crate::stores::{BenchKVMap, Registry}; 13 | use crate::*; 14 | 15 | #[derive(Clone)] 16 | pub struct Flurry(Arc, Box<[u8]>>>); 17 | 18 | impl Flurry { 19 | pub fn new() -> Self { 20 | Self(Arc::new(flurry::HashMap::, Box<[u8]>>::new())) 21 | } 22 | 23 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 24 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 25 | } 26 | } 27 | 28 | impl KVMap for Flurry { 29 | fn handle(&self) -> Box { 30 | Box::new(self.clone()) 31 | } 32 | } 33 | 34 | impl KVMapHandle for Flurry { 35 | fn set(&mut self, key: &[u8], value: &[u8]) { 36 | self.0.pin().insert(key.into(), value.into()); 37 | } 38 | 39 | fn get(&mut self, key: &[u8]) -> Option> { 40 | match self.0.pin().get(key) { 41 | Some(v) => Some(v.clone()), 42 | None => None, 43 | } 44 | } 45 | 46 | fn delete(&mut self, key: &[u8]) { 47 | self.0.pin().remove(key); 48 | } 49 | 50 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 51 | unimplemented!("Range query is not supported"); 52 | } 53 | } 54 | 55 | inventory::submit! { 56 | Registry::new("flurry", Flurry::new_benchkvmap) 57 | } 58 | -------------------------------------------------------------------------------- /src/stores/papaya.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`papaya::HashMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "papaya" 8 | //! ``` 9 | //! 10 | //! This store is [`KVMap`]. 11 | 12 | use crate::stores::{BenchKVMap, Registry}; 13 | use crate::*; 14 | 15 | #[derive(Clone)] 16 | pub struct Papaya(Arc, Box<[u8]>>>); 17 | 18 | impl Papaya { 19 | pub fn new() -> Self { 20 | Self(Arc::new(papaya::HashMap::, Box<[u8]>>::new())) 21 | } 22 | 23 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 24 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 25 | } 26 | } 27 | 28 | impl KVMap for Papaya { 29 | fn handle(&self) -> Box { 30 | Box::new(self.clone()) 31 | } 32 | } 33 | 34 | impl KVMapHandle for Papaya { 35 | fn set(&mut self, key: &[u8], value: &[u8]) { 36 | self.0.pin().insert(key.into(), value.into()); 37 | } 38 | 39 | fn get(&mut self, key: &[u8]) -> Option> { 40 | match self.0.pin().get(key) { 41 | Some(v) => Some(v.clone()), 42 | None => None, 43 | } 44 | } 45 | 46 | fn delete(&mut self, key: &[u8]) { 47 | self.0.pin().remove(key); 48 | } 49 | 50 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 51 | unimplemented!("Range query is not supported"); 52 | } 53 | } 54 | 55 | inventory::submit! { 56 | Registry::new("papaya", Papaya::new_benchkvmap) 57 | } 58 | -------------------------------------------------------------------------------- /examples/ycsbd/README.md: -------------------------------------------------------------------------------- 1 | This example shows how to run YCSB-like benchmarks in kvbench by utilizing its customizable 2 | workloads. Here we simulate the YCSB-D workload where there are 5% inserts/writes and 95% reads. 3 | The key distribution still follows Zipfian but the hotspot is always the latest written key. 4 | 5 | Unlike YCSB that really inserts a key, kvbench assumes a fixed-sized key space, and a write 6 | operation can be either an insert or an update internally. For the latest key, it should not make 7 | a huge difference as it's usually cached. 8 | 9 | The workload file, `ycsbd.toml` is as follows: 10 | 11 | ```toml 12 | [global] 13 | threads = 1 14 | repeat = 1 15 | klen = 8 16 | vlen = 16 17 | kmin = 0 18 | kmax = 1000000 19 | 20 | [[benchmark]] 21 | set_perc = 100 22 | get_perc = 0 23 | del_perc = 0 24 | repeat = 1 25 | dist = "shufflep" 26 | report = "hidden" 27 | 28 | [[benchmark]] 29 | timeout = 3 30 | set_perc = 5 31 | get_perc = 95 32 | del_perc = 0 33 | dist = "latest" 34 | report = "finish" 35 | ``` 36 | 37 | In the first phase, all worker threads fill the key space of the store, and the metrics are hidden. 38 | Note that the loading is done using `shufflep` instead of `incrementp`. With `shufflep`, the order 39 | of the keys in the key space is shuffled and each key is written exactly once. 40 | In the second phase, worker threads execute the workload using the built-in `latest` key 41 | distribution. 42 | 43 | The script file `run.sh` runs this benchmark against multiple stores with different number of 44 | threads. The number of threads are dynamically adjusted via `global.threads` environment variable. 45 | 46 | AMD Ryzen 9 5950X CPU 0-15 results ([pdf](ycsbd.pdf)): 47 | 48 | ![ycsbd](ycsbd.png) 49 | -------------------------------------------------------------------------------- /src/stores/scc.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`scc::hash_map::HashMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "scchashmap" 8 | //! ``` 9 | //! This store is [`KVMap`]. 10 | 11 | use crate::stores::{BenchKVMap, Registry}; 12 | use crate::*; 13 | 14 | #[derive(Clone)] 15 | pub struct SccHashMap(Arc, Box<[u8]>>>); 16 | 17 | impl SccHashMap { 18 | pub fn new() -> Self { 19 | Self(Arc::new( 20 | scc::hash_map::HashMap::, Box<[u8]>>::new(), 21 | )) 22 | } 23 | 24 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 25 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 26 | } 27 | } 28 | 29 | impl KVMap for SccHashMap { 30 | fn handle(&self) -> Box { 31 | Box::new(self.clone()) 32 | } 33 | } 34 | 35 | impl KVMapHandle for SccHashMap { 36 | fn set(&mut self, key: &[u8], value: &[u8]) { 37 | match self.0.entry(key.into()) { 38 | scc::hash_map::Entry::Occupied(mut o) => { 39 | *o.get_mut() = value.into(); 40 | } 41 | scc::hash_map::Entry::Vacant(v) => { 42 | v.insert_entry(value.into()); 43 | } 44 | } 45 | } 46 | 47 | fn get(&mut self, key: &[u8]) -> Option> { 48 | self.0.read(key, |_, r| r.clone()) 49 | } 50 | 51 | fn delete(&mut self, key: &[u8]) { 52 | self.0.remove(key); 53 | } 54 | 55 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 56 | unimplemented!("Range query is not supported"); 57 | } 58 | } 59 | 60 | inventory::submit! { 61 | Registry::new("scchashmap", SccHashMap::new_benchkvmap) 62 | } 63 | -------------------------------------------------------------------------------- /examples/your-kv-store/src/main.rs: -------------------------------------------------------------------------------- 1 | //! How to add your implementation to `kvbench`. 2 | 3 | extern crate kvbench; 4 | 5 | use kvbench::inventory; 6 | use kvbench::toml; 7 | 8 | use kvbench::stores::{BenchKVMap, Registry}; 9 | use kvbench::{KVMap, KVMapHandle}; 10 | use std::collections::HashMap; 11 | use std::sync::{Arc, RwLock}; 12 | 13 | #[derive(Clone)] 14 | pub struct YourKVMap(Arc, Box<[u8]>>>>); 15 | 16 | impl YourKVMap { 17 | pub fn new() -> Self { 18 | Self(Arc::new( 19 | RwLock::new(HashMap::, Box<[u8]>>::new()), 20 | )) 21 | } 22 | 23 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 24 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 25 | } 26 | } 27 | 28 | impl KVMap for YourKVMap { 29 | fn handle(&self) -> Box { 30 | Box::new(self.clone()) 31 | } 32 | } 33 | 34 | impl KVMapHandle for YourKVMap { 35 | fn set(&mut self, key: &[u8], value: &[u8]) { 36 | self.0.write().unwrap().insert(key.into(), value.into()); 37 | } 38 | 39 | fn get(&mut self, key: &[u8]) -> Option> { 40 | match self.0.read().unwrap().get(key) { 41 | Some(v) => Some(v.clone()), 42 | None => None, 43 | } 44 | } 45 | 46 | fn delete(&mut self, key: &[u8]) { 47 | self.0.write().unwrap().remove(key); 48 | } 49 | 50 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 51 | unimplemented!(); 52 | } 53 | } 54 | 55 | inventory::submit! { 56 | Registry::new("your_kv_store", YourKVMap::new_benchkvmap) 57 | } 58 | 59 | fn main() { 60 | // Call the `cmdline()` function directly here, and you will get the same benchmark binary 61 | // that contains your kv and all the built-in stores in `kvbench`. 62 | kvbench::cmdline(); 63 | } 64 | -------------------------------------------------------------------------------- /examples/mixed/README.md: -------------------------------------------------------------------------------- 1 | This example shows a benchmark that consists of multiple phases (5 seconds 2 | running time each), benchmarked with 32 threads. 3 | 4 | The workload file, `mixed.toml` is as follows: 5 | 6 | ```toml 7 | [global] 8 | threads = 1 9 | repeat = 5 10 | klen = 8 11 | vlen = 16 12 | kmin = 0 13 | kmax = 1000000 14 | report = "repeat" 15 | 16 | [[benchmark]] 17 | set_perc = 100 18 | get_perc = 0 19 | del_perc = 0 20 | repeat = 1 21 | dist = "incrementp" 22 | report = "hidden" 23 | 24 | # write-intensive, zipfian 25 | [[benchmark]] 26 | set_perc = 50 27 | get_perc = 50 28 | del_perc = 0 29 | timeout = 1.0 30 | dist = "zipfian" 31 | 32 | # write-intensive, zipfian, hotspot in middle 33 | [[benchmark]] 34 | set_perc = 50 35 | get_perc = 50 36 | del_perc = 0 37 | timeout = 1.0 38 | dist = "zipfian" 39 | zipf_hotspot = 0.5 40 | 41 | # read-intensive, zipfian 42 | [[benchmark]] 43 | set_perc = 5 44 | get_perc = 95 45 | del_perc = 0 46 | timeout = 1.0 47 | dist = "zipfian" 48 | 49 | # read-only, uniform 50 | [[benchmark]] 51 | set_perc = 0 52 | get_perc = 100 53 | del_perc = 0 54 | timeout = 1.0 55 | dist = "uniform" 56 | ``` 57 | 58 | In the first phase, all worker threads fill the key space of the store, and the metrics are hidden. 59 | Then, the benchmark consists of 4 parts with 5 seconds running time each: 60 | 61 | - Write-intensive on popular keys. 62 | - Write-intensive on popular keys while the popular keys shifted to the middle of the 63 | key space. 64 | - Read-intensive (only 5% writes) on popular keys. 65 | - Read-only on uniformly random keys. 66 | 67 | The script file `run.sh` runs this benchmark against multiple stores with 32 threads. 68 | Although the number of threads set in the configuration file is only 1, the number of threads are 69 | dynamically adjusted by setting `global.threads` to 32. 70 | 71 | AMD Ryzen 9 5950X CPU 0-15 results ([pdf](mixed.pdf)): 72 | 73 | ![mixed](mixed.png) 74 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kvbench" 3 | version = "0.3.0" 4 | authors = ["Chen Chen "] 5 | description = "A key-value store benchmark framework with customizable workloads" 6 | edition = "2021" 7 | readme = "README.md" 8 | repository = "https://github.com/nerdroychan/kvbench" 9 | license = "MIT" 10 | keywords = ["benchmark", "key-value"] 11 | categories = ["concurrency", "development-tools::profiling", "development-tools::testing"] 12 | exclude = ["examples"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [dependencies] 19 | bincode = "1.3.3" 20 | chashmap = { version = "2.2.2", optional = true } 21 | clap = { version = "4.5.28", features = ["derive"] } 22 | contrie = { version = "0.1.4", optional = true } 23 | core_affinity = "0.8.1" 24 | ctrlc = "3.4.5" 25 | dashmap = { version = "6.1.0", features = ["inline"], optional = true } 26 | env_logger = "0.11.6" 27 | figment = { version = "0.10.19", features = ["toml", "env"] } 28 | flurry = { version = "0.5.2", optional = true } 29 | hashbrown = "0.14.5" 30 | hdrhistogram = "7.5.4" 31 | inventory = "0.3.19" 32 | log = "0.4.25" 33 | mio = { version = "1.0.3", features = ["net", "os-poll"] } 34 | papaya = { version = "0.1.8", optional = true } 35 | parking_lot = "0.12.3" 36 | quanta = "0.12.5" 37 | rand = "0.9.0" 38 | rand_distr = "0.5.1" 39 | rocksdb = { version = "0.22.0", optional = true } 40 | rustc-hash = "2.1.1" 41 | scc = { version = "2.3.3", optional = true } 42 | serde = { version = "1.0.217", features = ["derive"] } 43 | toml = "0.8.20" 44 | 45 | [dev-dependencies] 46 | tempfile = "3.16.0" 47 | 48 | [features] 49 | chashmap = ["dep:chashmap"] 50 | contrie = ["dep:contrie"] 51 | dashmap = ["dep:dashmap"] 52 | flurry = ["dep:flurry"] 53 | papaya = ["dep:papaya"] 54 | rocksdb = ["dep:rocksdb"] 55 | scc = ["dep:scc"] 56 | 57 | [profile.release-lto] 58 | inherits = "release" 59 | lto = true 60 | -------------------------------------------------------------------------------- /src/stores/remote.rs: -------------------------------------------------------------------------------- 1 | //! A client of a key-value server. The server can be backed by any stores available. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "remote" 8 | //! host = "..." # hostname of the server 9 | //! port = "..." # port of the server 10 | //! ``` 11 | //! 12 | //! This store is [`AsyncKVMap`]. 13 | 14 | use crate::server::KVClient; 15 | use crate::stores::{BenchKVMap, Registry}; 16 | use crate::*; 17 | use serde::Deserialize; 18 | use std::rc::Rc; 19 | 20 | pub struct RemoteMap { 21 | host: String, 22 | port: String, 23 | } 24 | 25 | pub struct RemoteMapHandle { 26 | client: KVClient, 27 | responder: Rc, 28 | pending: usize, 29 | } 30 | 31 | #[derive(Deserialize)] 32 | pub struct RemoteMapOpt { 33 | host: String, 34 | port: String, 35 | } 36 | 37 | impl RemoteMap { 38 | pub fn new(opt: &RemoteMapOpt) -> Self { 39 | Self { 40 | host: opt.host.clone(), 41 | port: opt.port.clone(), 42 | } 43 | } 44 | 45 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 46 | let opt: RemoteMapOpt = opt.clone().try_into().unwrap(); 47 | BenchKVMap::Async(Arc::new(Box::new(Self::new(&opt)))) 48 | } 49 | } 50 | 51 | impl AsyncKVMap for RemoteMap { 52 | fn handle(&self, responder: Rc) -> Box { 53 | Box::new(RemoteMapHandle { 54 | client: KVClient::new(&self.host, &self.port).unwrap(), 55 | responder, 56 | pending: 0, 57 | }) 58 | } 59 | } 60 | 61 | impl AsyncKVMapHandle for RemoteMapHandle { 62 | fn submit(&mut self, requests: &Vec) { 63 | self.client.send_requests(requests); 64 | self.pending += requests.len(); 65 | } 66 | 67 | fn drain(&mut self) { 68 | if self.pending > 0 { 69 | for r in self.client.recv_responses().into_iter() { 70 | self.responder.callback(r); 71 | self.pending -= 1; 72 | } 73 | } 74 | } 75 | } 76 | 77 | inventory::submit! { 78 | Registry::new("remotemap", RemoteMap::new_benchkvmap) 79 | } 80 | -------------------------------------------------------------------------------- /src/stores/rocksdb.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`rocksdb`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ``` toml 6 | //! [map] 7 | //! name = "rocksdb" 8 | //! path = "..." # path to the rocksdb data directory 9 | //! ``` 10 | //! 11 | //! This store is [`KVMap`]. 12 | 13 | use crate::stores::{BenchKVMap, Registry}; 14 | use crate::*; 15 | use rocksdb::{Direction, IteratorMode, DB}; 16 | use serde::Deserialize; 17 | 18 | #[derive(Deserialize)] 19 | pub struct RocksDBOpt { 20 | pub path: String, 21 | } 22 | 23 | #[derive(Clone)] 24 | pub struct RocksDB { 25 | db: Arc, 26 | } 27 | 28 | impl RocksDB { 29 | pub fn new(opt: &RocksDBOpt) -> Self { 30 | let db = Arc::new(DB::open_default(&opt.path).unwrap()); 31 | Self { db } 32 | } 33 | 34 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 35 | let opt: RocksDBOpt = opt.clone().try_into().unwrap(); 36 | BenchKVMap::Regular(Arc::new(Box::new(Self::new(&opt)))) 37 | } 38 | } 39 | 40 | impl KVMap for RocksDB { 41 | fn handle(&self) -> Box { 42 | Box::new(self.clone()) 43 | } 44 | } 45 | 46 | impl KVMapHandle for RocksDB { 47 | fn set(&mut self, key: &[u8], value: &[u8]) { 48 | assert!(self.db.put(key, value).is_ok()); 49 | } 50 | 51 | fn get(&mut self, key: &[u8]) -> Option> { 52 | if let Ok(v) = self.db.get(key) { 53 | v.map(|vec| vec.into_boxed_slice()) 54 | } else { 55 | None 56 | } 57 | } 58 | 59 | fn delete(&mut self, key: &[u8]) { 60 | assert!(self.db.delete(key).is_ok()); 61 | } 62 | 63 | fn scan(&mut self, key: &[u8], n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 64 | let mut kv = Vec::with_capacity(n); 65 | let iter = self 66 | .db 67 | .iterator(IteratorMode::From(key, Direction::Forward)); 68 | let mut i = 0; 69 | for item in iter { 70 | if i == n { 71 | break; 72 | } 73 | kv.push(item.unwrap()); 74 | i += 1; 75 | } 76 | kv 77 | } 78 | } 79 | 80 | inventory::submit! { 81 | Registry::new("rocksdb", RocksDB::new_benchkvmap) 82 | } 83 | -------------------------------------------------------------------------------- /src/thread.rs: -------------------------------------------------------------------------------- 1 | //! Spawn-join functionality. 2 | //! 3 | //! **You may not need to check this if it is OK to run benchmarks with [`std::thread`].** 4 | //! 5 | //! A key-value store is generally passive. However, some store may act like a server with 6 | //! active threads. In that case, one may employ its own implementation of spawn-join. If that is 7 | //! the case, their join handle (like [`std::thread::JoinHandle`]) should implement the 8 | //! [`JoinHandle`] trait and the spawn struct needs to implement [`Thread`]. 9 | //! 10 | //! Note that for simplicity, the function spawn is generic over should not have a return value. So 11 | //! it is with the [`JoinHandle`]. Because the purpose is not general spawn-join but solely for 12 | //! benchmark code, which does not use any return values. 13 | 14 | /// A join handle returned by a spawn function. 15 | pub trait JoinHandle { 16 | /// Join the thread, consume the boxed self. 17 | fn join(self: Box); 18 | } 19 | 20 | /// A thread management abstraction. 21 | pub trait Thread { 22 | /// Spawn a new thread using a boxed closure. 23 | fn spawn(&self, f: Box) -> Box; 24 | 25 | /// Yield the current thread. 26 | fn yield_now(&self); 27 | 28 | /// Pin the current thread to a certain CPU core. 29 | fn pin(&self, core: usize); 30 | } 31 | 32 | /// A zero-sized wrapper for [`std::thread`] functions. 33 | #[derive(Clone)] 34 | pub struct DefaultThread; 35 | 36 | /// A wrapper for [`std::thread::JoinHandle`]. 37 | pub struct DefaultJoinHandle(std::thread::JoinHandle<()>); 38 | 39 | impl JoinHandle for DefaultJoinHandle { 40 | fn join(self: Box) { 41 | let handle = self.0; 42 | assert!(handle.join().is_ok()); 43 | } 44 | } 45 | 46 | impl Thread for DefaultThread { 47 | fn spawn(&self, f: Box) -> Box { 48 | let handle = std::thread::spawn(f); 49 | Box::new(DefaultJoinHandle(handle)) 50 | } 51 | 52 | fn yield_now(&self) { 53 | std::thread::yield_now(); 54 | } 55 | 56 | fn pin(&self, core: usize) { 57 | let cores = core_affinity::get_core_ids().unwrap(); 58 | core_affinity::set_for_current(cores[core % cores.len()]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/stores/null.rs: -------------------------------------------------------------------------------- 1 | //! A store that does nothing. It can be used to measure overheads of the crate. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ### Regular 6 | //! 7 | //! ``` toml 8 | //! [map] 9 | //! name = "nullmap" 10 | //! ``` 11 | //! 12 | //! This store is [`KVMap`]. 13 | //! 14 | //! ### Async 15 | //! 16 | //! ``` toml 17 | //! [map] 18 | //! name = "nullmap_async" 19 | //! ``` 20 | //! 21 | //! This store is [`AsyncKVMap`]. 22 | 23 | use crate::stores::{BenchKVMap, Registry}; 24 | use crate::*; 25 | 26 | #[derive(Clone)] 27 | pub struct NullMap; 28 | 29 | impl NullMap { 30 | pub fn new() -> Self { 31 | Self 32 | } 33 | 34 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 35 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 36 | } 37 | 38 | pub fn new_benchkvmap_async(_opt: &toml::Table) -> BenchKVMap { 39 | BenchKVMap::Async(Arc::new(Box::new(Self::new()))) 40 | } 41 | } 42 | 43 | impl KVMap for NullMap { 44 | fn handle(&self) -> Box { 45 | Box::new(self.clone()) 46 | } 47 | } 48 | 49 | impl KVMapHandle for NullMap { 50 | fn set(&mut self, _key: &[u8], _value: &[u8]) {} 51 | 52 | fn get(&mut self, _key: &[u8]) -> Option> { 53 | None 54 | } 55 | 56 | fn delete(&mut self, _key: &[u8]) {} 57 | 58 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 59 | Vec::new() 60 | } 61 | } 62 | 63 | inventory::submit! { 64 | Registry::new("nullmap", NullMap::new_benchkvmap) 65 | } 66 | 67 | struct NullMapAsyncHandle(Vec, Rc); 68 | 69 | impl AsyncKVMap for NullMap { 70 | fn handle(&self, responder: Rc) -> Box { 71 | Box::new(NullMapAsyncHandle(Vec::new(), responder.clone())) 72 | } 73 | } 74 | 75 | impl AsyncKVMapHandle for NullMapAsyncHandle { 76 | fn drain(&mut self) { 77 | for i in self.0.iter() { 78 | self.1.callback(Response { id: *i, data: None }); 79 | } 80 | self.0.clear(); 81 | } 82 | 83 | fn submit(&mut self, requests: &Vec) { 84 | for r in requests.iter() { 85 | self.0.push(r.id); 86 | } 87 | } 88 | } 89 | 90 | inventory::submit! { 91 | Registry::new("nullmap_async", NullMap::new_benchkvmap_async) 92 | } 93 | -------------------------------------------------------------------------------- /src/stores/remotereplicated.rs: -------------------------------------------------------------------------------- 1 | //! A client of a gateway-replicated key-value server. Each endpoint points to the same store. 2 | //! The server can be backed by any stores available. 3 | //! 4 | //! ## Configuration Format 5 | //! 6 | //! ``` toml 7 | //! [map] 8 | //! name = "remotereplicated" 9 | //! 10 | //! [[map.addr]] 11 | //! host = "..." # host 1 12 | //! port = "..." # port 1 13 | //! 14 | //! [[map.addr]] 15 | //! host = "..." # host 2 16 | //! port = "..." # port 2 17 | //! ``` 18 | //! 19 | //! This store is [`AsyncKVMap`]. 20 | 21 | use crate::stores::remote::{RemoteMap, RemoteMapOpt}; 22 | use crate::stores::{BenchKVMap, Registry}; 23 | use crate::*; 24 | use serde::Deserialize; 25 | use std::rc::Rc; 26 | use std::sync::atomic::AtomicUsize; 27 | 28 | pub struct RemoteReplicatedMap { 29 | maps: Vec, 30 | next: AtomicUsize, 31 | } 32 | 33 | #[derive(Deserialize)] 34 | pub struct RemoteReplicatedMapOpt { 35 | addr: Vec, 36 | } 37 | 38 | impl RemoteReplicatedMap { 39 | pub fn new(opt: &RemoteReplicatedMapOpt) -> Self { 40 | Self { 41 | maps: opt.addr.iter().map(|a| RemoteMap::new(a)).collect(), 42 | next: AtomicUsize::new(0), 43 | } 44 | } 45 | 46 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 47 | let opt: RemoteReplicatedMapOpt = opt.clone().try_into().unwrap(); 48 | BenchKVMap::Async(Arc::new(Box::new(Self::new(&opt)))) 49 | } 50 | } 51 | 52 | impl AsyncKVMap for RemoteReplicatedMap { 53 | fn handle(&self, responder: Rc) -> Box { 54 | let next = self.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 55 | let map = &self.maps[next % self.maps.len()]; 56 | map.handle(responder) 57 | } 58 | } 59 | 60 | inventory::submit! { 61 | Registry::new("remotereplicatedmap", RemoteReplicatedMap::new_benchkvmap) 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn parse() { 70 | let opt: RemoteReplicatedMapOpt = toml::from_str( 71 | r#" 72 | [[addr]] 73 | host = "127.0.0.1" 74 | port = "8080" 75 | 76 | [[addr]] 77 | host = "127.0.0.1" 78 | port = "8081" 79 | "#, 80 | ) 81 | .unwrap(); 82 | let map = RemoteReplicatedMap::new(&opt); 83 | assert_eq!(map.maps.len(), 2); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/stores/btreemap.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`std::collections::BTreeMap`]. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ### [`Mutex`]-based: 6 | //! 7 | //! ``` toml 8 | //! [map] 9 | //! name = "mutex_btreemap" 10 | //! ``` 11 | //! 12 | //! This store is [`KVMap`]. 13 | //! 14 | //! ### [`RwLock`]-based: 15 | //! ``` toml 16 | //! [map] 17 | //! name = "rwlock_btreemap" 18 | //! ``` 19 | //! 20 | //! This store is [`KVMap`]. 21 | 22 | use crate::stores::*; 23 | use parking_lot::{Mutex, RwLock}; 24 | use std::collections::BTreeMap; 25 | use std::sync::Arc; 26 | 27 | #[derive(Clone)] 28 | pub struct MutexBTreeMap(Arc, Box<[u8]>>>>); 29 | 30 | impl MutexBTreeMap { 31 | pub fn new() -> Self { 32 | Self(Arc::new( 33 | Mutex::new(BTreeMap::, Box<[u8]>>::new()), 34 | )) 35 | } 36 | 37 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 38 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 39 | } 40 | } 41 | 42 | impl KVMap for MutexBTreeMap { 43 | fn handle(&self) -> Box { 44 | Box::new(self.clone()) 45 | } 46 | } 47 | 48 | impl KVMapHandle for MutexBTreeMap { 49 | fn set(&mut self, key: &[u8], value: &[u8]) { 50 | self.0.lock().insert(key.into(), value.into()); 51 | } 52 | 53 | fn get(&mut self, key: &[u8]) -> Option> { 54 | match self.0.lock().get(key) { 55 | Some(v) => Some(v.clone()), 56 | None => None, 57 | } 58 | } 59 | 60 | fn delete(&mut self, key: &[u8]) { 61 | self.0.lock().remove(key); 62 | } 63 | 64 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 65 | // technically iteration is supported but querying a specific range is not a stable feature 66 | unimplemented!("Range query is not supported"); 67 | } 68 | } 69 | 70 | inventory::submit! { 71 | Registry::new("mutex_btreemap", MutexBTreeMap::new_benchkvmap) 72 | } 73 | 74 | #[derive(Clone)] 75 | pub struct RwLockBTreeMap(Arc, Box<[u8]>>>>); 76 | 77 | impl RwLockBTreeMap { 78 | pub fn new() -> Self { 79 | Self(Arc::new(RwLock::new( 80 | BTreeMap::, Box<[u8]>>::new(), 81 | ))) 82 | } 83 | 84 | pub fn new_benchkvmap(_opt: &toml::Table) -> BenchKVMap { 85 | BenchKVMap::Regular(Arc::new(Box::new(Self::new()))) 86 | } 87 | } 88 | 89 | impl KVMap for RwLockBTreeMap { 90 | fn handle(&self) -> Box { 91 | Box::new(self.clone()) 92 | } 93 | } 94 | 95 | impl KVMapHandle for RwLockBTreeMap { 96 | fn set(&mut self, key: &[u8], value: &[u8]) { 97 | self.0.write().insert(key.into(), value.into()); 98 | } 99 | 100 | fn get(&mut self, key: &[u8]) -> Option> { 101 | match self.0.read().get(key) { 102 | Some(v) => Some(v.clone()), 103 | None => None, 104 | } 105 | } 106 | 107 | fn delete(&mut self, key: &[u8]) { 108 | self.0.write().remove(key); 109 | } 110 | 111 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 112 | // technically iteration is supported but querying a specific range is not a stable feature 113 | unimplemented!("Range query is not supported"); 114 | } 115 | } 116 | 117 | inventory::submit! { 118 | Registry::new("rwlock_btreemap", RwLockBTreeMap::new_benchkvmap) 119 | } 120 | -------------------------------------------------------------------------------- /examples/latency/plot.gpl: -------------------------------------------------------------------------------- 1 | set terminal pdf size 10,3 2 | set xlabel "Threads" 3 | set key left top 4 | 5 | set output "latency-writeheavy.pdf" 6 | set multiplot layout 1,3 7 | set ylabel "Average Latency (us)" 8 | plot [0:17] [0:] \ 9 | "chashmap-writeheavy.txt" using 2:20 with lp ti "chashmap",\ 10 | "contrie-writeheavy.txt" using 2:20 with lp ti "contrie",\ 11 | "dashmap-writeheavy.txt" using 2:20 with lp ti "dashmap",\ 12 | "flurry-writeheavy.txt" using 2:20 with lp ti "flurry",\ 13 | "papaya-writeheavy.txt" using 2:20 with lp ti "papaya",\ 14 | "scchashmap-writeheavy.txt" using 2:20 with lp ti "scchashmap",\ 15 | "mutex_hashmap-writeheavy.txt" using 2:20 with lp ti "mutexhashmap",\ 16 | "rwlock_hashmap-writeheavy.txt" using 2:20 with lp ti "rwlockhashmap" 17 | 18 | set ylabel "P99 Latency (us)" 19 | plot [0:17] [0:] \ 20 | "chashmap-writeheavy.txt" using 2:26 with lp ti "chashmap",\ 21 | "contrie-writeheavy.txt" using 2:26 with lp ti "contrie",\ 22 | "dashmap-writeheavy.txt" using 2:26 with lp ti "dashmap",\ 23 | "flurry-writeheavy.txt" using 2:26 with lp ti "flurry",\ 24 | "papaya-writeheavy.txt" using 2:26 with lp ti "papaya",\ 25 | "scchashmap-writeheavy.txt" using 2:26 with lp ti "scchashmap",\ 26 | "mutex_hashmap-writeheavy.txt" using 2:26 with lp ti "mutexhashmap",\ 27 | "rwlock_hashmap-writeheavy.txt" using 2:26 with lp ti "rwlockhashmap" 28 | 29 | set ylabel "P999 Latency (us)" 30 | plot [0:17] [0:] \ 31 | "chashmap-writeheavy.txt" using 2:28 with lp ti "chashmap",\ 32 | "contrie-writeheavy.txt" using 2:28 with lp ti "contrie",\ 33 | "dashmap-writeheavy.txt" using 2:28 with lp ti "dashmap",\ 34 | "flurry-writeheavy.txt" using 2:28 with lp ti "flurry",\ 35 | "papaya-writeheavy.txt" using 2:28 with lp ti "papaya",\ 36 | "scchashmap-writeheavy.txt" using 2:28 with lp ti "scchashmap",\ 37 | "mutex_hashmap-writeheavy.txt" using 2:28 with lp ti "mutexhashmap",\ 38 | "rwlock_hashmap-writeheavy.txt" using 2:28 with lp ti "rwlockhashmap" 39 | 40 | unset multiplot 41 | set output "latency-readpopular.pdf" 42 | set multiplot layout 1,3 43 | set ylabel "Average Latency (us)" 44 | plot [0:17] [0:] \ 45 | "chashmap-readpopular.txt" using 2:20 with lp ti "chashmap",\ 46 | "contrie-readpopular.txt" using 2:20 with lp ti "contrie",\ 47 | "dashmap-readpopular.txt" using 2:20 with lp ti "dashmap",\ 48 | "flurry-readpopular.txt" using 2:20 with lp ti "flurry",\ 49 | "papaya-readpopular.txt" using 2:20 with lp ti "papaya",\ 50 | "scchashmap-readpopular.txt" using 2:20 with lp ti "scchashmap",\ 51 | "mutex_hashmap-readpopular.txt" using 2:20 with lp ti "mutexhashmap",\ 52 | "rwlock_hashmap-readpopular.txt" using 2:20 with lp ti "rwlockhashmap" 53 | 54 | set ylabel "P99 Latency (us)" 55 | plot [0:17] [0:] \ 56 | "chashmap-readpopular.txt" using 2:26 with lp ti "chashmap",\ 57 | "contrie-readpopular.txt" using 2:26 with lp ti "contrie",\ 58 | "dashmap-readpopular.txt" using 2:26 with lp ti "dashmap",\ 59 | "flurry-readpopular.txt" using 2:26 with lp ti "flurry",\ 60 | "papaya-readpopular.txt" using 2:26 with lp ti "papaya",\ 61 | "scchashmap-readpopular.txt" using 2:26 with lp ti "scchashmap",\ 62 | "mutex_hashmap-readpopular.txt" using 2:26 with lp ti "mutexhashmap",\ 63 | "rwlock_hashmap-readpopular.txt" using 2:26 with lp ti "rwlockhashmap" 64 | 65 | set ylabel "P999 Latency (us)" 66 | plot [0:17] [0:] \ 67 | "chashmap-readpopular.txt" using 2:28 with lp ti "chashmap",\ 68 | "contrie-readpopular.txt" using 2:28 with lp ti "contrie",\ 69 | "dashmap-readpopular.txt" using 2:28 with lp ti "dashmap",\ 70 | "flurry-readpopular.txt" using 2:28 with lp ti "flurry",\ 71 | "papaya-readpopular.txt" using 2:28 with lp ti "papaya",\ 72 | "scchashmap-readpopular.txt" using 2:28 with lp ti "scchashmap",\ 73 | "mutex_hashmap-readpopular.txt" using 2:28 with lp ti "mutexhashmap",\ 74 | "rwlock_hashmap-readpopular.txt" using 2:28 with lp ti "rwlockhashmap" 75 | -------------------------------------------------------------------------------- /src/cmdline.rs: -------------------------------------------------------------------------------- 1 | use crate::stores::Registry; 2 | use clap::ValueHint::FilePath; 3 | use clap::{Args, Parser, Subcommand}; 4 | use log::debug; 5 | use std::fs::read_to_string; 6 | use std::sync::mpsc::channel; 7 | 8 | #[derive(Args, Debug)] 9 | struct BenchArgs { 10 | #[arg(short = 's')] 11 | #[arg(value_hint = FilePath)] 12 | #[arg(help = "Path to the key-value store's TOML config file")] 13 | store_config: String, 14 | 15 | #[arg(short = 'b')] 16 | #[arg(value_hint = FilePath)] 17 | #[arg(help = "Path to the benchmark's TOML config file")] 18 | benchmark_config: String, 19 | } 20 | 21 | #[derive(Args, Debug)] 22 | struct ServerArgs { 23 | #[arg(short = 'a', default_value = "0.0.0.0")] 24 | #[arg(help = "Bind address")] 25 | host: String, 26 | 27 | #[arg(short = 'p', default_value = "9000")] 28 | #[arg(help = "Bind port")] 29 | port: String, 30 | 31 | #[arg(short = 's')] 32 | #[arg(value_hint = FilePath)] 33 | #[arg(help = "Path to the key-value store's TOML config file")] 34 | store_config: String, 35 | 36 | #[arg(short = 'n', default_value_t = 1)] 37 | #[arg(help = "Number of worker threads")] 38 | workers: usize, 39 | } 40 | 41 | #[derive(Parser, Debug)] 42 | #[command(version, about)] 43 | struct Cli { 44 | #[command(subcommand)] 45 | command: Commands, 46 | } 47 | 48 | #[derive(Subcommand, Debug)] 49 | enum Commands { 50 | #[command(about = "Run a benchmark")] 51 | Bench(BenchArgs), 52 | #[command(about = "Start a key-value server")] 53 | Server(ServerArgs), 54 | #[command(about = "List all registered key-value stores")] 55 | List, 56 | } 57 | 58 | fn bench_cli(args: &BenchArgs) { 59 | let opt: String = { 60 | let s = args.store_config.clone(); 61 | let b = args.benchmark_config.clone(); 62 | read_to_string(s.as_str()).unwrap() + "\n" + &read_to_string(b.as_str()).unwrap() 63 | }; 64 | 65 | let (map, phases) = crate::bench::init(&opt); 66 | map.bench(&phases); 67 | } 68 | 69 | fn server_cli(args: &ServerArgs) { 70 | let host = &args.host; 71 | let port = &args.port; 72 | let nr_workers = args.workers; 73 | 74 | let opt: String = read_to_string(args.store_config.as_str()).unwrap(); 75 | let map = crate::server::init(&opt); 76 | 77 | let (stop_tx, stop_rx) = channel(); 78 | let (grace_tx, grace_rx) = channel(); 79 | 80 | ctrlc::set_handler(move || { 81 | assert!(stop_tx.send(()).is_ok()); 82 | debug!("SIGINT received and stop message sent to server"); 83 | }) 84 | .expect("Error setting Ctrl-C handler for server"); 85 | 86 | map.server(&host, &port, nr_workers, stop_rx, grace_tx); 87 | 88 | assert!(grace_rx.recv().is_ok()); 89 | debug!("All server threads have been shut down gracefully, exit"); 90 | } 91 | 92 | fn list_cli() { 93 | for r in inventory::iter:: { 94 | println!("Registered map: {}", r.name); 95 | } 96 | } 97 | 98 | /// The default command line interface. 99 | /// 100 | /// This function is public and can be called in a different crate. For example, one can integrate 101 | /// their own key-value stores by registering the constructor function. Then, adding this function 102 | /// will produce a benchmark binary the has the same usage as the one in this crate. 103 | /// 104 | /// ## Usage 105 | /// 106 | /// To get the usage of the command line interface, users can run: 107 | /// 108 | /// ```bash 109 | /// kvbench -h 110 | /// ``` 111 | /// 112 | /// The interface supports three modes, `bench`, `server` and `list`. 113 | /// 114 | /// ### Benchmark Mode 115 | /// 116 | /// Usage: 117 | /// 118 | /// ```bash 119 | /// kvbench bench -s -b 120 | /// ``` 121 | /// 122 | /// Where `STORE_CONFIG` and `BENCH_CONFIG` are the paths to the key-value store and benchmark 123 | /// configuration files, respectively. 124 | /// 125 | /// ### Server mode 126 | /// 127 | /// Usage: 128 | /// 129 | /// ```bash 130 | /// kvbench server -s -a -p -n 131 | /// ``` 132 | /// 133 | /// Where `STORE_CONFIG` is the path of the key-value store configuration file. 134 | /// 135 | /// The default `HOST` and `PORT` are `0.0.0.0` and `9000`. By default, the server will spawn one 136 | /// worker thread only for incoming connections. You can adjust the number of worker threads by 137 | /// specifying `-n`. 138 | /// 139 | /// ### List mode 140 | /// 141 | /// Usage: 142 | /// ``` bash 143 | /// kvbench list 144 | /// ``` 145 | /// 146 | /// This command lists all registered key-value stores' names. 147 | 148 | pub fn cmdline() { 149 | env_logger::init(); 150 | let cli = Cli::parse(); 151 | debug!("Starting kvbench with args: {:?}", cli); 152 | match cli.command { 153 | Commands::Bench(args) => bench_cli(&args), 154 | Commands::Server(args) => server_cli(&args), 155 | Commands::List => list_cli(), 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/stores/remotesharded.rs: -------------------------------------------------------------------------------- 1 | //! A client of a client-side sharded key-value server. Each endpoint points to an independent 2 | //! store. But the client will shard the keys internally and send requests to the correct shard. 3 | //! The server can be backed by any stores available. However, the shading method is hash-based by 4 | //! default. Therefore, range queries are not supported. 5 | //! 6 | //! ## Configuration Format 7 | //! 8 | //! ``` toml 9 | //! [map] 10 | //! name = "remotesharded" 11 | //! 12 | //! [[map.addr]] 13 | //! host = "..." # host 1 14 | //! port = "..." # port 1 15 | //! 16 | //! [[map.addr]] 17 | //! host = "..." # host 2 18 | //! port = "..." # port 2 19 | //! ``` 20 | //! 21 | //! This store is [`AsyncKVMap`]. 22 | 23 | use crate::stores::hashmap::shard; 24 | use crate::stores::remote::{RemoteMap, RemoteMapOpt}; 25 | use crate::stores::{BenchKVMap, Registry}; 26 | use crate::*; 27 | use serde::Deserialize; 28 | use std::rc::Rc; 29 | 30 | pub struct RemoteShardedMap { 31 | maps: Vec, 32 | } 33 | 34 | pub struct RemoteShardedMapHandle(Vec>); 35 | 36 | #[derive(Deserialize)] 37 | pub struct RemoteShardedMapOpt { 38 | addr: Vec, 39 | } 40 | 41 | impl RemoteShardedMap { 42 | pub fn new(opt: &RemoteShardedMapOpt) -> Self { 43 | Self { 44 | maps: opt.addr.iter().map(|a| RemoteMap::new(a)).collect(), 45 | } 46 | } 47 | 48 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 49 | let opt: RemoteShardedMapOpt = opt.clone().try_into().unwrap(); 50 | BenchKVMap::Async(Arc::new(Box::new(Self::new(&opt)))) 51 | } 52 | } 53 | 54 | impl AsyncKVMap for RemoteShardedMap { 55 | fn handle(&self, responder: Rc) -> Box { 56 | let nr_shards = self.maps.len(); 57 | let handles = (0..nr_shards) 58 | .into_iter() 59 | .map(|i| self.maps[i].handle(responder.clone())) 60 | .collect(); 61 | Box::new(RemoteShardedMapHandle(handles)) 62 | } 63 | } 64 | 65 | fn shard_requests(requests: &Vec, nr_shards: usize) -> Vec> { 66 | let mut ret: Vec> = (0..nr_shards) 67 | .into_iter() 68 | .map(|_| Vec::::new()) 69 | .collect(); 70 | for r in requests.iter() { 71 | match &r.op { 72 | Operation::Set { key, value: _ } => ret[shard(key, nr_shards)].push(r.clone()), 73 | Operation::Get { key } => ret[shard(key, nr_shards)].push(r.clone()), 74 | Operation::Delete { key } => ret[shard(key, nr_shards)].push(r.clone()), 75 | Operation::Scan { key: _, n: _ } => { 76 | unimplemented!("remotesharded doesn't support range query") 77 | } 78 | } 79 | } 80 | ret 81 | } 82 | 83 | impl AsyncKVMapHandle for RemoteShardedMapHandle { 84 | fn submit(&mut self, requests: &Vec) { 85 | let sharded_requests = shard_requests(requests, self.0.len()); 86 | for (shard, req) in sharded_requests.iter().enumerate() { 87 | self.0[shard].submit(req); 88 | } 89 | } 90 | 91 | fn drain(&mut self) { 92 | for r in self.0.iter_mut() { 93 | r.drain(); 94 | } 95 | } 96 | } 97 | 98 | inventory::submit! { 99 | Registry::new("remoteshardedmap", RemoteShardedMap::new_benchkvmap) 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn parse() { 108 | let opt: RemoteShardedMapOpt = toml::from_str( 109 | r#" 110 | [[addr]] 111 | host = "127.0.0.1" 112 | port = "8080" 113 | 114 | [[addr]] 115 | host = "127.0.0.1" 116 | port = "8081" 117 | "#, 118 | ) 119 | .unwrap(); 120 | let map = RemoteShardedMap::new(&opt); 121 | assert_eq!(map.maps.len(), 2); 122 | } 123 | 124 | #[test] 125 | #[should_panic(expected = "not implemented")] 126 | fn shard_requests_invalid() { 127 | let mut requests = Vec::new(); 128 | requests.push(Request { 129 | id: 0, 130 | op: Operation::Scan { 131 | key: Box::new([0u8; 8]), 132 | n: 2, 133 | }, 134 | }); 135 | let _ = super::shard_requests(&requests, 10); 136 | } 137 | 138 | #[test] 139 | fn shard_requests() { 140 | let mut requests = Vec::new(); 141 | for i in 0..1000 { 142 | requests.push(Request { 143 | id: i, 144 | op: Operation::Get { 145 | key: Box::new(i.to_be_bytes()), 146 | }, 147 | }); 148 | requests.push(Request { 149 | id: i * 2 + 1, 150 | op: Operation::Set { 151 | key: Box::new(i.to_be_bytes()), 152 | value: Box::new([0u8; 16]), 153 | }, 154 | }); 155 | } 156 | let sharded_requests = super::shard_requests(&requests, 10); 157 | assert_eq!(sharded_requests.len(), 10); 158 | let mut count = 0; 159 | for r in sharded_requests.iter() { 160 | count += r.len(); 161 | } 162 | assert_eq!(count, 2000); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/stores/hashmap.rs: -------------------------------------------------------------------------------- 1 | //! Adapter implementation of [`hashbrown::HashMap`]. Internally sharded. 2 | //! 3 | //! ## Configuration Format 4 | //! 5 | //! ### [`Mutex`]-based: 6 | //! 7 | //! ``` toml 8 | //! [map] 9 | //! name = "mutex_hashmap" 10 | //! shards = ... # number of shards 11 | //! ``` 12 | //! 13 | //! This store is [`KVMap`]. 14 | //! 15 | //! ### [`RwLock`]-based: 16 | //! ``` toml 17 | //! [map] 18 | //! name = "rwlock_hashmap" 19 | //! shards = ... # number of shards 20 | //! ``` 21 | //! 22 | //! This store is [`KVMap`]. 23 | 24 | use crate::stores::{BenchKVMap, Registry}; 25 | use crate::*; 26 | use ::hashbrown::HashMap; 27 | use parking_lot::{Mutex, RwLock}; 28 | use serde::Deserialize; 29 | use std::hash::BuildHasher; 30 | use std::sync::Arc; 31 | 32 | /// Calculate the [`u64`] hash value of a given key using [`AHasher`]. 33 | pub fn hash(key: &[u8]) -> u64 { 34 | rustc_hash::FxBuildHasher.hash_one(key) 35 | } 36 | 37 | pub fn shard(key: &[u8], nr_shards: usize) -> usize { 38 | let hash = hash(key); 39 | usize::try_from(hash).unwrap() % nr_shards 40 | } 41 | 42 | /// A wrapper around raw [`HashMap`] with variable-sized keys and values. 43 | /// 44 | /// It is used as the building block of other types. Note that this is not [`KVMap`]. 45 | pub type BaseHashMap = HashMap, Box<[u8]>>; 46 | 47 | #[derive(Clone)] 48 | pub struct MutexHashMap { 49 | nr_shards: usize, 50 | shards: Arc>>, 51 | } 52 | 53 | #[derive(Deserialize)] 54 | pub struct MutexHashMapOpt { 55 | pub shards: usize, 56 | } 57 | 58 | impl MutexHashMap { 59 | pub fn new(opt: &MutexHashMapOpt) -> Self { 60 | let nr_shards = opt.shards; 61 | let mut shards = Vec::>::with_capacity(nr_shards); 62 | for _ in 0..nr_shards { 63 | shards.push(Mutex::new(BaseHashMap::new())); 64 | } 65 | let shards = Arc::new(shards); 66 | Self { nr_shards, shards } 67 | } 68 | 69 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 70 | let opt: MutexHashMapOpt = opt.clone().try_into().unwrap(); 71 | BenchKVMap::Regular(Arc::new(Box::new(Self::new(&opt)))) 72 | } 73 | } 74 | 75 | impl KVMap for MutexHashMap { 76 | fn handle(&self) -> Box { 77 | Box::new(self.clone()) 78 | } 79 | } 80 | 81 | impl KVMapHandle for MutexHashMap { 82 | fn set(&mut self, key: &[u8], value: &[u8]) { 83 | let sid = shard(key, self.nr_shards); 84 | self.shards[sid].lock().insert(key.into(), value.into()); 85 | } 86 | 87 | fn get(&mut self, key: &[u8]) -> Option> { 88 | let sid = shard(key, self.nr_shards); 89 | match self.shards[sid].lock().get(key) { 90 | Some(v) => Some(v.clone()), 91 | None => None, 92 | } 93 | } 94 | 95 | fn delete(&mut self, key: &[u8]) { 96 | let sid = shard(key, self.nr_shards); 97 | self.shards[sid].lock().remove(key); 98 | } 99 | 100 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 101 | unimplemented!("Range query is not supported"); 102 | } 103 | } 104 | 105 | inventory::submit! { 106 | Registry::new("mutex_hashmap", MutexHashMap::new_benchkvmap) 107 | } 108 | 109 | // }}} mutex_hashmap 110 | 111 | // {{{ rwlock_hashmap 112 | 113 | #[derive(Clone)] 114 | pub struct RwLockHashMap { 115 | pub nr_shards: usize, 116 | shards: Arc>>, 117 | } 118 | 119 | #[derive(Deserialize)] 120 | pub struct RwLockHashMapOpt { 121 | pub shards: usize, 122 | } 123 | 124 | impl RwLockHashMap { 125 | pub fn new(opt: &RwLockHashMapOpt) -> Self { 126 | let nr_shards = opt.shards; 127 | let mut shards = Vec::>::with_capacity(nr_shards); 128 | for _ in 0..nr_shards { 129 | shards.push(RwLock::new(BaseHashMap::new())); 130 | } 131 | let shards = Arc::new(shards); 132 | Self { nr_shards, shards } 133 | } 134 | 135 | pub fn new_benchkvmap(opt: &toml::Table) -> BenchKVMap { 136 | let opt: RwLockHashMapOpt = opt.clone().try_into().unwrap(); 137 | BenchKVMap::Regular(Arc::new(Box::new(Self::new(&opt)))) 138 | } 139 | } 140 | 141 | impl KVMap for RwLockHashMap { 142 | fn handle(&self) -> Box { 143 | Box::new(self.clone()) 144 | } 145 | } 146 | 147 | impl KVMapHandle for RwLockHashMap { 148 | fn set(&mut self, key: &[u8], value: &[u8]) { 149 | let sid = shard(key, self.nr_shards); 150 | self.shards[sid].write().insert(key.into(), value.into()); 151 | } 152 | 153 | fn get(&mut self, key: &[u8]) -> Option> { 154 | let sid = shard(key, self.nr_shards); 155 | match self.shards[sid].read().get(key) { 156 | Some(v) => Some(v.clone()), 157 | None => None, 158 | } 159 | } 160 | 161 | fn delete(&mut self, key: &[u8]) { 162 | let sid = shard(key, self.nr_shards); 163 | self.shards[sid].write().remove(key); 164 | } 165 | 166 | fn scan(&mut self, _key: &[u8], _n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)> { 167 | unimplemented!("Range query is not supported"); 168 | } 169 | } 170 | 171 | inventory::submit! { 172 | Registry::new("rwlock_hashmap", RwLockHashMap::new_benchkvmap) 173 | } 174 | 175 | // }}} rwlock_hashmap 176 | -------------------------------------------------------------------------------- /src/stores.rs: -------------------------------------------------------------------------------- 1 | //! Adapters for built-in and external key-value stores. 2 | //! 3 | //! ## Built-in Stores 4 | //! 5 | //! The usage of built-in stores can be found in the module-level documentations. Please note that 6 | //! it may be necessary to enable specific features of the crate to enable a certain built-in 7 | //! store. 8 | //! 9 | //! ## Registering New Stores 10 | //! 11 | //! When users would like to dynamically register new key-value stores from their own crate, first 12 | //! of all, they need to implement the corresponding [`KVMap`]/[`KVMapHandle`] 13 | //! (or [`AsyncKVMap`]/[`AsyncKVMapHandle`]) for the store. Then, they need to create a constructor 14 | //! function with a signature of `fn(&toml::Table) -> BenchKVMap`. 15 | //! 16 | //! The final step is to register the store's constructor (along with its name) using 17 | //! [`inventory`]. A minimal example would be: `inventory::submit! { Registry::new("name", 18 | //! constructor_fn) };`. 19 | //! 20 | //! The source code of all built-in stores provide good examples on this process. 21 | 22 | use crate::bench::{bench_async, bench_regular, Benchmark}; 23 | use crate::server::{server_async, server_regular}; 24 | use crate::*; 25 | use hashbrown::HashMap; 26 | use log::debug; 27 | use std::sync::mpsc::{Receiver, Sender}; 28 | use std::sync::Arc; 29 | use toml::Table; 30 | 31 | /// A unified enum for a created key-value store that is ready to run. 32 | pub enum BenchKVMap { 33 | Regular(Arc>), 34 | Async(Arc>), 35 | } 36 | 37 | impl BenchKVMap { 38 | pub(crate) fn bench(&self, phases: &Vec>) { 39 | match self { 40 | BenchKVMap::Regular(map) => { 41 | bench_regular(map.clone(), phases); 42 | } 43 | BenchKVMap::Async(map) => { 44 | bench_async(map.clone(), phases); 45 | } 46 | }; 47 | } 48 | 49 | pub(crate) fn server( 50 | &self, 51 | host: &str, 52 | port: &str, 53 | nr_workers: usize, 54 | stop_rx: Receiver<()>, 55 | grace_tx: Sender<()>, 56 | ) { 57 | match self { 58 | BenchKVMap::Regular(map) => { 59 | server_regular(map.clone(), host, port, nr_workers, stop_rx, grace_tx); 60 | } 61 | BenchKVMap::Async(map) => { 62 | server_async(map.clone(), host, port, nr_workers, stop_rx, grace_tx); 63 | } 64 | } 65 | } 66 | } 67 | 68 | /// The centralized registry that maps the name of newly added key-value store to its constructor 69 | /// function. 70 | /// 71 | /// A user-defined store can use the [`inventory::submit!`] macro to register their own stores to 72 | /// be used in the benchmark framework. 73 | pub struct Registry<'a> { 74 | pub(crate) name: &'a str, 75 | constructor: fn(&Table) -> BenchKVMap, 76 | } 77 | 78 | impl<'a> Registry<'a> { 79 | pub const fn new(name: &'a str, constructor: fn(&Table) -> BenchKVMap) -> Self { 80 | Self { name, constructor } 81 | } 82 | } 83 | 84 | inventory::collect!(Registry<'static>); 85 | 86 | /// An aggregated option enum that can be parsed from a TOML string. It contains all necessary 87 | /// parameters for each type of maps to be created. 88 | #[derive(Deserialize, Clone, Debug)] 89 | pub(crate) struct BenchKVMapOpt { 90 | name: String, 91 | #[serde(flatten)] 92 | opt: Table, 93 | } 94 | 95 | impl BenchKVMap { 96 | pub(crate) fn new(opt: &BenchKVMapOpt) -> BenchKVMap { 97 | // construct the hashmap.. this will be done every time 98 | let mut registered: HashMap<&'static str, fn(&Table) -> BenchKVMap> = HashMap::new(); 99 | for r in inventory::iter:: { 100 | debug!("Adding supported kvmap: {}", r.name); 101 | assert!(registered.insert(r.name, r.constructor).is_none()); // no existing name 102 | } 103 | let f = registered.get(opt.name.as_str()).unwrap_or_else(|| { 104 | panic!("map {} not found in registry", opt.name); 105 | }); 106 | f(&opt.opt) 107 | } 108 | } 109 | 110 | pub mod btreemap; 111 | #[cfg(feature = "chashmap")] 112 | pub mod chashmap; 113 | #[cfg(feature = "contrie")] 114 | pub mod contrie; 115 | #[cfg(feature = "dashmap")] 116 | pub mod dashmap; 117 | #[cfg(feature = "flurry")] 118 | pub mod flurry; 119 | pub mod hashmap; 120 | pub mod null; 121 | #[cfg(feature = "papaya")] 122 | pub mod papaya; 123 | pub mod remote; 124 | pub mod remotereplicated; 125 | pub mod remotesharded; 126 | #[cfg(feature = "rocksdb")] 127 | pub mod rocksdb; 128 | #[cfg(feature = "scc")] 129 | pub mod scc; 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | fn _map_test(map: &impl KVMap) { 136 | let mut handle = map.handle(); 137 | // insert + get 138 | handle.set(b"foo", b"bar"); 139 | assert_eq!(handle.get(b"foo"), Some((*b"bar").into())); 140 | assert_eq!(handle.get(b"f00"), None); 141 | 142 | // update 143 | handle.set(b"foo", b"0ar"); 144 | assert_eq!(handle.get(b"foo"), Some((*b"0ar").into())); 145 | 146 | // delete 147 | handle.delete(b"foo"); 148 | assert_eq!(handle.get(b"foo"), None); 149 | } 150 | 151 | fn _map_test_scan(map: &impl KVMap) { 152 | let mut handle = map.handle(); 153 | for i in 10000..20000usize { 154 | let bytes = i.clone().to_be_bytes(); 155 | handle.set(&bytes, &bytes); 156 | } 157 | 158 | // query 10000 next 10000 159 | let v = handle.scan(&10000_usize.to_be_bytes(), 10000); 160 | assert_eq!(v.len(), 10000); 161 | for i in 10000..20000usize { 162 | let bytes = i.clone().to_be_bytes(); 163 | assert_eq!(*v[i - 10000].0, bytes); 164 | assert_eq!(*v[i - 10000].1, bytes); 165 | } 166 | 167 | // query 10000 next 20000, should have 10000 168 | let v = handle.scan(&10000_usize.to_be_bytes(), 20000); 169 | assert_eq!(v.len(), 10000); 170 | for i in 10000..20000usize { 171 | let bytes = i.clone().to_be_bytes(); 172 | assert_eq!(*v[i - 10000].0, bytes); 173 | assert_eq!(*v[i - 10000].1, bytes); 174 | } 175 | 176 | // query 10000 next 5, should have 5 177 | let v = handle.scan(&10000_usize.to_be_bytes(), 5); 178 | assert_eq!(v.len(), 5); 179 | for i in 10000..10005usize { 180 | let bytes = i.clone().to_be_bytes(); 181 | assert_eq!(*v[i - 10000].0, bytes); 182 | assert_eq!(*v[i - 10000].1, bytes); 183 | } 184 | 185 | // query 13333 next 444, should have 444 186 | let v = handle.scan(&13333_usize.to_be_bytes(), 444); 187 | assert_eq!(v.len(), 444); 188 | for i in 13333..13777usize { 189 | let bytes = i.clone().to_be_bytes(); 190 | assert_eq!(*v[i - 13333].0, bytes); 191 | assert_eq!(*v[i - 13333].1, bytes); 192 | } 193 | 194 | // query 13333 next 0, should have 0 195 | let v = handle.scan(&13333_usize.to_be_bytes(), 0); 196 | assert_eq!(v.len(), 0); 197 | 198 | // query 20000 next 10000, should have 0 199 | let v = handle.scan(&20000_usize.to_be_bytes(), 10000); 200 | assert_eq!(v.len(), 0); 201 | 202 | // query 0 next 5000, should have 5000 203 | let v = handle.scan(&0_usize.to_be_bytes(), 5000); 204 | assert_eq!(v.len(), 5000); 205 | for i in 10000..15000usize { 206 | let bytes = i.clone().to_be_bytes(); 207 | assert_eq!(*v[i - 10000].0, bytes); 208 | assert_eq!(*v[i - 10000].1, bytes); 209 | } 210 | 211 | // query 8000 next 5000, should have 5000 212 | let v = handle.scan(&8000_usize.to_be_bytes(), 5000); 213 | assert_eq!(v.len(), 5000); 214 | for i in 10000..15000usize { 215 | let bytes = i.clone().to_be_bytes(); 216 | assert_eq!(*v[i - 10000].0, bytes); 217 | assert_eq!(*v[i - 10000].1, bytes); 218 | } 219 | } 220 | 221 | #[test] 222 | fn mutex_btreemap() { 223 | let mut map = btreemap::MutexBTreeMap::new(); 224 | _map_test(&mut map); 225 | } 226 | 227 | #[test] 228 | fn rwlock_btreemap() { 229 | let mut map = btreemap::RwLockBTreeMap::new(); 230 | _map_test(&mut map); 231 | } 232 | 233 | #[test] 234 | #[cfg(feature = "chashmap")] 235 | fn chashmap() { 236 | let mut map = chashmap::CHashMap::new(); 237 | _map_test(&mut map); 238 | } 239 | 240 | #[test] 241 | #[cfg(feature = "contrie")] 242 | fn contrie() { 243 | let mut map = contrie::Contrie::new(); 244 | _map_test(&mut map); 245 | } 246 | 247 | #[test] 248 | #[cfg(feature = "dashmap")] 249 | fn dashmap() { 250 | let mut map = dashmap::DashMap::new(); 251 | _map_test(&mut map); 252 | } 253 | 254 | #[test] 255 | #[cfg(feature = "flurry")] 256 | fn flurry() { 257 | let mut map = flurry::Flurry::new(); 258 | _map_test(&mut map); 259 | } 260 | 261 | #[test] 262 | fn mutex_hashmap() { 263 | let opt = hashmap::MutexHashMapOpt { shards: 512 }; 264 | let mut map = hashmap::MutexHashMap::new(&opt); 265 | _map_test(&mut map); 266 | } 267 | 268 | #[test] 269 | fn rwlock_hashmap() { 270 | let opt = hashmap::RwLockHashMapOpt { shards: 512 }; 271 | let mut map = hashmap::RwLockHashMap::new(&opt); 272 | _map_test(&mut map); 273 | } 274 | 275 | #[test] 276 | #[cfg(feature = "papaya")] 277 | fn papaya() { 278 | let mut map = papaya::Papaya::new(); 279 | _map_test(&mut map); 280 | } 281 | 282 | #[test] 283 | fn nullmap() { 284 | let mut map = null::NullMap::new(); 285 | assert!(map.get("foo".as_bytes().into()).is_none()); 286 | } 287 | 288 | #[test] 289 | #[cfg(feature = "scc")] 290 | fn scchashmap() { 291 | let mut map = scc::SccHashMap::new(); 292 | _map_test(&mut map); 293 | } 294 | 295 | #[test] 296 | #[cfg(feature = "rocksdb")] 297 | fn rocksdb() { 298 | let tmp_dir = tempfile::tempdir().unwrap(); 299 | let opt = rocksdb::RocksDBOpt { 300 | path: tmp_dir.path().to_str().unwrap().to_string(), 301 | }; 302 | let mut map = rocksdb::RocksDB::new(&opt); 303 | _map_test(&mut map); 304 | } 305 | 306 | #[test] 307 | #[cfg(feature = "rocksdb")] 308 | fn rocksdb_scan() { 309 | let tmp_dir = tempfile::tempdir().unwrap(); 310 | let opt = rocksdb::RocksDBOpt { 311 | path: tmp_dir.path().to_str().unwrap().to_string(), 312 | }; 313 | let mut map = rocksdb::RocksDB::new(&opt); 314 | _map_test_scan(&mut map); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | 3 | //! A benchmark framework designed for testing key-value stores with easily customizable 4 | //! workloads. 5 | //! 6 | //! Key features: 7 | //! 8 | //! 1. Flexible and ergonomic control over benchmark specifications using TOML configuration files. 9 | //! 2. Collecting diverse metrics, including throughput, latency (w/ CDF), and rate-limited latency. 10 | //! 3. One-shot execution of multiple benchmark steps with different properties. 11 | //! 4. Various built-in key-value stores in place as well as a client/server implementation. 12 | //! 5. Highly extensible and can be seamlessly integrated into your own store. 13 | //! 14 | //! # Benchmark Configuration 15 | //! 16 | //! A benchmark in kvbench consists of one or more benchmark runs, termed as *phases*. 17 | //! Phases will be run sequentially following their order in the configuration file. 18 | //! 19 | //! A benchmark configuration file is formatted in TOML. It consists of the definition of each 20 | //! phase in an array named `benchmark`, so the configuration of each phase starts with 21 | //! `[[benchmark]]`. The file also optionally contains a `[global]` section which will override the 22 | //! unspecified field in each phase. This can eliminate redundant options in each phase, for 23 | //! example, when those options are the same across the board. 24 | //! 25 | //! A configuration file generally looks like the following: 26 | //! 27 | //! ```toml 28 | //! [global] 29 | //! # global options 30 | //! 31 | //! [[benchmark]] 32 | //! # phase 1 configuration 33 | //! 34 | //! [[benchmark]] 35 | //! # phase 2 configuration 36 | //! 37 | //! ... 38 | //! ``` 39 | //! Options in `[global]` section can also be overwritten via environment variables without 40 | //! modifying the TOML file. For example, if the user needs to override `x` in `[global]`, one can 41 | //! set the environment variable `global.x` (case insensitive). This is helpful when the user would 42 | //! like to run different benchmarks when changing only a few options using a shell script. 43 | //! 44 | //! **Reference** 45 | //! 46 | //! - [`BenchmarkOpt`]: the available options for benchmark phase configuration. 47 | //! - [`GlobalOpt`]: the available options for global configuration. 48 | //! 49 | //! # Key-Value Store Configuration 50 | //! 51 | //! In addition to the specification of the benchmark itself, kvbench also requires the 52 | //! parameters of the key-value store it runs against. Only one key-value store runs at a time. 53 | //! 54 | //! The configuration of a key-value store is stored in a dictionary `map`. 55 | //! A store's configuration file looks like the following: 56 | //! 57 | //! ```toml 58 | //! [map] 59 | //! name = "..." 60 | //! # option1 = ... 61 | //! # option2 = ... 62 | //! 63 | //! ... 64 | //! ``` 65 | //! The field `name` must be given and it should be equal to the name registered by the store. 66 | //! Other than `name`, all the fields are parsed as a string map and will be passed to the 67 | //! store's constructor function. The options in `[map]` section can also be overwritten via 68 | //! environment variables (e.g., setting `map.x` overrides property `x`). 69 | //! 70 | //! **Reference** 71 | //! 72 | //! - [`mod@stores`]: the available options for built-in stores and how to register new stores. 73 | //! 74 | //! # Run a Benchmark 75 | //! 76 | //! Once the configuration files of the benchmark along with the key-value store are ready, a 77 | //! benchmark can be started by using the `bench` mode of the built-in command-line interface. 78 | //! 79 | //! **Reference** 80 | //! 81 | //! - [`cmdline()`]: the usage of the default command-line interface. 82 | //! 83 | //! # Metrics Collection 84 | //! 85 | //! Currently, all outputs are in plain text format. This makes the output easy to process using 86 | //! shell scripts and tools including gnuplot. If there are new data added to the output, it 87 | //! will be appended at the end of existing entries (but before `cdf` if it exists, see below) 88 | //! to make sure outputs from old versions can still be processed without changes. 89 | //! 90 | //! ## Throughput-only Output (default case) 91 | //! 92 | //! When measuring throughput, an output may look like the following: 93 | //! ```txt 94 | //! phase 0 repeat 0 duration 1.00 elapsed 1.00 total 1000000 mops 1.00 95 | //! phase 0 repeat 1 duration 1.00 elapsed 2.00 total 1000000 mops 1.00 96 | //! phase 0 repeat 2 duration 1.00 elapsed 3.00 total 1000000 mops 1.00 97 | //! phase 0 finish . duration 1.00 elapsed 3.00 total 3000000 mops 1.00 98 | //! ``` 99 | //! 100 | //! The general format is: 101 | //! 102 | //! ```txt 103 | //! phase

repeat duration elapsed total mops 104 | //! ``` 105 | //! 106 | //! Where: 107 | //! 108 | //! - `

`: phase id. 109 | //! - ``: repeat id in a phase, or string `finish .`, if the line is the aggregated report 110 | //! of a whole phase. 111 | //! - ``: the duration of the repeat/phase, in seconds. 112 | //! - ``: the total elapsed seconds since the starting of the program. 113 | //! - ``: the total key-value operations executed by all worker threads in the repeat/phase. 114 | //! - ``: followed by the throughput in million operations per second of the repeat/phase. 115 | //! 116 | //! ## Throughput + Latency Output (when `latency` is `true`) 117 | //! 118 | //! When latency measurement is enabled, the latency metrics shall be printed at the end of each 119 | //! benchmark. It is not shown after each repeat, because unlike throughput which is a singleton 120 | //! value at a given time, latency is a set of values and it usually matters only when we aggregate 121 | //! a lot of them. The output format in this case is generally the same as throughput-only 122 | //! measurements, but the `finish` line has extra output like the following: 123 | //! 124 | //! ```txt 125 | //! phase 0 repeat 0 duration 1.00 elapsed 1.00 total 1000000 mops 1.00 126 | //! phase 0 repeat 1 duration 1.00 elapsed 2.00 total 1000000 mops 1.00 127 | //! phase 0 repeat 2 duration 1.00 elapsed 3.00 total 1000000 mops 1.00 128 | //! phase 0 finish . duration 1.00 elapsed 3.00 total 3000000 mops 1.00 min_us 0.05 max_us 100.00 avg_us 50.00 p50_us 50.00 p95_us 95.00 p99_us 99.00 p999_us 100.00 129 | //! ``` 130 | //! 131 | //! The extra output on the last line has a format of: 132 | //! 133 | //! ```txt 134 | //! min_us max_us avg_us p50_us p95_us p99_us

p999_us 135 | //! ``` 136 | //! 137 | //! Where (all units are microseconds): 138 | //! 139 | //! - ``: minimum latency 140 | //! - ``: maximum latency 141 | //! - ``: mean latency 142 | //! - ``: median latency (50% percentile) 143 | //! - ``: P95 latency 144 | //! - `

`: P99 latency 145 | //! - ``: P999 latency (99.9%) 146 | //! 147 | //! ## Throughput + Latency + Latency CDF Mode (when both `latency` and `cdf` are `true`) 148 | //! 149 | //! When `cdf` is enabled, the latency CDF data will be printed at the end of the same line as the 150 | //! latency metrics above. In that case, the output will be like the following: 151 | //! 152 | //! ```txt 153 | //! phase 0 repeat 0 duration 1.00 elapsed 1.00 total 1000000 mops 1.00 154 | //! phase 0 repeat 1 duration 1.00 elapsed 2.00 total 1000000 mops 1.00 155 | //! phase 0 repeat 2 duration 1.00 elapsed 3.00 total 1000000 mops 1.00 156 | //! phase 0 finish . duration 1.00 elapsed 3.00 total 3000000 mops 1.00 min_us 0.05 max_us 100.00 avg_us 50.00 p50_us 50.00 p95_us 95.00 p99_us 99.00 p999_us 100.00 cdf_us percentile ... 157 | //! ``` 158 | //! Since the latency metrics vary a lot between different benchmarks/runs, the number of data 159 | //! points of the CDF is different. Therefore, it is printed at the end of the output only. It is 160 | //! printed as a tuple of ` ` where `` is the latency in microseconds and 161 | //! `` is the percentile of the accumulated operations with latency higher than between 162 | //! ` - 1` and ``, inclusively, ranging from 0 to 100 (two digit precision). 163 | //! There can be arbitrary number of tuples. The output ends when the maximum recorded latency is 164 | //! reached. 165 | //! 166 | //! An example of the CDF data will look like: 167 | //! 168 | //! ```txt 169 | //! cdf_us percentile 1 0.00 2 0.00 3 0.00 4 10.00 5 20.00 6 20.00 ... 170 | //! ``` 171 | //! 172 | //! It means there are not data points at 1/2/3 microseconds. At 4 microseconds, there are 10% data 173 | //! points. At 5 microseconds, there are another 10% data points which makes the total percentile 174 | //! 20.00. At 6 microseconds, there are no data points so the percentile is still 20.00. Users can 175 | //! post-process the output and make a smooth CDF plot out of it. 176 | //! 177 | //! # Server Mode 178 | //! A key-value client/server implementation is available in kvbench. The server can be backed by 179 | //! an arbitrary key-value store defined by a TOML file as in a benchmark, and the server can be 180 | //! started using the `server` mode of the built-in command-line interface. 181 | //! 182 | //! To benchmark the server's performance, users can use the built-in client implementation. 183 | //! 184 | //! **Reference** 185 | //! 186 | //! - [`cmdline()`]: the usage of the default command-line interface. 187 | //! - [`stores::remote`]: the available options of the key-value store client. 188 | 189 | use serde::{Deserialize, Serialize}; 190 | use std::cell::RefCell; 191 | use std::rc::Rc; 192 | use std::sync::Arc; 193 | 194 | /// A synchronous, thread-safe key-value store. 195 | /// 196 | /// This trait is used for owned stores, with which a per-thread handle can be created. The default 197 | /// benchmark/server implementation is provided, unless the use case needs to use specific thread 198 | /// management implementations. 199 | pub trait KVMap: Send + Sync + 'static { 200 | /// Create a handle that can be referenced by different threads in the system. 201 | /// For most stores, this can just be done using an Arc. 202 | fn handle(&self) -> Box; 203 | 204 | fn thread(&self) -> Box { 205 | Box::new(self::thread::DefaultThread) 206 | } 207 | } 208 | 209 | /// A per-thread handle that references a [`KVMap`]. 210 | /// 211 | /// The handle is the real object that exposes a key-value interface. 212 | pub trait KVMapHandle { 213 | /// Adding a new key-value pair or blindly updating an existing key's value. 214 | fn set(&mut self, key: &[u8], value: &[u8]); 215 | 216 | /// Retrieving the value of a key if it exists. 217 | fn get(&mut self, key: &[u8]) -> Option>; 218 | 219 | /// Removing a key if it exists. 220 | fn delete(&mut self, key: &[u8]); 221 | 222 | /// Querying a range starting from the first key greater than or equal to the given key. 223 | fn scan(&mut self, key: &[u8], n: usize) -> Vec<(Box<[u8]>, Box<[u8]>)>; 224 | } 225 | 226 | /// A single operation that is applied to the key-value store. 227 | /// 228 | /// This trait is used mainly in [`AsyncKVMap`] and server/client implementation. 229 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 230 | pub enum Operation { 231 | /// Adding a new key-value pair or blindly updating an existing key's value. 232 | Set { key: Box<[u8]>, value: Box<[u8]> }, 233 | 234 | /// Retrieving the value of a key if it exists. 235 | Get { key: Box<[u8]> }, 236 | 237 | /// Removing a key if it exists. 238 | Delete { key: Box<[u8]> }, 239 | 240 | /// Querying a range starting from the first key greater than or equal to the given key. 241 | Scan { key: Box<[u8]>, n: usize }, 242 | } 243 | 244 | /// A request submitted by an asynchronous store. 245 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 246 | pub struct Request { 247 | /// The (usually unique) identifier of the request, or custom data. 248 | pub id: usize, 249 | 250 | /// The real payload that contains the operation. 251 | pub op: Operation, 252 | } 253 | 254 | /// A response received by an asynchronous store. 255 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 256 | pub struct Response { 257 | /// The `id` of the corresponding request. 258 | pub id: usize, 259 | 260 | /// The real payload that contains the potential returned value. 261 | /// 262 | /// - For a `SET` or `DELETE` request, this should be `None`. 263 | /// - For a `GET` request, this should contain the value of the key (single element). 264 | /// - For a `SCAN` request, this should contain the sequence of the range query results, ordered 265 | /// like `key0, value0, key1, value1 ...`. 266 | pub data: Option>>, 267 | } 268 | 269 | /// A non-blocking, thread-safe key-value map. 270 | /// 271 | /// Unlike [`KVMap`], [`AsyncKVMap`] works in request/response style. Where each handle needs to be 272 | /// created by registering an explicit responder that serves as the "callback" when the underlying 273 | /// routine produces a response. 274 | /// 275 | /// Specifically, for benchmark, each worker thread maintains just one handle, so the buffer is 276 | /// per-worker. For server, each worker may manage multiple connections. Each connection needs its 277 | /// own responder (we should not mix responses for different connections, obviously). Therefore, it 278 | /// creates a handle for each incoming connection, and maintains a responder for it. 279 | pub trait AsyncKVMap: Sync + Send + 'static { 280 | /// Create a handle that can be referenced by different threads in the system. Each handle 281 | /// corresponds to a shared `responder` that implements [`AsyncResponder`]. 282 | fn handle(&self, responder: Rc) -> Box; 283 | 284 | fn thread(&self) -> Box { 285 | Box::new(self::thread::DefaultThread) 286 | } 287 | } 288 | 289 | /// A per-thread handle that references a [`AsyncKVMap`]. 290 | /// 291 | /// The handle is the real object that exposes a key-value interface. 292 | pub trait AsyncKVMapHandle { 293 | /// Submit a batch of requests to the store and immediately return without responses. 294 | fn submit(&mut self, requests: &Vec); 295 | 296 | /// Try to fill more responses into the registered responder. This operation can be, for 297 | /// example, yield more CPU time for the worker thread to handle requests, or flush a buffer to 298 | /// get more responses. 299 | fn drain(&mut self); 300 | } 301 | 302 | /// An asynchronous entry point to callback when a response returns. 303 | pub trait AsyncResponder { 304 | /// Whenever a new response is returned, this method is called with a [`Response`] moved. 305 | fn callback(&self, response: Response); 306 | } 307 | 308 | impl AsyncResponder for RefCell> { 309 | fn callback(&self, response: Response) { 310 | self.borrow_mut().push(response); 311 | } 312 | } 313 | 314 | mod bench; 315 | mod cmdline; 316 | mod server; 317 | pub mod stores; 318 | pub mod thread; 319 | mod workload; 320 | 321 | pub use bench::{BenchmarkOpt, GlobalOpt}; 322 | pub use cmdline::cmdline; 323 | pub use workload::WorkloadOpt; 324 | 325 | pub extern crate inventory; 326 | pub extern crate toml; 327 | -------------------------------------------------------------------------------- /src/workload.rs: -------------------------------------------------------------------------------- 1 | //! Workload generator. 2 | 3 | use crate::Operation; 4 | use rand::distr::{Distribution, Uniform}; 5 | use rand::prelude::SliceRandom; 6 | use rand::Rng; 7 | use rand_distr::weighted::WeightedIndex; 8 | use rand_distr::Zipf; 9 | use serde::Deserialize; 10 | 11 | /// This is for internal use in the workload mod. It is essentially Operation without 12 | /// generated keys, values, or other parameters. They are generated based on a Mix defined below. 13 | #[derive(Clone)] 14 | enum OperationType { 15 | Set, 16 | Get, 17 | Delete, 18 | Scan, 19 | } 20 | 21 | /// Mix defines the percentages of operations, it consists of multiple supported operations 22 | /// and the total of each operation should be 100. 23 | /// As of now, it supports two access types, insert and read. 24 | #[derive(Debug)] 25 | struct Mix { 26 | dist: WeightedIndex, 27 | } 28 | 29 | impl Mix { 30 | fn new(set: u8, get: u8, delete: u8, scan: u8) -> Self { 31 | let dist = WeightedIndex::new(&[set, get, delete, scan]).unwrap(); 32 | Self { dist } 33 | } 34 | 35 | fn next(&self, rng: &mut impl Rng) -> OperationType { 36 | let ops = [ 37 | OperationType::Set, 38 | OperationType::Get, 39 | OperationType::Delete, 40 | OperationType::Scan, 41 | ]; 42 | ops[self.dist.sample(rng)].clone() 43 | } 44 | } 45 | 46 | /// The distribution of keys, more distributions might be added. 47 | #[derive(Debug)] 48 | enum KeyDistribution { 49 | Increment, 50 | Shuffle(Vec), 51 | Uniform(Uniform), 52 | Zipfian(Zipf, usize), 53 | ZipfianLatest(Zipf, usize, usize), 54 | } 55 | 56 | /// Key generator that takes care of synthetic keys based on a distribution. Currently it only 57 | /// generates fixed-sized keys based on the parameters of length and key space size. 58 | #[derive(Debug)] 59 | struct KeyGenerator { 60 | len: usize, 61 | min: usize, 62 | max: usize, 63 | keyspace: usize, 64 | serial: usize, 65 | dist: KeyDistribution, 66 | } 67 | 68 | /// Since we use `usize` for the numeric keys generated, the maximum key space size is limited by 69 | /// the platform. If the target platform is 32-bit, all possible keys would have already filled the 70 | /// memory, unless it is supporting a large persistent store, which is unlikely the case. 71 | const KEY_BYTES: usize = std::mem::size_of::(); 72 | 73 | impl KeyGenerator { 74 | fn new(len: usize, min: usize, max: usize, dist: KeyDistribution) -> Self { 75 | let keyspace = max - min; 76 | let serial = 0; 77 | Self { 78 | len, 79 | min, 80 | max, 81 | keyspace, 82 | serial, 83 | dist, 84 | } 85 | } 86 | 87 | fn new_increment(len: usize, min: usize, max: usize) -> Self { 88 | let dist = KeyDistribution::Increment; 89 | Self::new(len, min, max, dist) 90 | } 91 | 92 | fn new_shuffle(len: usize, min: usize, max: usize) -> Self { 93 | let mut shuffle = (0..(max - min)).collect::>(); 94 | shuffle.shuffle(&mut rand::rng()); 95 | let dist = KeyDistribution::Shuffle(shuffle); 96 | Self::new(len, min, max, dist) 97 | } 98 | 99 | fn new_uniform(len: usize, min: usize, max: usize) -> Self { 100 | let dist = KeyDistribution::Uniform(Uniform::new(0, max - min).unwrap()); 101 | Self::new(len, min, max, dist) 102 | } 103 | 104 | fn new_zipfian(len: usize, min: usize, max: usize, theta: f64, hotspot: f64) -> Self { 105 | let hotspot = (hotspot * (max - min - 1) as f64) as usize; // approx location for discrete keys 106 | let dist = KeyDistribution::Zipfian(Zipf::new((max - min) as f64, theta).unwrap(), hotspot); 107 | Self::new(len, min, max, dist) 108 | } 109 | 110 | fn new_zipfian_latest(len: usize, min: usize, max: usize, theta: f64, hotspot: f64) -> Self { 111 | let hotspot = (hotspot * (max - min - 1) as f64) as usize; // approx location for discrete keys 112 | let dist = KeyDistribution::ZipfianLatest( 113 | Zipf::new((max - min) as f64, theta).unwrap(), 114 | hotspot, 115 | 0, 116 | ); 117 | Self::new(len, min, max, dist) 118 | } 119 | 120 | fn next(&mut self, rng: &mut impl Rng) -> Box<[u8]> { 121 | let k = match self.dist { 122 | KeyDistribution::Increment => self.serial % self.keyspace, 123 | KeyDistribution::Shuffle(ref shuffle) => shuffle[self.serial % self.keyspace], 124 | KeyDistribution::Uniform(dist) => dist.sample(rng), 125 | KeyDistribution::Zipfian(dist, hotspot) => { 126 | // zipf starts at 1 127 | (dist.sample(rng) as usize - 1 + hotspot) % self.keyspace 128 | } 129 | KeyDistribution::ZipfianLatest(dist, hotspot, ref mut latest) => { 130 | // just like zipfian, but always store the latest key 131 | let sample = dist.sample(rng) as usize - 1; 132 | *latest = sample; 133 | (sample + hotspot) % self.keyspace 134 | } 135 | } + self.min; 136 | self.serial += 1; 137 | assert!(k < self.max); 138 | // fill 0s in the key to construct a key with length self.len 139 | let bytes = k.to_be_bytes(); 140 | // key will hold the final key which is a Box<[u8]> and here we just do the allocation 141 | let mut key: Box<[u8]> = (0..self.len).map(|_| 0u8).collect(); 142 | let len = self.len.min(KEY_BYTES); 143 | // copy from the big end to the beginning of the key slice 144 | key[0..len].copy_from_slice(&bytes[(KEY_BYTES - len)..KEY_BYTES]); 145 | key 146 | } 147 | } 148 | 149 | /// A set of workload parameters that can be deserialized from a TOML string. 150 | /// 151 | /// **Note 1**: If an option not explicitly marked optional and it is not specified by both the file 152 | /// and the global option, its default value will be applied. If it has no default value, an error 153 | /// will be raised. The precedence of a value is: file > global (after env overridden) > default. 154 | /// 155 | /// **Note 2**: the sum of all `*_perc` options must be equal to 100. 156 | #[derive(Deserialize, Clone, Debug, PartialEq)] 157 | pub struct WorkloadOpt { 158 | /// Percentage of `SET` operations. 159 | /// 160 | /// Must be a non-negative integer if given. 161 | /// 162 | /// Default: 0. 163 | pub set_perc: Option, 164 | 165 | /// Percentage of `GET` operations. 166 | /// 167 | /// Must be a non-negative integer if given. 168 | /// 169 | /// Default: 0. 170 | pub get_perc: Option, 171 | 172 | /// Percentage of `DELETE` operations. 173 | /// 174 | /// Must be a non-negative integer if given. 175 | /// 176 | /// Default: 0. 177 | pub del_perc: Option, 178 | 179 | /// Percentage of `SCAN` operations. 180 | /// 181 | /// Must be a non-negative integer if given. 182 | /// 183 | /// Default: 0. 184 | pub scan_perc: Option, 185 | 186 | /// The number of iterations per `SCAN`. 187 | /// 188 | /// Must be a positive integer if provided. 189 | /// 190 | /// Default: 10. 191 | pub scan_n: Option, 192 | 193 | /// Key length in bytes. 194 | /// 195 | /// Must be a positive integer. 196 | pub klen: Option, 197 | 198 | /// Value length in bytes. 199 | /// 200 | /// Must be a positive integer. 201 | pub vlen: Option, 202 | 203 | /// Minimum key. 204 | /// 205 | /// Must be a non-negative integer. 206 | pub kmin: Option, 207 | 208 | /// Maximum key. 209 | /// 210 | /// Must be greater than `kmin`. 211 | pub kmax: Option, 212 | 213 | /// Key distribution. 214 | /// 215 | /// - "increment": sequentially incrementing from `kmin` to `kmax`. 216 | /// - "incrementp": partitioned `increment`, where each thread takes a range of keys. For 217 | /// example, if there are two threads with `kmin` of 0 and `kmax` of 10, one thread will get an 218 | /// "increment" distribution from 0 to 5, and another one will get 6 to 10. 219 | /// - "shuffle": shuffled sequence from `kmin` to `kmax`. One key appears exactly once during 220 | /// an iteration of the whole key space. This is useful to randomly prefill keys. 221 | /// - "shufflep": partitioned `shuffle`, similar to "incrementp" but the keys are shuffled for 222 | /// each range/thread. 223 | /// - "uniform": uniformly random keys from `kmin` to `kmax`. 224 | /// - "zipfian": random keys from `kmin` to `kmax` following Zipfian distribution. 225 | /// - "latest": just like Zipfian but the hotspot is the latest key written to the store. 226 | pub dist: String, 227 | 228 | /// The theta parameter for Zipfian distribution. 229 | /// 230 | /// Default: 1.0. 231 | pub zipf_theta: Option, 232 | 233 | /// The hotspot location for Zipfian distribution. 234 | /// 235 | /// 0.0 means the first key. 0.5 means approximately the middle in the key space. 236 | /// 237 | /// Default: 0.0. 238 | pub zipf_hotspot: Option, 239 | } 240 | 241 | /// The minimal unit of workload context with its access pattern (mix and key generator). 242 | /// 243 | /// The values generated internally are fixed-sized only for now, similar to the keys. To 244 | /// pressurize the memory allocator, it might be a good idea to randomly adding a byte or two at 245 | /// each generated values. 246 | #[derive(Debug)] 247 | pub(crate) struct Workload { 248 | /// Percentage of different operations 249 | mix: Mix, 250 | /// Key generator based on distribution 251 | kgen: KeyGenerator, 252 | /// Scan length 253 | scan_n: usize, 254 | /// Value length for operations that need a value 255 | vlen: usize, 256 | /// How many operations have been access so far 257 | count: u64, 258 | } 259 | 260 | impl Workload { 261 | pub fn new(opt: &WorkloadOpt, thread_info: Option<(usize, usize)>) -> Self { 262 | // input sanity checks 263 | let set_perc = opt.set_perc.unwrap_or(0); 264 | let get_perc = opt.get_perc.unwrap_or(0); 265 | let del_perc = opt.del_perc.unwrap_or(0); 266 | let scan_perc = opt.scan_perc.unwrap_or(0); 267 | assert_eq!( 268 | set_perc + get_perc + del_perc + scan_perc, 269 | 100, 270 | "sum of ops in a mix should be 100" 271 | ); 272 | let scan_n = opt.scan_n.unwrap_or(10); 273 | let klen = opt.klen.expect("klen should be specified"); 274 | let vlen = opt.vlen.expect("vlen should be specified"); 275 | let kmin = opt.kmin.expect("kmin should be specified"); 276 | let kmax = opt.kmax.expect("kmax should be specified"); 277 | assert!(scan_n > 0, "scan size should be positive"); 278 | assert!(klen > 0, "klen should be positive"); 279 | assert!(kmax > kmin, "kmax should be greater than kmin"); 280 | 281 | let split_key_space = || { 282 | let (thread_id, nr_threads) = thread_info.expect("parallel keygen expects thread_info"); 283 | assert!(thread_id < nr_threads); 284 | let nr_keys_per = (kmax - kmin) / nr_threads; 285 | let kminp = kmin + thread_id * nr_keys_per; 286 | let kmaxp = if thread_id == nr_threads - 1 { 287 | kmax 288 | } else { 289 | kminp + nr_keys_per 290 | }; 291 | (kminp, kmaxp) 292 | }; 293 | 294 | let mix = Mix::new(set_perc, get_perc, del_perc, scan_perc); 295 | let kgen = match opt.dist.as_str() { 296 | "increment" => KeyGenerator::new_increment(klen, kmin, kmax), 297 | "incrementp" => { 298 | let (kminp, kmaxp) = split_key_space(); 299 | KeyGenerator::new_increment(klen, kminp, kmaxp) 300 | } 301 | "shuffle" => KeyGenerator::new_shuffle(klen, kmin, kmax), 302 | "shufflep" => { 303 | let (kminp, kmaxp) = split_key_space(); 304 | KeyGenerator::new_shuffle(klen, kminp, kmaxp) 305 | } 306 | "uniform" => KeyGenerator::new_uniform(klen, kmin, kmax), 307 | "zipfian" => { 308 | let theta = opt.zipf_theta.unwrap_or(1.0f64); 309 | let hotspot = opt.zipf_hotspot.unwrap_or(0.0f64); 310 | KeyGenerator::new_zipfian(klen, kmin, kmax, theta, hotspot) 311 | } 312 | "latest" => { 313 | let theta = opt.zipf_theta.unwrap_or(1.0f64); 314 | let hotspot = opt.zipf_hotspot.unwrap_or(0.0f64); 315 | KeyGenerator::new_zipfian_latest(klen, kmin, kmax, theta, hotspot) 316 | } 317 | _ => { 318 | panic!("invalid key distribution: {}", opt.dist); 319 | } 320 | }; 321 | Self { 322 | mix, 323 | kgen, 324 | scan_n, 325 | vlen, 326 | count: 0, 327 | } 328 | } 329 | 330 | pub fn next(&mut self, rng: &mut impl Rng) -> Operation { 331 | self.count += 1; 332 | let key = self.kgen.next(rng); 333 | match self.mix.next(rng) { 334 | OperationType::Set => { 335 | // special case for "latest" distribution: if the generated request is a `SET`, 336 | // update the hotspot to the latest generated key. 337 | if let KeyDistribution::ZipfianLatest(_, ref mut hotspot, latest) = self.kgen.dist { 338 | *hotspot = latest; 339 | } 340 | let value = vec![0u8; self.vlen].into_boxed_slice(); 341 | Operation::Set { key, value } 342 | } 343 | OperationType::Get => Operation::Get { key }, 344 | OperationType::Delete => Operation::Delete { key }, 345 | OperationType::Scan => Operation::Scan { 346 | key, 347 | n: self.scan_n, 348 | }, 349 | } 350 | } 351 | 352 | pub fn reset(&mut self) { 353 | self.kgen.serial = 0; 354 | } 355 | 356 | pub fn is_exhausted(&self) -> bool { 357 | self.kgen.serial == self.kgen.keyspace 358 | } 359 | } 360 | 361 | #[cfg(test)] 362 | mod tests { 363 | use super::*; 364 | use hashbrown::{HashMap, HashSet}; 365 | use quanta::Instant; 366 | 367 | #[test] 368 | fn mix_one_type_only() { 369 | let mut rng = rand::rng(); 370 | let mix = Mix::new(100, 0, 0, 0); 371 | for _ in 0..100 { 372 | assert!(matches!(mix.next(&mut rng), OperationType::Set)); 373 | } 374 | let mix = Mix::new(0, 100, 0, 0); 375 | for _ in 0..100 { 376 | assert!(matches!(mix.next(&mut rng), OperationType::Get)); 377 | } 378 | let mix = Mix::new(0, 0, 100, 0); 379 | for _ in 0..100 { 380 | assert!(matches!(mix.next(&mut rng), OperationType::Delete)); 381 | } 382 | let mix = Mix::new(0, 0, 0, 100); 383 | for _ in 0..100 { 384 | assert!(matches!(mix.next(&mut rng), OperationType::Scan)); 385 | } 386 | } 387 | 388 | #[test] 389 | fn mix_small_write() { 390 | let mut rng = rand::rng(); 391 | let mix = Mix::new(5, 95, 0, 0); 392 | let mut set = 0; 393 | #[allow(unused)] 394 | let mut get = 0; 395 | for _ in 0..1000000 { 396 | match mix.next(&mut rng) { 397 | OperationType::Set => set += 1, 398 | OperationType::Get => get += 1, 399 | OperationType::Delete => unreachable!(), 400 | OperationType::Scan => unreachable!(), 401 | }; 402 | } 403 | assert!(set < 65000 && set > 35000); 404 | } 405 | 406 | #[test] 407 | fn keygen_increment() { 408 | // this also checks unaligned key 409 | let mut rng = rand::rng(); 410 | for len in [3, 8, 16] { 411 | let mut kgen = KeyGenerator::new_increment(len, 0, 3); 412 | let mut k: Box<[u8]> = (0..len).map(|_| 0u8).collect(); 413 | for _ in 0..10 { 414 | k[len.min(8) - 1] = 0u8; 415 | assert_eq!(kgen.next(&mut rng), k); 416 | k[len.min(8) - 1] = 1u8; 417 | assert_eq!(kgen.next(&mut rng), k); 418 | k[len.min(8) - 1] = 2u8; 419 | assert_eq!(kgen.next(&mut rng), k); 420 | } 421 | } 422 | } 423 | 424 | #[test] 425 | fn keygen_shuffle() { 426 | let start = 117; 427 | let end = 135423; 428 | let mut rng = rand::rng(); 429 | let mut kgen = KeyGenerator::new_shuffle(8, start, end); 430 | let mut dist: HashSet> = HashSet::new(); 431 | for _ in start..end { 432 | let key = kgen.next(&mut rng); 433 | // the key is newly added, not repeated 434 | assert!(dist.insert(key.clone())); 435 | } 436 | // each key exists exactly once 437 | assert_eq!(dist.len(), end - start); 438 | let min = dist.iter().min().clone().unwrap(); 439 | let min_bytes = Box::from(start.to_be_bytes()); 440 | assert_eq!(*min, min_bytes); 441 | let max = dist.iter().max().clone().unwrap(); 442 | let max_bytes = Box::from((end - 1).to_be_bytes()); 443 | assert_eq!(*max, max_bytes); 444 | } 445 | 446 | #[test] 447 | fn keygen_uniform() { 448 | let mut rng = rand::rng(); 449 | let mut dist: HashMap, u64> = HashMap::new(); 450 | let mut kgen = KeyGenerator::new_uniform(8, 0, 100); 451 | // 100 keys, 1m gens so ~10k occurance ea. Bound to 9k to 11k 452 | // buy a lottry if this fails 453 | for _ in 0..1000000 { 454 | let k = kgen.next(&mut rng); 455 | dist.entry(k).and_modify(|c| *c += 1).or_insert(0); 456 | } 457 | for c in dist.values() { 458 | assert!(*c < 11000 && *c > 9000); 459 | } 460 | } 461 | 462 | #[test] 463 | fn keygen_zipfian() { 464 | let mut rng = rand::rng(); 465 | let mut dist: HashMap, u64> = HashMap::new(); 466 | let mut kgen = KeyGenerator::new_zipfian(8, 0, 10, 1.0, 0.0); 467 | for _ in 0..1000000 { 468 | let k = kgen.next(&mut rng); 469 | dist.entry(k).and_modify(|c| *c += 1).or_insert(0); 470 | } 471 | let mut freq: Vec = dist.values().map(|c| *c).collect(); 472 | freq.sort_by_key(|c| std::cmp::Reverse(*c)); 473 | // just some really nonsense checks 474 | let p1 = freq[0] as f64 / freq[1] as f64; 475 | assert!(p1 > 1.9 && p1 < 2.1, "zipf p1: {}", p1); 476 | let p2 = freq[1] as f64 / freq[2] as f64; 477 | assert!(p2 > 1.45 && p2 < 1.55, "zipf p2: {}", p2); 478 | } 479 | 480 | #[test] 481 | fn keygen_zipfian_hotspot() { 482 | let mut rng = rand::rng(); 483 | 484 | // hotspot is the middle key 485 | let mut dist: HashMap, u64> = HashMap::new(); 486 | let mut kgen = KeyGenerator::new_zipfian(8, 0, 9, 1.0, 0.5); 487 | for _ in 0..1000000 { 488 | let k = kgen.next(&mut rng); 489 | dist.entry(k).and_modify(|c| *c += 1).or_insert(0); 490 | } 491 | let mut freq: Vec<(Box<[u8]>, u64)> = dist.iter().map(|e| (e.0.clone(), *e.1)).collect(); 492 | freq.sort_by_key(|c| c.0.clone()); 493 | let p1 = freq[4].1 as f64 / freq[5].1 as f64; 494 | assert!(p1 > 1.9 && p1 < 2.1, "zipf p1: {}", p1); 495 | let p2 = freq[5].1 as f64 / freq[6].1 as f64; 496 | assert!(p2 > 1.45 && p2 < 1.55, "zipf p2: {}", p2); 497 | 498 | // hotspot is the last key 499 | let mut dist: HashMap, u64> = HashMap::new(); 500 | let mut kgen = KeyGenerator::new_zipfian(8, 0, 9, 1.0, 1.0); 501 | for _ in 0..1000000 { 502 | let k = kgen.next(&mut rng); 503 | dist.entry(k).and_modify(|c| *c += 1).or_insert(0); 504 | } 505 | let mut freq: Vec<(Box<[u8]>, u64)> = dist.iter().map(|e| (e.0.clone(), *e.1)).collect(); 506 | freq.sort_by_key(|c| c.0.clone()); 507 | let p1 = freq[8].1 as f64 / freq[0].1 as f64; 508 | assert!(p1 > 1.9 && p1 < 2.1, "zipf p1: {}", p1); 509 | let p2 = freq[0].1 as f64 / freq[1].1 as f64; 510 | assert!(p2 > 1.45 && p2 < 1.55, "zipf p2: {}", p2); 511 | } 512 | 513 | #[test] 514 | fn keygen_speed() { 515 | const N: usize = 1_000_0000; 516 | let mut rng = rand::rng(); 517 | let mut kgen = KeyGenerator::new_zipfian(8, 0, 1000, 1.0, 0.0); 518 | let t = Instant::now(); 519 | for _ in 0..N { 520 | let _ = kgen.next(&mut rng); 521 | } 522 | println!("zipfian time: {} ms", t.elapsed().as_millis()); 523 | let mut kgen = KeyGenerator::new_uniform(8, 0, 1000); 524 | let t = Instant::now(); 525 | for _ in 0..N { 526 | let _ = kgen.next(&mut rng); 527 | } 528 | println!("uniform time: {} ms", t.elapsed().as_millis()); 529 | let mut kgen = KeyGenerator::new_increment(8, 0, 1000); 530 | let t = Instant::now(); 531 | for _ in 0..N { 532 | let _ = kgen.next(&mut rng); 533 | } 534 | println!("increment time: {} ms", t.elapsed().as_millis()); 535 | let mut kgen = KeyGenerator::new_shuffle(8, 0, 1000); 536 | let t = Instant::now(); 537 | for _ in 0..N { 538 | let _ = kgen.next(&mut rng); 539 | } 540 | println!("shuffle time: {} ms", t.elapsed().as_millis()); 541 | } 542 | 543 | #[test] 544 | fn workload_toml_correct() { 545 | let s = r#"set_perc = 70 546 | get_perc = 20 547 | del_perc = 5 548 | scan_perc = 5 549 | scan_n = 100 550 | klen = 4 551 | vlen = 6 552 | dist = "uniform" 553 | kmin = 0 554 | kmax = 12345"#; 555 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 556 | let w = Workload::new(&opt, None); 557 | assert_eq!(w.scan_n, 100); 558 | assert_eq!(w.kgen.len, 4); 559 | assert_eq!(w.vlen, 6); 560 | assert_eq!(w.kgen.min, 0); 561 | assert_eq!(w.kgen.max, 12345); 562 | assert!(matches!(w.kgen.dist, KeyDistribution::Uniform(_))); 563 | 564 | let s = r#"set_perc = 70 565 | get_perc = 20 566 | del_perc = 5 567 | scan_perc = 5 568 | scan_n = 20 569 | klen = 40 570 | vlen = 60 571 | dist = "zipfian" 572 | kmin = 0 573 | kmax = 123450 574 | zipf_theta = 1.0 575 | zipf_hotspot = 1.0"#; 576 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 577 | let w = Workload::new(&opt, None); 578 | assert_eq!(w.scan_n, 20); 579 | assert_eq!(w.kgen.len, 40); 580 | assert_eq!(w.vlen, 60); 581 | assert_eq!(w.kgen.min, 0); 582 | assert_eq!(w.kgen.max, 123450); 583 | assert!(matches!(w.kgen.dist, KeyDistribution::Zipfian(_, 123449))); 584 | 585 | let s = r#"set_perc = 60 586 | get_perc = 25 587 | del_perc = 10 588 | scan_perc = 5 589 | scan_n = 30 590 | klen = 14 591 | vlen = 16 592 | dist = "shuffle" 593 | kmin = 10000 594 | kmax = 20000"#; 595 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 596 | let w = Workload::new(&opt, None); 597 | assert_eq!(w.scan_n, 30); 598 | assert_eq!(w.kgen.len, 14); 599 | assert_eq!(w.vlen, 16); 600 | assert_eq!(w.kgen.min, 10000); 601 | assert_eq!(w.kgen.max, 20000); 602 | assert!(matches!(w.kgen.dist, KeyDistribution::Shuffle(_))); 603 | } 604 | 605 | #[test] 606 | #[should_panic(expected = "should be positive")] 607 | fn workload_toml_invalid_wrong_size() { 608 | let s = r#"set_perc = 60 609 | get_perc = 40 610 | del_perc = 0 611 | scan_perc = 0 612 | scan_n = 10 613 | klen = 0 614 | vlen = 6 615 | dist = "uniform" 616 | kmin = 0 617 | kmax = 12345"#; 618 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 619 | let _ = Workload::new(&opt, None); 620 | } 621 | 622 | #[test] 623 | #[should_panic(expected = "should be positive")] 624 | fn workload_toml_invalid_wrong_scan() { 625 | let s = r#"set_perc = 60 626 | get_perc = 40 627 | del_perc = 0 628 | scan_perc = 0 629 | scan_n = 0 630 | klen = 2 631 | vlen = 6 632 | dist = "uniform" 633 | kmin = 0 634 | kmax = 12345"#; 635 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 636 | let _ = Workload::new(&opt, None); 637 | } 638 | 639 | #[test] 640 | #[should_panic(expected = "should be specified")] 641 | fn workload_toml_invalid_missing_fields() { 642 | let s = r#"set_perc = 60 643 | get_perc = 40 644 | del_perc = 0 645 | scan_perc = 0 646 | scan_n = 10 647 | dist = "uniform" 648 | kmin = 0 649 | kmax = 12345"#; 650 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 651 | let _ = Workload::new(&opt, None); 652 | } 653 | 654 | #[test] 655 | #[should_panic(expected = "should be greater")] 656 | fn workload_toml_invalid_wrong_keyspace() { 657 | let s = r#"set_perc = 60 658 | get_perc = 40 659 | del_perc = 0 660 | scan_perc = 0 661 | scan_n = 10 662 | klen = 4 663 | vlen = 6 664 | dist = "uniform" 665 | kmin = 5 666 | kmax = 1"#; 667 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 668 | let _ = Workload::new(&opt, None); 669 | } 670 | 671 | #[test] 672 | #[should_panic(expected = "should be 100")] 673 | fn workload_toml_invalid_wrong_mix() { 674 | let s = r#"set_perc = 70 675 | get_perc = 40 676 | del_perc = 0 677 | scan_perc = 0 678 | scan_n = 10 679 | klen = 4 680 | vlen = 6 681 | dist = "uniform" 682 | kmin = 0 683 | kmax = 12345"#; 684 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 685 | let _ = Workload::new(&opt, None); 686 | } 687 | 688 | #[test] 689 | #[should_panic(expected = "should be 100")] 690 | fn workload_toml_invalid_wrong_mix_missing() { 691 | let s = r#"klen = 4 692 | vlen = 6 693 | dist = "uniform" 694 | kmin = 0 695 | kmax = 12345"#; 696 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 697 | let _ = Workload::new(&opt, None); 698 | } 699 | 700 | #[test] 701 | #[should_panic(expected = "invalid key distribution")] 702 | fn workload_toml_invalid_key_distribution() { 703 | let s = r#"set_perc = 70 704 | get_perc = 30 705 | del_perc = 0 706 | scan_perc = 0 707 | scan_n = 10 708 | klen = 4 709 | vlen = 6 710 | dist = "uniorm" 711 | kmin = 0 712 | kmax = 12345"#; 713 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 714 | let _ = Workload::new(&opt, None); 715 | } 716 | 717 | #[test] 718 | fn workload_toml_defaults_are_applied() { 719 | let s = r#"set_perc = 70 720 | get_perc = 30 721 | klen = 4 722 | vlen = 6 723 | dist = "zipfian" 724 | kmin = 111 725 | kmax = 12345"#; 726 | let opt: WorkloadOpt = toml::from_str(s).unwrap(); 727 | let w = Workload::new(&opt, None); 728 | assert_eq!(w.scan_n, 10); 729 | assert!(matches!(w.kgen.dist, KeyDistribution::Zipfian(_, 0))); 730 | } 731 | 732 | #[test] 733 | fn workload_keygen_parallel() { 734 | let mut opt = WorkloadOpt { 735 | set_perc: Some(100), 736 | get_perc: None, 737 | del_perc: None, 738 | scan_perc: None, 739 | scan_n: Some(10), 740 | klen: Some(16), 741 | vlen: Some(100), 742 | dist: "incrementp".to_string(), 743 | kmin: Some(10000), 744 | kmax: Some(22347), 745 | zipf_theta: None, 746 | zipf_hotspot: None, 747 | }; 748 | let test = |opt: &WorkloadOpt| { 749 | let workload = Workload::new(&opt, Some((0, 3))); 750 | assert_eq!(workload.kgen.min, 10000); 751 | assert_eq!(workload.kgen.max, 14115); 752 | 753 | let workload = Workload::new(&opt, Some((1, 3))); 754 | assert_eq!(workload.kgen.min, 14115); 755 | assert_eq!(workload.kgen.max, 18230); 756 | 757 | let workload = Workload::new(&opt, Some((2, 3))); 758 | assert_eq!(workload.kgen.min, 18230); 759 | assert_eq!(workload.kgen.max, 22347); // 2 more keys 760 | }; 761 | // incrementp 762 | test(&opt); 763 | // shufflep 764 | opt.dist = "shufflep".to_string(); 765 | test(&opt); 766 | } 767 | 768 | #[test] 769 | fn workload_keygen_parallel_fill() { 770 | let mut opt = WorkloadOpt { 771 | set_perc: Some(100), 772 | get_perc: None, 773 | del_perc: None, 774 | scan_perc: None, 775 | scan_n: Some(10), 776 | klen: Some(16), 777 | vlen: Some(100), 778 | dist: "incrementp".to_string(), 779 | kmin: Some(10000), 780 | kmax: Some(22347), 781 | zipf_theta: None, 782 | zipf_hotspot: None, 783 | }; 784 | let test = |opt: &WorkloadOpt| { 785 | let mut rng = rand::rng(); 786 | let mut keys = HashSet::>::new(); 787 | let mut workloads: Vec = 788 | (0..5).map(|t| Workload::new(&opt, Some((t, 5)))).collect(); 789 | for w in workloads.iter_mut() { 790 | while !w.is_exhausted() { 791 | let op = w.next(&mut rng); 792 | let Operation::Set { key, .. } = op else { 793 | panic!() 794 | }; 795 | assert!(keys.insert(key)); 796 | } 797 | } 798 | assert_eq!(keys.len(), 12347); 799 | }; 800 | // incrementp 801 | test(&opt); 802 | // shufflep 803 | opt.dist = "shufflep".to_string(); 804 | test(&opt); 805 | } 806 | 807 | #[test] 808 | fn workload_keygen_zipfian_latest() { 809 | let opt = WorkloadOpt { 810 | set_perc: Some(5), 811 | get_perc: Some(95), 812 | del_perc: None, 813 | scan_perc: None, 814 | scan_n: Some(10), 815 | klen: Some(16), 816 | vlen: Some(100), 817 | dist: "latest".to_string(), 818 | kmin: Some(10000), 819 | kmax: Some(22347), 820 | zipf_theta: None, 821 | zipf_hotspot: None, 822 | }; 823 | 824 | let mut workload = Workload::new(&opt, None); 825 | let mut rng = rand::rng(); 826 | assert!(matches!( 827 | workload.kgen.dist, 828 | KeyDistribution::ZipfianLatest(_, 0, 0) 829 | )); 830 | let mut dist: HashSet = HashSet::new(); 831 | let mut set_count = 0; 832 | for _ in 0..1000000 { 833 | let KeyDistribution::ZipfianLatest(_, this_hotspot, _) = workload.kgen.dist else { 834 | panic!(); 835 | }; 836 | dist.insert(this_hotspot); 837 | let op = workload.next(&mut rng); 838 | if let Operation::Set { key, value } = op { 839 | assert_eq!(key.len(), 16); 840 | assert_eq!(value.len(), 100); 841 | let KeyDistribution::ZipfianLatest(_, hotspot, latest) = workload.kgen.dist else { 842 | panic!(); 843 | }; 844 | assert_eq!(hotspot, latest); 845 | set_count += 1; 846 | } 847 | } 848 | // approx. proportion of set should be ~5%. 849 | assert!(set_count < 60000); 850 | // and all the observed hotspot must be way less than that 851 | assert!(dist.len() < set_count); 852 | } 853 | 854 | #[test] 855 | fn workload_uniform_write_intensive() { 856 | let opt = WorkloadOpt { 857 | set_perc: Some(50), 858 | get_perc: Some(50), 859 | del_perc: None, 860 | scan_perc: None, 861 | scan_n: Some(10), 862 | klen: Some(16), 863 | vlen: Some(100), 864 | dist: "uniform".to_string(), 865 | kmin: Some(1000), 866 | kmax: Some(2000), 867 | zipf_theta: None, 868 | zipf_hotspot: None, 869 | }; 870 | let mut workload = Workload::new(&opt, None); 871 | let mut set = 0; 872 | #[allow(unused)] 873 | let mut get = 0; 874 | let mut dist: HashMap, u64> = HashMap::new(); 875 | let mut rng = rand::rng(); 876 | for _ in 0..10000000 { 877 | let op = workload.next(&mut rng); 878 | match op { 879 | Operation::Set { key, value } => { 880 | assert!(key.len() == 16); 881 | assert!(value.len() == 100); 882 | dist.entry(key).and_modify(|c| *c += 1).or_insert(0); 883 | set += 1; 884 | } 885 | Operation::Get { key } => { 886 | assert!(key.len() == 16); 887 | dist.entry(key).and_modify(|c| *c += 1).or_insert(0); 888 | get += 1; 889 | } 890 | Operation::Delete { .. } | Operation::Scan { .. } => { 891 | unreachable!(); 892 | } 893 | } 894 | } 895 | assert!(dist.keys().len() <= 1000); 896 | for c in dist.values() { 897 | assert!(*c < 12000 && *c > 8000); 898 | } 899 | assert!(set < 5500000 && set > 4500000); 900 | } 901 | 902 | #[test] 903 | fn workload_even_operations() { 904 | let opt = WorkloadOpt { 905 | set_perc: Some(25), 906 | get_perc: Some(25), 907 | del_perc: Some(25), 908 | scan_perc: Some(25), 909 | scan_n: None, 910 | klen: Some(16), 911 | vlen: Some(100), 912 | dist: "uniform".to_string(), 913 | kmin: Some(1000), 914 | kmax: Some(2000), 915 | zipf_theta: None, 916 | zipf_hotspot: None, 917 | }; 918 | let mut workload = Workload::new(&opt, None); 919 | let mut set = 0; 920 | let mut get = 0; 921 | let mut del = 0; 922 | let mut scan = 0; 923 | let mut rng = rand::rng(); 924 | for _ in 0..10000000 { 925 | let op = workload.next(&mut rng); 926 | match op { 927 | Operation::Set { .. } => { 928 | set += 1; 929 | } 930 | Operation::Get { .. } => { 931 | get += 1; 932 | } 933 | Operation::Delete { .. } => { 934 | del += 1; 935 | } 936 | Operation::Scan { .. } => { 937 | scan += 1; 938 | } 939 | } 940 | } 941 | assert!(set > 2400000 && set < 2600000); 942 | assert!(get > 2400000 && get < 2600000); 943 | assert!(del > 2400000 && del < 2600000); 944 | assert!(scan > 2400000 && scan < 2600000); 945 | } 946 | } 947 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | //! A key-value server/client implementation. 2 | 3 | use crate::stores::{BenchKVMap, BenchKVMapOpt}; 4 | use crate::thread::{JoinHandle, Thread}; 5 | use crate::*; 6 | use figment::providers::{Env, Format, Toml}; 7 | use figment::Figment; 8 | use hashbrown::HashMap; 9 | use log::debug; 10 | use mio::net::TcpStream; 11 | use mio::{Events, Interest, Poll, Token}; 12 | use std::cell::RefCell; 13 | use std::io::{BufRead, BufReader, BufWriter, Read, Write}; 14 | use std::net::{SocketAddr, TcpListener, TcpStream as StdTcpStream}; 15 | use std::rc::Rc; 16 | use std::sync::atomic::{AtomicUsize, Ordering}; 17 | use std::sync::mpsc::{channel, Receiver, Sender}; 18 | use std::sync::Arc; 19 | use std::time::Duration; 20 | 21 | /// Requests are sent in a batch. Here we do not send the requests in the batch one by one, but 22 | /// instead in a vector, because the server implementation uses event-based notification. It does 23 | /// not know how many requests are there during an event, so it may not read all requests, 24 | /// especially when the batch is large. 25 | fn write_requests(writer: &mut impl Write, requests: &Vec) -> Result<(), bincode::Error> { 26 | bincode::serialize_into(writer, requests) 27 | } 28 | 29 | fn read_requests(reader: &mut impl Read) -> Result, bincode::Error> { 30 | bincode::deserialize_from::<_, Vec>(reader) 31 | } 32 | 33 | /// Responses have a customized header and the (de)serialization process is manual because the 34 | /// payload (data) in a response may be from a reference. It is preferable to directly write the 35 | /// bytes from the reference to the writer instead of creating a new [`Response`] and perform a 36 | /// copy of the payload data. 37 | #[derive(Serialize, Deserialize)] 38 | struct ResponseHeader { 39 | id: usize, 40 | has_data: bool, 41 | } 42 | 43 | fn write_response( 44 | writer: &mut impl Write, 45 | id: usize, 46 | data: Option<&[&[u8]]>, 47 | ) -> Result<(), bincode::Error> { 48 | let has_data = match data { 49 | Some(_) => true, 50 | None => false, 51 | }; 52 | let header = ResponseHeader { id, has_data }; 53 | if let Err(e) = bincode::serialize_into(&mut *writer, &header) { 54 | return Err(e); 55 | } 56 | // has payload 57 | if has_data { 58 | if let Err(e) = bincode::serialize_into(&mut *writer, data.unwrap()) { 59 | return Err(e); 60 | } 61 | } 62 | Ok(()) 63 | } 64 | 65 | fn read_response(reader: &mut impl Read) -> Result { 66 | let ResponseHeader { id, has_data } = 67 | bincode::deserialize_from::<_, ResponseHeader>(&mut *reader)?; 68 | if has_data { 69 | let data = bincode::deserialize_from::<_, Vec>>(&mut *reader)?; 70 | Ok(Response { 71 | id, 72 | data: Some(data), 73 | }) 74 | } else { 75 | Ok(Response { id, data: None }) 76 | } 77 | } 78 | 79 | const POLLING_TIMEOUT: Option = Some(Duration::new(0, 0)); 80 | 81 | enum WorkerMsg { 82 | NewConnection(StdTcpStream, SocketAddr), 83 | Terminate, 84 | } 85 | 86 | fn serve_requests_regular( 87 | handle: &mut Box, 88 | requests: &Vec, 89 | writer: &mut ResponseWriter, 90 | ) { 91 | for Request { id, op: body } in requests.iter() { 92 | let id = id.clone(); 93 | match body { 94 | Operation::Set { ref key, ref value } => { 95 | handle.set(key, value); 96 | assert!(write_response(&mut *writer, id, None).is_ok()); 97 | } 98 | Operation::Get { ref key } => match handle.get(key) { 99 | Some(v) => { 100 | let data = &vec![&v[..]]; 101 | assert!(write_response(&mut *writer, id, Some(data)).is_ok()); 102 | } 103 | None => { 104 | assert!(write_response(&mut *writer, id, None).is_ok()); 105 | } 106 | }, 107 | Operation::Delete { ref key } => { 108 | handle.delete(key); 109 | assert!(write_response(&mut *writer, id, None).is_ok()); 110 | } 111 | Operation::Scan { ref key, n } => { 112 | let kv = handle.scan(key, *n); 113 | let data = kv 114 | .iter() 115 | .fold(Vec::with_capacity(kv.len() * 2), |mut vec, kv| { 116 | vec.push(&kv.0[..]); 117 | vec.push(&kv.1[..]); 118 | vec 119 | }); 120 | assert!(write_response(&mut *writer, id, Some(&data[..])).is_ok()); 121 | } 122 | } 123 | } 124 | } 125 | 126 | pub(crate) fn serve_requests_async( 127 | handle: &mut Box, 128 | requests: &Vec, 129 | ) { 130 | handle.submit(requests); 131 | } 132 | 133 | /// Wrapper around [`TcpStream`] to enable multi-ownership in reader/writer for the same connection 134 | #[derive(Clone)] 135 | struct RcTcpStream(Rc); 136 | 137 | impl RcTcpStream { 138 | fn new(stream: TcpStream) -> RcTcpStream { 139 | RcTcpStream(Rc::new(stream)) 140 | } 141 | } 142 | 143 | impl Read for RcTcpStream { 144 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 145 | (&*self.0).read(buf) 146 | } 147 | } 148 | 149 | impl Write for RcTcpStream { 150 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 151 | (&*self.0).write(buf) 152 | } 153 | 154 | fn flush(&mut self) -> std::io::Result<()> { 155 | (&*self.0).flush() 156 | } 157 | } 158 | 159 | /// Wrapper around buffered request reader. It is always read only by the corresponding worker 160 | /// thread, so there is no sharing. 161 | struct RequestReader(BufReader); 162 | 163 | impl RequestReader { 164 | fn new(stream: RcTcpStream) -> Self { 165 | Self(BufReader::new(stream)) 166 | } 167 | } 168 | 169 | impl Read for RequestReader { 170 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 171 | self.0.read(buf) 172 | } 173 | } 174 | 175 | impl BufRead for RequestReader { 176 | fn fill_buf(&mut self) -> std::io::Result<&[u8]> { 177 | self.0.fill_buf() 178 | } 179 | 180 | fn consume(&mut self, amt: usize) { 181 | self.0.consume(amt); 182 | } 183 | } 184 | 185 | /// Unlike reader, writer is registered to one or more handles. Therefore, it needs a cell to work, 186 | /// and potentially a wrapper [`Rc`]. 187 | struct ResponseWriter(RefCell>); 188 | 189 | impl ResponseWriter { 190 | fn new(stream: RcTcpStream) -> Self { 191 | Self(RefCell::new(BufWriter::new(stream))) 192 | } 193 | } 194 | 195 | impl Write for ResponseWriter { 196 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 197 | self.0.borrow_mut().write(buf) 198 | } 199 | 200 | fn flush(&mut self) -> std::io::Result<()> { 201 | self.0.borrow_mut().flush() 202 | } 203 | } 204 | 205 | enum WriterOrHandle { 206 | Writer(ResponseWriter), // for regular 207 | Handle(Rc, Box), // for async 208 | } 209 | 210 | struct Connection { 211 | reader: RequestReader, 212 | writer_or_handle: WriterOrHandle, 213 | } 214 | 215 | impl Connection { 216 | fn writer(&mut self) -> &mut ResponseWriter { 217 | let WriterOrHandle::Writer(writer) = &mut self.writer_or_handle else { 218 | unreachable!(); 219 | }; 220 | writer 221 | } 222 | 223 | fn handle(&mut self) -> (&Rc, &mut Box) { 224 | let WriterOrHandle::Handle(writer, handle) = &mut self.writer_or_handle else { 225 | unreachable!(); 226 | }; 227 | (writer, handle) 228 | } 229 | } 230 | 231 | type StreamMap = HashMap; 232 | 233 | impl AsyncResponder for ResponseWriter { 234 | fn callback(&self, response: Response) { 235 | let Response { id, data } = response; 236 | match data { 237 | Some(data) => { 238 | assert!(write_response( 239 | &mut *self.0.borrow_mut(), 240 | id, 241 | Some(&data.iter().map(|d| &d[..]).collect::>()[..]) 242 | ) 243 | .is_ok()); 244 | } 245 | None => { 246 | assert!(write_response(&mut *self.0.borrow_mut(), id, None).is_ok()); 247 | } 248 | } 249 | } 250 | } 251 | 252 | fn new_listener(host: &str, port: &str, nonblocking: bool) -> TcpListener { 253 | let addr: String = "".to_string() + host + ":" + port; 254 | let listener = TcpListener::bind(&addr).unwrap_or_else(|e| { 255 | panic!("Server fails to bind address {}: {}", &addr, e); 256 | }); 257 | assert!(listener.set_nonblocking(nonblocking).is_ok()); 258 | listener 259 | } 260 | 261 | fn recv_requests(reader: &mut RequestReader) -> Vec { 262 | assert!(reader.fill_buf().is_ok()); 263 | let mut requests = Vec::new(); 264 | 265 | // here we must drain the buffer until it is empty, because one event may consist of multiple 266 | // batches, usually when the client keeps sending. 267 | while !reader.0.buffer().is_empty() { 268 | requests.append(&mut read_requests(reader).unwrap()); 269 | } 270 | 271 | requests 272 | } 273 | 274 | fn server_worker_regular_main( 275 | worker_id: usize, 276 | poll: &mut Poll, 277 | events: &mut Events, 278 | smap: &mut StreamMap, 279 | handle: &mut Box, 280 | thread: &Box, 281 | ) { 282 | for (_, connection) in smap.iter_mut() { 283 | assert!(connection.writer().flush().is_ok()); 284 | } 285 | assert!(poll.poll(events, POLLING_TIMEOUT).is_ok()); 286 | for event in events.iter() { 287 | let token = event.token(); 288 | assert_ne!(token, Token(0)); 289 | if event.is_read_closed() || event.is_write_closed() { 290 | assert!(smap.remove(&token).is_some()); 291 | } else if event.is_error() { 292 | panic!("Server worker {} receives error event", worker_id); 293 | } else { 294 | if let Some(connection) = smap.get_mut(&token) { 295 | let requests = recv_requests(&mut connection.reader); 296 | serve_requests_regular(handle, &requests, connection.writer()); 297 | } else { 298 | panic!("Server worker {} receives non-exist event", worker_id); 299 | } 300 | } 301 | thread.yield_now(); 302 | } 303 | } 304 | 305 | fn server_worker_async_main( 306 | worker_id: usize, 307 | poll: &mut Poll, 308 | events: &mut Events, 309 | smap: &mut StreamMap, 310 | thread: &Box, 311 | ) { 312 | for (_, connection) in smap.iter_mut() { 313 | let (writer, handle) = connection.handle(); 314 | handle.drain(); 315 | assert!(writer.0.borrow_mut().flush().is_ok()); 316 | } 317 | assert!(poll.poll(events, POLLING_TIMEOUT).is_ok()); 318 | for event in events as &Events { 319 | let token = event.token(); 320 | assert_ne!(token, Token(0)); 321 | if event.is_read_closed() || event.is_write_closed() { 322 | assert!(smap.remove(&token).is_some()); 323 | } else if event.is_error() { 324 | panic!("Server worker {} receives error event", worker_id); 325 | } else { 326 | if let Some(connection) = smap.get_mut(&token) { 327 | let requests = recv_requests(&mut connection.reader); 328 | let handle = connection.handle().1; 329 | serve_requests_async(handle, &requests); 330 | } else { 331 | panic!("Server worker {} receives non-exist event", worker_id); 332 | } 333 | } 334 | thread.yield_now(); 335 | } 336 | } 337 | 338 | fn server_worker_check_msg( 339 | listener: &Arc, 340 | rx: &Receiver, 341 | txs: &Vec>, 342 | counter: &Arc, 343 | nr_workers: usize, 344 | ) -> Option { 345 | if let Ok((s, addr)) = listener.accept() { 346 | let w = counter.fetch_add(1, Ordering::AcqRel) % nr_workers; 347 | debug!("New connection dispatched to worker {}", w); 348 | assert!(txs[w].send(WorkerMsg::NewConnection(s, addr)).is_ok()); 349 | } 350 | if let Ok(msg) = rx.try_recv() { 351 | return Some(msg); 352 | } 353 | None 354 | } 355 | 356 | fn server_worker_new_connection( 357 | stream: StdTcpStream, 358 | addr: SocketAddr, 359 | poll: &Poll, 360 | ) -> (Token, RequestReader, ResponseWriter) { 361 | stream.set_nodelay(true).expect("server set_nodelay failed"); 362 | let mut stream = TcpStream::from_std(stream); 363 | let token = Token(addr.port().into()); 364 | assert!(poll 365 | .registry() 366 | .register(&mut stream, token, Interest::READABLE) 367 | .is_ok()); 368 | let stream = RcTcpStream::new(stream); 369 | let reader = RequestReader::new(stream.clone()); 370 | let writer = ResponseWriter::new(stream.clone()); 371 | (token, reader, writer) 372 | } 373 | 374 | fn server_worker_common() -> (Events, StreamMap, Poll) { 375 | let events = Events::with_capacity(1024); 376 | let smap = StreamMap::new(); 377 | let poll = Poll::new().unwrap(); 378 | (events, smap, poll) 379 | } 380 | 381 | fn server_worker_regular( 382 | map: Arc>, 383 | worker_id: usize, 384 | listener: Arc, 385 | rx: Receiver, 386 | txs: Vec>, 387 | nr_workers: usize, 388 | counter: Arc, 389 | ) { 390 | let thread = map.thread(); 391 | thread.pin(worker_id); 392 | 393 | let (mut events, mut smap, mut poll) = server_worker_common(); 394 | debug!("Server worker {} is ready", worker_id); 395 | 396 | let mut handle = map.handle(); 397 | 398 | loop { 399 | if let Some(msg) = server_worker_check_msg(&listener, &rx, &txs, &counter, nr_workers) { 400 | match msg { 401 | WorkerMsg::Terminate => { 402 | debug!("Server worker {} terminates", worker_id); 403 | break; 404 | } 405 | WorkerMsg::NewConnection(s, addr) => { 406 | let (token, reader, writer) = server_worker_new_connection(s, addr, &poll); 407 | smap.insert( 408 | token, 409 | Connection { 410 | reader, 411 | writer_or_handle: WriterOrHandle::Writer(writer), 412 | }, 413 | ); 414 | } 415 | } 416 | } 417 | server_worker_regular_main( 418 | worker_id, 419 | &mut poll, 420 | &mut events, 421 | &mut smap, 422 | &mut handle, 423 | &thread, 424 | ); 425 | thread.yield_now(); 426 | } 427 | } 428 | 429 | fn server_worker_async( 430 | map: Arc>, 431 | worker_id: usize, 432 | listener: Arc, 433 | rx: Receiver, 434 | txs: Vec>, 435 | nr_workers: usize, 436 | counter: Arc, 437 | ) { 438 | let thread = map.thread(); 439 | thread.pin(worker_id); 440 | 441 | let (mut events, mut smap, mut poll) = server_worker_common(); 442 | debug!("Server worker {} is ready", worker_id); 443 | 444 | loop { 445 | if let Some(msg) = server_worker_check_msg(&listener, &rx, &txs, &counter, nr_workers) { 446 | match msg { 447 | WorkerMsg::Terminate => { 448 | debug!("Server worker {} terminates", worker_id); 449 | break; 450 | } 451 | WorkerMsg::NewConnection(s, addr) => { 452 | let (token, reader, writer) = server_worker_new_connection(s, addr, &poll); 453 | let writer = Rc::new(writer); 454 | let handle = map.handle(writer.clone()); 455 | smap.insert( 456 | token, 457 | Connection { 458 | reader, 459 | writer_or_handle: WriterOrHandle::Handle(writer, handle), 460 | }, 461 | ); 462 | } 463 | } 464 | } 465 | server_worker_async_main(worker_id, &mut poll, &mut events, &mut smap, &thread); 466 | thread.yield_now(); 467 | } 468 | } 469 | 470 | fn server_common( 471 | host: &str, 472 | port: &str, 473 | nr_workers: usize, 474 | ) -> ( 475 | Arc, 476 | Vec>, 477 | Vec>, 478 | Arc, 479 | ) { 480 | let listener = Arc::new(new_listener(host, port, true)); 481 | 482 | let mut senders = Vec::>::with_capacity(nr_workers); 483 | let mut receivers = Vec::>::with_capacity(nr_workers); 484 | 485 | // create channels 486 | for _ in 0..nr_workers { 487 | let (tx, rx) = channel(); 488 | senders.push(tx); 489 | receivers.push(rx); 490 | } 491 | let counter = Arc::new(AtomicUsize::new(0)); 492 | 493 | return (listener, senders, receivers, counter); 494 | } 495 | 496 | fn server_mainloop( 497 | stop_rx: Receiver<()>, 498 | grace_tx: Sender<()>, 499 | senders: Vec>, 500 | mut handles: Vec>, 501 | thread: Box, 502 | ) { 503 | loop { 504 | if let Ok(_) = stop_rx.try_recv() { 505 | break; 506 | } 507 | thread.yield_now(); 508 | } 509 | let nr_workers = handles.len(); 510 | for i in 0..nr_workers { 511 | assert!(senders[i].send(WorkerMsg::Terminate).is_ok()); 512 | } 513 | while let Some(handle) = handles.pop() { 514 | handle.join(); 515 | } 516 | assert!(grace_tx.send(()).is_ok()); 517 | } 518 | 519 | pub(crate) fn server_regular( 520 | map: Arc>, 521 | host: &str, 522 | port: &str, 523 | nr_workers: usize, 524 | stop_rx: Receiver<()>, 525 | grace_tx: Sender<()>, 526 | ) { 527 | let thread = map.thread(); 528 | 529 | let (listener, senders, mut receivers, counter) = server_common(host, port, nr_workers); 530 | 531 | let mut handles = Vec::new(); 532 | for i in 0..nr_workers { 533 | let map = map.clone(); 534 | let listener = listener.clone(); 535 | let txs: Vec> = (0..nr_workers).map(|w| senders[w].clone()).collect(); 536 | let rx = receivers.pop().unwrap(); // guaranteed to succeed 537 | let nr_workers = nr_workers.clone(); 538 | let counter = counter.clone(); 539 | let handle = thread.spawn(Box::new(move || { 540 | server_worker_regular(map, i, listener, rx, txs, nr_workers, counter); 541 | })); 542 | handles.push(handle); 543 | } 544 | 545 | server_mainloop(stop_rx, grace_tx, senders, handles, thread); 546 | } 547 | 548 | pub(crate) fn server_async( 549 | map: Arc>, 550 | host: &str, 551 | port: &str, 552 | nr_workers: usize, 553 | stop_rx: Receiver<()>, 554 | grace_tx: Sender<()>, 555 | ) { 556 | let thread = map.thread(); 557 | 558 | let (listener, senders, mut receivers, counter) = server_common(host, port, nr_workers); 559 | 560 | let mut handles = Vec::new(); 561 | for i in 0..nr_workers { 562 | let map = map.clone(); 563 | let listener = listener.clone(); 564 | let txs: Vec> = (0..nr_workers).map(|w| senders[w].clone()).collect(); 565 | let rx = receivers.pop().unwrap(); // guaranteed to succeed 566 | let nr_workers = nr_workers.clone(); 567 | let counter = counter.clone(); 568 | let handle = thread.spawn(Box::new(move || { 569 | server_worker_async(map, i, listener, rx, txs, nr_workers, counter); 570 | })); 571 | handles.push(handle); 572 | } 573 | 574 | server_mainloop(stop_rx, grace_tx, senders, handles, thread); 575 | } 576 | 577 | pub(crate) struct KVClient { 578 | request_writer: BufWriter, 579 | response_reader: BufReader, 580 | } 581 | 582 | impl KVClient { 583 | pub(crate) fn new(host: &str, port: &str) -> Result { 584 | let addr: String = "".to_string() + host + ":" + port; 585 | match StdTcpStream::connect(&addr) { 586 | Ok(s) => { 587 | s.set_nodelay(true).expect("client set_nodelay failed"); 588 | let s2 = s.try_clone().unwrap_or_else(|e| { 589 | panic!("KVClient fails to clone a tcp stream: {}", e); 590 | }); 591 | Ok(KVClient { 592 | request_writer: BufWriter::new(TcpStream::from_std(s)), 593 | response_reader: BufReader::new(TcpStream::from_std(s2)), 594 | }) 595 | } 596 | Err(e) => Err(e), 597 | } 598 | } 599 | 600 | pub(crate) fn send_requests(&mut self, requests: &Vec) { 601 | assert!(write_requests(&mut self.request_writer, requests).is_ok()); 602 | assert!(self.request_writer.flush().is_ok()); 603 | } 604 | 605 | // recv all (drain the buffer) 606 | pub(crate) fn recv_responses(&mut self) -> Vec { 607 | assert!(self.response_reader.fill_buf().is_ok()); 608 | let mut responses = Vec::new(); 609 | 610 | while !self.response_reader.buffer().is_empty() { 611 | match read_response(&mut self.response_reader) { 612 | Ok(r) => { 613 | responses.push(r); 614 | } 615 | Err(e) => { 616 | panic!("KVClient failed to read response: {}", e); 617 | } 618 | } 619 | } 620 | 621 | responses 622 | } 623 | 624 | #[cfg(test)] 625 | fn recv_responses_n(&mut self, nr: usize) -> Vec { 626 | let mut responses = Vec::::with_capacity(nr); 627 | 628 | for _ in 0..nr { 629 | match read_response(&mut self.response_reader) { 630 | Ok(r) => { 631 | responses.push(r); 632 | } 633 | Err(e) => { 634 | panic!("KVClient failed to read response: {}", e); 635 | } 636 | } 637 | } 638 | 639 | responses 640 | } 641 | 642 | #[cfg(test)] 643 | fn set(&mut self, key: &[u8], value: &[u8]) { 644 | let mut requests = Vec::::with_capacity(1); 645 | let op = Operation::Set { 646 | key: key.into(), 647 | value: value.into(), 648 | }; 649 | requests.push(Request { id: 0, op }); 650 | self.send_requests(&requests); 651 | 652 | let mut responses = self.recv_responses_n(1); 653 | assert_eq!(responses.len(), 1); 654 | let response = responses.pop().unwrap(); 655 | assert_eq!(response.id, 0); 656 | assert!(response.data.is_none()); 657 | } 658 | 659 | #[cfg(test)] 660 | fn get(&mut self, key: &[u8]) -> Option> { 661 | let mut requests = Vec::::with_capacity(1); 662 | let op = Operation::Get { key: key.into() }; 663 | requests.push(Request { id: 0, op }); 664 | self.send_requests(&requests); 665 | 666 | let mut responses = self.recv_responses_n(1); 667 | assert_eq!(responses.len(), 1); 668 | let response = responses.pop().unwrap(); 669 | assert_eq!(response.id, 0); 670 | match response.data { 671 | Some(mut v) => { 672 | assert_eq!(v.len(), 1); 673 | v.pop() 674 | } 675 | None => None, 676 | } 677 | } 678 | 679 | #[cfg(test)] 680 | fn delete(&mut self, key: &[u8]) { 681 | let mut requests = Vec::::with_capacity(1); 682 | let op = Operation::Delete { key: key.into() }; 683 | requests.push(Request { id: 0, op }); 684 | self.send_requests(&requests); 685 | 686 | let mut responses = self.recv_responses_n(1); 687 | assert_eq!(responses.len(), 1); 688 | let response = responses.pop().unwrap(); 689 | assert_eq!(response.id, 0); 690 | assert!(response.data.is_none()); 691 | } 692 | } 693 | 694 | #[derive(Deserialize, Debug)] 695 | struct ServerMapOpt { 696 | map: BenchKVMapOpt, 697 | } 698 | 699 | pub(crate) fn init(text: &str) -> BenchKVMap { 700 | let opt: ServerMapOpt = Figment::new() 701 | .merge(Toml::string(&text)) 702 | .merge(Env::raw()) 703 | .extract() 704 | .unwrap(); 705 | BenchKVMap::new(&opt.map) 706 | } 707 | 708 | #[cfg(test)] 709 | mod tests { 710 | use super::*; 711 | use crate::stores::*; 712 | use std::sync::atomic::{AtomicU32, Ordering}; 713 | use std::sync::mpsc::{channel, Receiver, Sender}; 714 | use std::time::Duration; 715 | 716 | static PORT: AtomicU32 = AtomicU32::new(9000); 717 | 718 | fn addr() -> (String, String, String) { 719 | let host = "127.0.0.1".to_string(); 720 | let port = PORT.fetch_add(1, Ordering::AcqRel).to_string(); 721 | let addr = "".to_owned() + &host + ":" + &port; 722 | (host, port, addr) 723 | } 724 | 725 | fn server_run( 726 | map: BenchKVMap, 727 | host: &str, 728 | port: &str, 729 | nr_workers: usize, 730 | ) -> (Sender<()>, Receiver<()>) { 731 | let _ = env_logger::try_init(); 732 | let (host, port) = (host.to_string(), port.to_string()); 733 | let (stop_tx, stop_rx) = channel(); 734 | let (grace_tx, grace_rx) = channel(); 735 | let _ = std::thread::spawn(Box::new(move || { 736 | map.server(&host, &port, nr_workers, stop_rx, grace_tx); 737 | })); 738 | std::thread::sleep(Duration::from_millis(1000)); 739 | (stop_tx, grace_rx) 740 | } 741 | 742 | fn simple(map: BenchKVMap) { 743 | let (host, port, _) = addr(); 744 | let (stop_tx, grace_rx) = server_run(map, &host, &port, 4); 745 | let mut client = KVClient::new(&host, &port) 746 | .unwrap_or_else(|e| panic!("Failed to unwrap client instance: {}", e)); 747 | 748 | client.set(b"foo", b"bar"); 749 | assert_eq!(client.get(b"foo").unwrap(), (*b"bar").into()); 750 | assert!(client.get(b"f00").is_none()); 751 | 752 | client.set(b"foo", b"car"); 753 | assert_eq!(client.get(b"foo").unwrap(), (*b"car").into()); 754 | 755 | client.delete(b"foo"); 756 | assert!(client.get(b"foo").is_none()); 757 | 758 | assert!(stop_tx.send(()).is_ok()); 759 | assert!(grace_rx.recv().is_ok()); 760 | } 761 | 762 | #[test] 763 | fn simple_mutex_hashmap() { 764 | let opt = hashmap::MutexHashMapOpt { shards: 512 }; 765 | let map = BenchKVMap::Regular(Arc::new(Box::new(hashmap::MutexHashMap::new(&opt)))); 766 | simple(map); 767 | } 768 | 769 | #[test] 770 | fn simple_rwlock_hashmap() { 771 | let opt = hashmap::RwLockHashMapOpt { shards: 512 }; 772 | let map = BenchKVMap::Regular(Arc::new(Box::new(hashmap::RwLockHashMap::new(&opt)))); 773 | simple(map); 774 | } 775 | 776 | #[test] 777 | #[cfg(feature = "dashmap")] 778 | fn simple_dashmap() { 779 | let map = BenchKVMap::Regular(Arc::new(Box::new(dashmap::DashMap::new()))); 780 | simple(map); 781 | } 782 | 783 | #[test] 784 | #[cfg(feature = "contrie")] 785 | fn simple_contrie() { 786 | let map = BenchKVMap::Regular(Arc::new(Box::new(contrie::Contrie::new()))); 787 | simple(map); 788 | } 789 | 790 | #[test] 791 | #[cfg(feature = "chashmap")] 792 | fn simple_chashmap() { 793 | let map = BenchKVMap::Regular(Arc::new(Box::new(chashmap::CHashMap::new()))); 794 | simple(map); 795 | } 796 | 797 | #[test] 798 | #[cfg(feature = "scc")] 799 | fn simple_scchashmap() { 800 | let map = BenchKVMap::Regular(Arc::new(Box::new(scc::SccHashMap::new()))); 801 | simple(map); 802 | } 803 | 804 | #[test] 805 | #[cfg(feature = "flurry")] 806 | fn simple_flurry() { 807 | let map = BenchKVMap::Regular(Arc::new(Box::new(flurry::Flurry::new()))); 808 | simple(map); 809 | } 810 | 811 | #[test] 812 | #[cfg(feature = "papaya")] 813 | fn simple_papaya() { 814 | let map = BenchKVMap::Regular(Arc::new(Box::new(papaya::Papaya::new()))); 815 | simple(map); 816 | } 817 | 818 | #[test] 819 | fn simple_mutex_btreemap() { 820 | let map = BenchKVMap::Regular(Arc::new(Box::new(btreemap::MutexBTreeMap::new()))); 821 | simple(map); 822 | } 823 | 824 | #[test] 825 | fn simple_rwlock_btreemap() { 826 | let map = BenchKVMap::Regular(Arc::new(Box::new(btreemap::RwLockBTreeMap::new()))); 827 | simple(map); 828 | } 829 | 830 | #[test] 831 | #[cfg(feature = "rocksdb")] 832 | fn simple_rocksdb() { 833 | let tmp_dir = tempfile::tempdir().unwrap(); 834 | let opt = rocksdb::RocksDBOpt { 835 | path: tmp_dir.path().to_str().unwrap().to_string(), 836 | }; 837 | let map = BenchKVMap::Regular(Arc::new(Box::new(rocksdb::RocksDB::new(&opt)))); 838 | simple(map); 839 | } 840 | 841 | fn batch(map: BenchKVMap) { 842 | const NR_CLIENTS: usize = 8; 843 | const NR_BATCHES: usize = 1000; 844 | const BATCH_SIZE: usize = 100; 845 | 846 | assert_eq!(BATCH_SIZE % 2, 0); 847 | 848 | let mut requests = Vec::>>::with_capacity(NR_CLIENTS); 849 | for i in 0..NR_CLIENTS { 850 | let mut seq = 0; 851 | requests.push(Vec::>::with_capacity(NR_BATCHES)); 852 | for j in 0..NR_BATCHES { 853 | requests[i].push(Vec::::with_capacity(BATCH_SIZE)); 854 | for k in 0..BATCH_SIZE / 2 { 855 | let op1 = Operation::Set { 856 | key: format!("{}", k + j * BATCH_SIZE + i * NR_BATCHES * BATCH_SIZE) 857 | .as_bytes() 858 | .into(), 859 | value: [170u8; 16].into(), 860 | }; 861 | let op2 = Operation::Get { 862 | key: format!("{}", k + j * BATCH_SIZE + i * NR_BATCHES * BATCH_SIZE) 863 | .as_bytes() 864 | .into(), 865 | }; 866 | requests[i][j].push(Request { id: seq, op: op1 }); 867 | requests[i][j].push(Request { 868 | id: seq + 1, 869 | op: op2, 870 | }); 871 | seq += 2; 872 | } 873 | } 874 | } 875 | 876 | let (host, port, _) = addr(); 877 | let (stop_tx, grace_rx) = server_run(map, &host, &port, 8); 878 | 879 | for i in 0..NR_CLIENTS { 880 | let mut client = KVClient::new(&host, &port) 881 | .unwrap_or_else(|e| panic!("Failed to create client instance: {}", e)); 882 | let batch = requests[i].clone(); 883 | let mut pending: usize = 0; 884 | for j in 0..NR_BATCHES { 885 | client.send_requests(&batch[j]); 886 | pending += BATCH_SIZE; 887 | loop { 888 | let response = client.recv_responses(); 889 | for r in response { 890 | let id = r.id; 891 | if id % 2 == 0 { 892 | // set 893 | assert_eq!(r.data, None); 894 | } else { 895 | assert_eq!(r.data, Some(vec![[170u8; 16].into()])); 896 | } 897 | pending -= 1; 898 | } 899 | if pending < BATCH_SIZE * 10 { 900 | break; 901 | } 902 | } 903 | } 904 | // finish remaining 905 | loop { 906 | if pending == 0 { 907 | break; 908 | } 909 | let response = client.recv_responses_n(pending); 910 | for r in response { 911 | let id = r.id; 912 | if id % 2 == 0 { 913 | // set 914 | assert_eq!(r.data, None); 915 | } else { 916 | assert_eq!(r.data, Some(vec![[170u8; 16].into()])); 917 | } 918 | pending -= 1; 919 | } 920 | } 921 | } 922 | 923 | assert!(stop_tx.send(()).is_ok()); 924 | assert!(grace_rx.recv().is_ok()); 925 | } 926 | 927 | #[test] 928 | fn batch_mutex_hashmap() { 929 | let opt = hashmap::MutexHashMapOpt { shards: 512 }; 930 | let map = BenchKVMap::Regular(Arc::new(Box::new(hashmap::MutexHashMap::new(&opt)))); 931 | batch(map); 932 | } 933 | 934 | #[test] 935 | fn batch_rwlock_hashmap() { 936 | let opt = hashmap::RwLockHashMapOpt { shards: 512 }; 937 | let map = BenchKVMap::Regular(Arc::new(Box::new(hashmap::RwLockHashMap::new(&opt)))); 938 | batch(map); 939 | } 940 | 941 | #[test] 942 | #[cfg(feature = "dashmap")] 943 | fn batch_dashmap() { 944 | let map = BenchKVMap::Regular(Arc::new(Box::new(dashmap::DashMap::new()))); 945 | batch(map); 946 | } 947 | 948 | #[test] 949 | #[cfg(feature = "contrie")] 950 | fn batch_contrie() { 951 | let map = BenchKVMap::Regular(Arc::new(Box::new(contrie::Contrie::new()))); 952 | batch(map); 953 | } 954 | 955 | #[test] 956 | #[cfg(feature = "chashmap")] 957 | fn batch_chashmap() { 958 | let map = BenchKVMap::Regular(Arc::new(Box::new(chashmap::CHashMap::new()))); 959 | batch(map); 960 | } 961 | 962 | #[test] 963 | #[cfg(feature = "scc")] 964 | fn batch_scchashmap() { 965 | let map = BenchKVMap::Regular(Arc::new(Box::new(scc::SccHashMap::new()))); 966 | batch(map); 967 | } 968 | 969 | #[test] 970 | #[cfg(feature = "flurry")] 971 | fn batch_flurry() { 972 | let map = BenchKVMap::Regular(Arc::new(Box::new(flurry::Flurry::new()))); 973 | batch(map); 974 | } 975 | 976 | #[test] 977 | #[cfg(feature = "papaya")] 978 | fn batch_papaya() { 979 | let map = BenchKVMap::Regular(Arc::new(Box::new(papaya::Papaya::new()))); 980 | batch(map); 981 | } 982 | 983 | #[test] 984 | fn batch_mutex_btreemap() { 985 | let map = BenchKVMap::Regular(Arc::new(Box::new(btreemap::MutexBTreeMap::new()))); 986 | batch(map); 987 | } 988 | 989 | #[test] 990 | fn batch_rwlock_btreemap() { 991 | let map = BenchKVMap::Regular(Arc::new(Box::new(btreemap::RwLockBTreeMap::new()))); 992 | batch(map); 993 | } 994 | 995 | #[test] 996 | #[cfg(feature = "rocksdb")] 997 | fn batch_rocksdb() { 998 | let tmp_dir = tempfile::tempdir().unwrap(); 999 | let opt = rocksdb::RocksDBOpt { 1000 | path: tmp_dir.path().to_str().unwrap().to_string(), 1001 | }; 1002 | let map = BenchKVMap::Regular(Arc::new(Box::new(rocksdb::RocksDB::new(&opt)))); 1003 | batch(map); 1004 | } 1005 | } 1006 | -------------------------------------------------------------------------------- /src/bench.rs: -------------------------------------------------------------------------------- 1 | //! The core benchmark functionality. 2 | 3 | use crate::stores::{BenchKVMap, BenchKVMapOpt}; 4 | use crate::workload::{Workload, WorkloadOpt}; 5 | use crate::*; 6 | use figment::providers::{Env, Format, Toml}; 7 | use figment::Figment; 8 | use hashbrown::hash_map::HashMap; 9 | use hdrhistogram::Histogram; 10 | use log::debug; 11 | use parking_lot::Mutex; 12 | use quanta::Instant; 13 | use serde::Deserialize; 14 | use std::rc::Rc; 15 | use std::sync::atomic::{AtomicU64, Ordering}; 16 | use std::sync::{Arc, Barrier}; 17 | use std::time::Duration; 18 | 19 | // {{{ benchmark 20 | 21 | /// Length determines when a benchmark should stop or how often the metrics should be collected. 22 | #[derive(Clone, Debug, PartialEq)] 23 | enum Length { 24 | /// Each worker thread syncs after a timeout (e.g., 0.1s). 25 | Timeout(Duration), 26 | /// Each worker thread syncs after a number of operations (e.g., 1M operations ea.). 27 | Count(u64), 28 | /// Special: exhaust the number of keys in the key space (max - min) 29 | Exhaust, 30 | } 31 | 32 | /// How the results are printed out. 33 | /// "hidden": no results 34 | /// "repeat": only each repeat's own metrics 35 | /// "finish": only the finish metrics 36 | /// "all": equals to repeat + finish 37 | #[derive(Debug, PartialEq)] 38 | enum ReportMode { 39 | Hidden, 40 | Repeat, 41 | Finish, 42 | All, 43 | } 44 | 45 | /// The configuration of a single benchmark deserialized from a TOML string. 46 | /// 47 | /// The fields are optional to ease parsing from TOML, as there can be global parameters that are 48 | /// set for them. The default value will be applied if an option is not specified by both the file 49 | /// and the global option. 50 | /// 51 | /// **Note**: If an option not explicitly marked optional and it is not specified by both the file 52 | /// and the global option, its default value will be applied. If it has no default value, an error 53 | /// will be raised. The precedence of a value is: file > global (after env overridden) > default. 54 | #[derive(Deserialize, Clone, Debug)] 55 | pub struct BenchmarkOpt { 56 | /// Number of threads that runs this benchmark. 57 | /// 58 | /// Default: 1. 59 | pub threads: Option, 60 | 61 | /// How many times this benchmark will be repeated. 62 | /// 63 | /// This option is useful when user would like to plot the performance trend over time in the 64 | /// same benchmark. For example, setting this option to 100 with one second timeout for each 65 | /// repeat can provide 100 data points over a 100 second period. 66 | /// 67 | /// Default: 1. 68 | pub repeat: Option, 69 | 70 | /// How long this benchmark will run, unit is seconds. If this option is specified, the `ops` 71 | /// option will be ignored. 72 | /// 73 | /// Note: see `ops`. 74 | /// 75 | /// *This value is optional.* 76 | pub timeout: Option, 77 | 78 | /// How many operations each worker will execute. Only used if `timeout` is not given. 79 | /// 80 | /// Note: if both `timeout` and `ops` are not given, the run is only stopped when all possible 81 | /// keys are generated. 82 | /// 83 | /// *This value is optional.* 84 | pub ops: Option, 85 | 86 | /// Report mode. 87 | /// 88 | /// - "hidden": not reported. 89 | /// - "repeat": after each repeat, the metrics for that repeat is printed. 90 | /// - "finish": after all repeats are finished, the metrics of the whole phase is printed. 91 | /// - "all": equals to "repeat" + "finish". 92 | /// 93 | /// Default: "all". 94 | pub report: Option, 95 | 96 | /// Max depth of queue for each worker (only used with async stores). 97 | /// 98 | /// When the pending requests are less than `qd`, the worker will not attempt to get more 99 | /// responses. 100 | /// 101 | /// Default: 1. 102 | pub qd: Option, 103 | 104 | /// Batch size for each request (only used with async stores). 105 | /// 106 | /// Default: 1. 107 | pub batch: Option, 108 | 109 | /// Whether or not to record latency during operation. Since measuring time is of extra cost, 110 | /// enabling latency measurement usually affects the throughput metrics. 111 | /// 112 | /// Default: false. 113 | pub latency: Option, 114 | 115 | /// Whether or not to print out latency CDF at the end of each benchmark. If this is set to 116 | /// `true`, `latency` must also be set to `true`. 117 | /// 118 | /// Default: false. 119 | pub cdf: Option, 120 | 121 | /// If set, the overall throughput is roughly limited to the number given (unit in kops). 122 | /// 123 | /// This is useful to check the maximum throughput that the system could reach with 124 | /// controllable latency metrics. If this option is set, `latency` must also be set to true. 125 | /// 0 means unlimited. 126 | /// 127 | /// Default: 0. 128 | pub rate_limit: Option, 129 | 130 | /// The definition of a workload. 131 | /// 132 | /// This section is embedded and flattened, so that you can directly use options in 133 | /// [`WorkloadOpt`]. 134 | #[serde(flatten)] 135 | pub workload: WorkloadOpt, 136 | } 137 | 138 | impl BenchmarkOpt { 139 | /// Internal function called after all global options are applied and when all the options are 140 | /// set. This will test if the opt can be a valid benchmark. It does not check the workload's 141 | /// configuration, as it will be checked when a workload instance is created. 142 | /// 143 | /// Note: `timeout` and `ops` may not be set as of now, and they are not checked. They will be 144 | /// converted to `Length` when creating a new benchmark object. 145 | fn sanity(&self) { 146 | // these must be present, so `unwrap` won't panic. 147 | assert!( 148 | *self.threads.as_ref().unwrap() > 0, 149 | "threads should be positive if given" 150 | ); 151 | assert!( 152 | *self.repeat.as_ref().unwrap() > 0, 153 | "repeat should be positive if given" 154 | ); 155 | match self.report.as_ref().unwrap().as_str() { 156 | "hidden" | "repeat" | "finish" | "all" => {} 157 | _ => panic!("report mode should be one of: hidden, repeat, finish, all"), 158 | } 159 | if let Some(true) = self.cdf { 160 | assert!( 161 | *self.latency.as_ref().unwrap(), 162 | "when cdf is true, latency must also be true" 163 | ); 164 | } 165 | if let Some(r) = self.rate_limit { 166 | if r > 0 { 167 | assert!( 168 | *self.latency.as_ref().unwrap(), 169 | "when rate_limit is set, latency must be true" 170 | ); 171 | } 172 | } 173 | assert!( 174 | *self.qd.as_ref().unwrap() > 0, 175 | "queue depth should be positive if given" 176 | ); 177 | assert!( 178 | *self.batch.as_ref().unwrap() > 0, 179 | "queue depth should be positive if given" 180 | ); 181 | } 182 | } 183 | 184 | /// The configuration of a benchmark, parsed from user's input. 185 | #[derive(Debug, PartialEq)] 186 | pub(crate) struct Benchmark { 187 | threads: usize, 188 | repeat: usize, 189 | qd: usize, 190 | batch: usize, 191 | len: Length, 192 | report: ReportMode, 193 | latency: bool, 194 | cdf: bool, 195 | rate_limit: u64, 196 | wopt: WorkloadOpt, 197 | } 198 | 199 | const TIME_CHECK_INTERVAL: u64 = 32; 200 | 201 | impl Benchmark { 202 | /// The constructor of Benchmark expects all fields have their values, the struct should 203 | /// contain either its own parameters, or carry the default parameters. 204 | fn new(opt: &BenchmarkOpt) -> Self { 205 | opt.sanity(); 206 | let threads = opt.threads.unwrap(); 207 | let repeat = opt.repeat.unwrap(); 208 | let qd = opt.qd.unwrap(); 209 | let batch = opt.batch.unwrap(); 210 | // handle length in the following, now 3 modes 211 | let len = if let Some(t) = opt.timeout { 212 | assert!( 213 | opt.ops.is_none(), 214 | "timeout and ops cannot be provided at the same time" 215 | ); 216 | Length::Timeout(Duration::from_secs_f32(t)) 217 | } else if let Some(c) = opt.ops { 218 | Length::Count(c) 219 | } else { 220 | Length::Exhaust 221 | }; 222 | let report = match opt.report.as_ref().unwrap().as_str() { 223 | "hidden" => ReportMode::Hidden, 224 | "repeat" => ReportMode::Repeat, 225 | "finish" => ReportMode::Finish, 226 | "all" => ReportMode::All, 227 | _ => panic!("Invalid report mode provided"), 228 | }; 229 | let latency = opt.latency.unwrap(); 230 | let cdf = opt.cdf.unwrap(); 231 | let rate_limit = opt.rate_limit.unwrap(); 232 | let wopt = opt.workload.clone(); 233 | Self { 234 | threads, 235 | repeat, 236 | qd, 237 | batch, 238 | len, 239 | report, 240 | latency, 241 | cdf, 242 | rate_limit, 243 | wopt, 244 | } 245 | } 246 | } 247 | 248 | // }}} benchmark 249 | 250 | // {{{ benchmarkgroup 251 | 252 | /// The global options that go to the `[global]` section. 253 | /// 254 | /// They will override the unspecified fields in each `[[benchmark]]` section with the same name. 255 | /// For the usage of each option, please refer to [`BenchmarkOpt`]. 256 | #[derive(Deserialize, Clone, Debug)] 257 | pub struct GlobalOpt { 258 | // benchmark 259 | pub threads: Option, 260 | pub repeat: Option, 261 | pub qd: Option, 262 | pub batch: Option, 263 | pub report: Option, 264 | pub latency: Option, 265 | pub cdf: Option, 266 | pub rate_limit: Option, 267 | // workload 268 | pub klen: Option, 269 | pub vlen: Option, 270 | pub kmin: Option, 271 | pub kmax: Option, 272 | } 273 | 274 | impl Default for GlobalOpt { 275 | fn default() -> Self { 276 | Self { 277 | threads: None, 278 | repeat: None, 279 | qd: None, 280 | batch: None, 281 | report: None, 282 | latency: None, 283 | cdf: None, 284 | rate_limit: None, 285 | klen: None, 286 | vlen: None, 287 | kmin: None, 288 | kmax: None, 289 | } 290 | } 291 | } 292 | 293 | impl GlobalOpt { 294 | fn apply(&self, opt: &mut BenchmarkOpt) { 295 | // benchmark itself (these fall back to defaults) 296 | opt.threads = opt.threads.or_else(|| Some(self.threads.unwrap_or(1))); 297 | opt.repeat = opt.repeat.or_else(|| Some(self.repeat.unwrap_or(1))); 298 | opt.qd = opt.qd.or_else(|| Some(self.qd.unwrap_or(1))); 299 | opt.batch = opt.batch.or_else(|| Some(self.batch.unwrap_or(1))); 300 | opt.report = opt 301 | .report 302 | .clone() 303 | .or_else(|| Some(self.report.clone().unwrap_or("all".to_string()))); 304 | opt.latency = opt 305 | .latency 306 | .clone() 307 | .or_else(|| Some(self.latency.clone().unwrap_or(false))); 308 | opt.cdf = opt 309 | .cdf 310 | .clone() 311 | .or_else(|| Some(self.cdf.clone().unwrap_or(false))); 312 | opt.rate_limit = opt 313 | .rate_limit 314 | .clone() 315 | .or_else(|| Some(self.rate_limit.clone().unwrap_or(0))); 316 | 317 | // the workload options (must be specified) 318 | opt.workload.klen = opt 319 | .workload 320 | .klen 321 | .or_else(|| Some(self.klen.expect("klen should be given"))); 322 | opt.workload.vlen = opt 323 | .workload 324 | .vlen 325 | .or_else(|| Some(self.vlen.expect("vlen should be given"))); 326 | opt.workload.kmin = opt 327 | .workload 328 | .kmin 329 | .or_else(|| Some(self.kmin.expect("kmin should be given"))); 330 | opt.workload.kmax = opt 331 | .workload 332 | .kmax 333 | .or_else(|| Some(self.kmax.expect("kmax should be given"))); 334 | } 335 | } 336 | 337 | /// The configuration of a group of benchmark(s). It has a global option that could possibly 338 | /// override benchmark-local options. 339 | #[derive(Deserialize, Clone, Debug)] 340 | struct BenchmarkGroupOpt { 341 | /// Global parameters (optional) 342 | global: Option, 343 | 344 | /// Map configuration 345 | map: BenchKVMapOpt, 346 | 347 | /// Array of the parameters of consisting Benchmark(s) 348 | benchmark: Vec, 349 | } 350 | 351 | // }}} benchmarkgroup 352 | 353 | // {{{ bencher 354 | 355 | pub(crate) fn init(text: &str) -> (BenchKVMap, Vec>) { 356 | let opt: BenchmarkGroupOpt = Figment::new() 357 | .merge(Toml::string(text)) 358 | .merge(Env::raw()) 359 | .extract() 360 | .unwrap(); 361 | debug!( 362 | "Creating benchmark group with the following configurations: {:?}", 363 | opt 364 | ); 365 | let global = opt.global.clone().unwrap_or_default(); 366 | // now we have a bunch of BenchmarkOpt(s), we need to update their params if they did 367 | // not specify, using the default values given. If all are missing, it's a panic. 368 | let mut bopts: Vec = opt.benchmark.iter().map(|o| o.clone()).collect(); 369 | for bopt in bopts.iter_mut() { 370 | global.apply(bopt); 371 | } 372 | debug!("Global options applied to benchmarks: {:?}", bopts); 373 | // this instance of map is actually not used - the sole purpose is to get a handle out of 374 | // it later in each phase 375 | let map = BenchKVMap::new(&opt.map); 376 | let phases = bopts 377 | .into_iter() 378 | .map(|o| Arc::new(Benchmark::new(&o))) 379 | .collect(); 380 | (map, phases) 381 | } 382 | 383 | fn bench_repeat_should_break( 384 | len: &Length, 385 | count: u64, 386 | start: Instant, 387 | workload: &mut Workload, 388 | ) -> bool { 389 | match len { 390 | Length::Count(c) => { 391 | if count == *c { 392 | return true; 393 | } 394 | } 395 | Length::Timeout(duration) => { 396 | // only checks after a certain interval 397 | if count % TIME_CHECK_INTERVAL == 0 { 398 | if Instant::now().duration_since(start) >= *duration { 399 | return true; 400 | } 401 | } 402 | } 403 | Length::Exhaust => { 404 | if workload.is_exhausted() { 405 | return true; 406 | } 407 | } 408 | } 409 | false 410 | } 411 | 412 | /// A per-worker counter for each repeat in the same benchmark. Using [`AtomicU64`] here makes the 413 | /// measurement `Sync` + `Send` so it can be freely accessed by different threads (mainly by the 414 | /// thread that aggregates the overall measurement). 415 | struct Counter(AtomicU64); 416 | 417 | impl Counter { 418 | fn new() -> Self { 419 | Self(AtomicU64::new(0)) 420 | } 421 | 422 | fn read(&self) -> u64 { 423 | self.0.load(Ordering::Relaxed) 424 | } 425 | 426 | fn reference(&self) -> &mut u64 { 427 | // SAFETY: the counter() method will only be called by the thread that updates its value 428 | unsafe { &mut *self.0.as_ptr() } 429 | } 430 | } 431 | 432 | /// A per-worker latency collector for each repeat in the same benchmark. This is only accessed and 433 | /// collected at the end of each benchmark. 434 | struct Latency { 435 | /// Async only: request_id -> submit time 436 | pending: HashMap, 437 | 438 | /// Latency histogram in us, maximum latency recorded here is 1 second 439 | hdr: Histogram, 440 | } 441 | 442 | impl Latency { 443 | fn new() -> Self { 444 | let pending = HashMap::new(); 445 | let hdr = Histogram::new(3).unwrap(); 446 | Self { pending, hdr } 447 | } 448 | 449 | fn record(&mut self, duration: Duration) { 450 | let us = duration.as_nanos() as u64; 451 | assert!(self.hdr.record(us).is_ok()); 452 | } 453 | 454 | fn async_register(&mut self, id: usize, t: Instant) { 455 | self.pending.insert(id, t); 456 | } 457 | 458 | fn async_record(&mut self, id: usize, t: Instant) { 459 | let d = t - self.pending.remove(&id).unwrap(); 460 | self.record(d); 461 | } 462 | 463 | fn merge(&mut self, other: &Latency) { 464 | assert!(self.pending.is_empty() && other.pending.is_empty()); 465 | assert!(self.hdr.add(&other.hdr).is_ok()); 466 | } 467 | } 468 | 469 | /// The main metrics for each worker thread in the same benchmark. 470 | struct Measurement { 471 | /// Per-repeat counters. This value is actively updated by the worker and loosely evaluated by 472 | /// the main thread. 473 | counters: Vec, 474 | 475 | /// Per-worker latency metrics. This value is avtively updated by the worker if latency needs 476 | /// to be checked, and is shared among all repeats. It is only merged at the end of a whole 477 | /// benchmark. 478 | latency: Mutex, 479 | 480 | /// The duration of each repeat that is measured by the corresponding worker thread. It is only 481 | /// updated once after a repeat is really done. In a time-limited run, the master thread will 482 | /// try to access the duration. If an entry exists, it means the thread has finished execution, 483 | /// so the master will directly use the time duration observed by the worker. If an entry is 484 | /// not here, the time will be observed by the master. 485 | durations: Vec>>, 486 | } 487 | 488 | impl Measurement { 489 | fn new(repeat: usize) -> Self { 490 | let counters = (0..repeat).into_iter().map(|_| Counter::new()).collect(); 491 | let latency = Mutex::new(Latency::new()); 492 | let durations = (0..repeat).into_iter().map(|_| Mutex::new(None)).collect(); 493 | Self { 494 | counters, 495 | latency, 496 | durations, 497 | } 498 | } 499 | } 500 | 501 | struct WorkerContext { 502 | /// The benchmark phase that the current work is referring to 503 | benchmark: Arc, 504 | 505 | /// The very beginning of all benchmarks in a group, for calculating elapsed timestamp 506 | since: Instant, 507 | 508 | /// The current phase of this benchmark in the group 509 | phase: usize, 510 | 511 | /// The measurement of all worker threads. One worker typically only needs to refer to one of 512 | /// them, and the master thread (thread.id == repeat) will aggregate the metrics and make an 513 | /// output 514 | measurements: Vec>, 515 | 516 | /// Barrier that syncs all workers 517 | barrier: Arc, 518 | 519 | /// `(worker_id, nr_threads)` pair, used to determine the identity of a worker and also 520 | thread_info: (usize, usize), 521 | } 522 | 523 | fn bench_stat_repeat( 524 | benchmark: &Arc, 525 | phase: usize, 526 | repeat: usize, 527 | since: Instant, 528 | start: Instant, 529 | end: Instant, 530 | thread_info: (usize, usize), 531 | measurements: &Vec>, 532 | ) { 533 | if benchmark.report != ReportMode::Repeat && benchmark.report != ReportMode::All { 534 | return; 535 | } 536 | 537 | assert!(thread_info.0 == 0); 538 | let mut throughput = 0.0f64; 539 | let mut total = 0u64; 540 | for i in 0..thread_info.1 { 541 | let d = match *measurements[i].durations[repeat].lock() { 542 | Some(d) => d, 543 | None => { 544 | // only applies to time-limited benchmarks 545 | assert!(matches!(benchmark.len, Length::Timeout(_))); 546 | start.elapsed() 547 | } 548 | }; 549 | let ops = measurements[i].counters[repeat].read(); 550 | let tput = ops as f64 / d.as_secs_f64() / 1_000_000.0; 551 | total += ops; 552 | throughput += tput; 553 | } 554 | 555 | let duration = (end - start).as_secs_f64(); 556 | let elapsed = (end - since).as_secs_f64(); 557 | 558 | println!( 559 | "phase {} repeat {} duration {:.2} elapsed {:.2} total {} mops {:.2}", 560 | phase, repeat, duration, elapsed, total, throughput, 561 | ); 562 | } 563 | 564 | fn bench_stat_final( 565 | benchmark: &Arc, 566 | phase: usize, 567 | since: Instant, 568 | start: Instant, 569 | end: Instant, 570 | thread_info: (usize, usize), 571 | measurements: &Vec>, 572 | ) { 573 | if benchmark.report != ReportMode::Finish && benchmark.report != ReportMode::All { 574 | return; 575 | } 576 | 577 | assert!(thread_info.0 == 0); 578 | let mut total = 0u64; 579 | let mut latency = Latency::new(); 580 | for i in 0..thread_info.1 { 581 | for j in 0..benchmark.repeat { 582 | let ops = measurements[i].counters[j].read(); 583 | total += ops; 584 | } 585 | latency.merge(&measurements[i].latency.lock()); 586 | } 587 | 588 | let duration = (end - start).as_secs_f64(); 589 | let elapsed = (end - since).as_secs_f64(); 590 | 591 | let throughput = total as f64 / duration / 1_000_000.0; 592 | 593 | print!( 594 | "phase {} finish . duration {:.2} elapsed {:.2} total {} mops {:.2}", 595 | phase, duration, elapsed, total, throughput, 596 | ); 597 | if benchmark.latency { 598 | print!(" "); 599 | assert_eq!(total, latency.hdr.len()); 600 | let hdr = &latency.hdr; 601 | print!( 602 | "min_us {:.2} max_us {:.2} avg_us {:.2} \ 603 | p50_us {:.2} p95_us {:.2} p99_us {:.2} p999_us {:.2}", 604 | hdr.min() as f64 / 1000.0, 605 | hdr.max() as f64 / 1000.0, 606 | hdr.mean() / 1000.0, 607 | hdr.value_at_quantile(0.50) as f64 / 1000.0, 608 | hdr.value_at_quantile(0.95) as f64 / 1000.0, 609 | hdr.value_at_quantile(0.99) as f64 / 1000.0, 610 | hdr.value_at_quantile(0.999) as f64 / 1000.0, 611 | ); 612 | if benchmark.cdf { 613 | print!(" cdf_us percentile "); 614 | let mut cdf = 0; 615 | for v in latency.hdr.iter_linear(1000) { 616 | let ns = v.value_iterated_to(); 617 | let us = (ns + 1) / 1000; 618 | cdf += v.count_since_last_iteration(); 619 | print!("{} {:.2}", us, cdf as f64 * 100.0 / total as f64); 620 | if ns >= hdr.max() { 621 | break; 622 | } 623 | print!(" "); 624 | } 625 | assert_eq!(cdf, total); 626 | } 627 | } 628 | 629 | println!(); 630 | } 631 | 632 | /// A simple rate limiter when the benchmark needs to limit its throughput. 633 | /// 634 | /// If the `ops` given is non-zero, calling `backoff` will halt the process until the target 635 | /// throughput is achieved. Otherwise, it does nothing. 636 | struct RateLimiter { 637 | ops: u64, 638 | start: Instant, 639 | } 640 | 641 | impl RateLimiter { 642 | fn new(kops: u64, nr_threads: usize, start: Instant) -> Self { 643 | assert!(kops > 0); 644 | let ops = kops * 1000 / u64::try_from(nr_threads).unwrap(); 645 | Self { ops, start } 646 | } 647 | 648 | /// Returns whether the backoff is done. 649 | #[inline(always)] 650 | fn try_backoff(&self, count: u64) -> bool { 651 | let elapsed = u64::try_from(self.start.elapsed().as_nanos()).unwrap(); 652 | let ops = count * 1_000_000_000 / elapsed; 653 | if ops <= self.ops { 654 | return true; 655 | } 656 | false 657 | } 658 | 659 | /// Blocking backoff. 660 | #[inline(always)] 661 | fn backoff(&self, count: u64) { 662 | loop { 663 | if self.try_backoff(count) { 664 | break; 665 | } 666 | } 667 | } 668 | } 669 | 670 | fn bench_worker_regular(map: Arc>, context: WorkerContext) { 671 | let thread = map.thread(); 672 | 673 | let WorkerContext { 674 | benchmark, 675 | since, 676 | phase, 677 | measurements, 678 | barrier, 679 | thread_info, 680 | } = context; 681 | 682 | let id = thread_info.0; 683 | thread.pin(id); 684 | 685 | // if record latency, take the lock guard of the latency counter until all repeats are done 686 | let mut latency = match benchmark.latency { 687 | true => Some(measurements[id].latency.lock()), 688 | false => None, 689 | }; 690 | let latency_tick = match latency { 691 | Some(_) => || Some(Instant::now()), 692 | None => || None, 693 | }; 694 | 695 | let mut handle = map.handle(); 696 | let mut rng = rand::rng(); 697 | let mut workload = Workload::new(&benchmark.wopt, Some(thread_info)); 698 | let start = Instant::now(); 699 | 700 | for i in 0..benchmark.repeat { 701 | let counter = measurements[id].counters[i].reference(); 702 | // start the benchmark phase at roughly the same time 703 | barrier.wait(); 704 | let start = Instant::now(); 705 | 706 | let rate_limiter = match benchmark.rate_limit { 707 | 0 => None, 708 | r => Some(RateLimiter::new(r, thread_info.1, start)), 709 | }; 710 | 711 | // start benchmark 712 | loop { 713 | // check if we need to break 714 | if bench_repeat_should_break(&benchmark.len, *counter, start, &mut workload) { 715 | workload.reset(); 716 | break; 717 | } 718 | 719 | let op = workload.next(&mut rng); 720 | let op_start = latency_tick(); 721 | match op { 722 | Operation::Set { key, value } => { 723 | handle.set(&key[..], &value[..]); 724 | } 725 | Operation::Get { key } => { 726 | let _ = handle.get(&key[..]); 727 | } 728 | Operation::Delete { key } => { 729 | handle.delete(&key[..]); 730 | } 731 | Operation::Scan { key, n } => { 732 | let _ = handle.scan(&key[..], n); 733 | } 734 | } 735 | let op_end = latency_tick(); 736 | if let Some(l) = &mut latency { 737 | l.record(op_end.unwrap() - op_start.unwrap()); 738 | } 739 | *counter += 1; 740 | 741 | if let Some(r) = &rate_limiter { 742 | r.backoff(*counter); 743 | } 744 | } 745 | 746 | // after the execution, counter is up-to-date, so it's time to update duration 747 | let end = Instant::now(); 748 | *measurements[id].durations[i].lock() = Some(end.duration_since(start.clone())); 749 | 750 | // for non time-limited benchmarks, sync first to make sure that all threads have finished 751 | // if a benchmark is time limited, loosely evaluate the metrics 752 | if !matches!(benchmark.len, Length::Timeout(_)) { 753 | barrier.wait(); 754 | } 755 | 756 | // master is 0, it will aggregate data and print info inside this call 757 | if id == 0 { 758 | bench_stat_repeat( 759 | &benchmark, 760 | phase, 761 | i, 762 | since, 763 | start, 764 | end, 765 | thread_info, 766 | &measurements, 767 | ); 768 | } 769 | } 770 | 771 | drop(latency); 772 | 773 | // every thread will sync on this 774 | barrier.wait(); 775 | 776 | if id == 0 { 777 | let end = Instant::now(); 778 | bench_stat_final( 779 | &benchmark, 780 | phase, 781 | since, 782 | start, 783 | end, 784 | thread_info, 785 | &measurements, 786 | ); 787 | } 788 | } 789 | 790 | fn bench_worker_async(map: Arc>, context: WorkerContext) { 791 | let thread = map.thread(); 792 | 793 | let WorkerContext { 794 | benchmark, 795 | since, 796 | phase, 797 | measurements, 798 | barrier, 799 | thread_info, 800 | } = context; 801 | 802 | let id = thread_info.0; 803 | thread.pin(id); 804 | 805 | // if record latency, take the lock guard of the latency counter until all repeats are done 806 | let mut latency = match benchmark.latency { 807 | true => Some(measurements[id].latency.lock()), 808 | false => None, 809 | }; 810 | 811 | let responder = Rc::new(RefCell::new(Vec::::new())); 812 | let mut handle = map.handle(responder.clone()); 813 | let mut rng = rand::rng(); 814 | let mut workload = Workload::new(&benchmark.wopt, Some(thread_info)); 815 | // pending requests is global, as it is not needed to drain all requests after each repeat 816 | let mut pending = 0usize; 817 | let mut requests = Vec::::with_capacity(benchmark.batch); 818 | let mut rid = 0usize; 819 | let start = Instant::now(); 820 | 821 | for i in 0..benchmark.repeat { 822 | let counter = measurements[id].counters[i].reference(); 823 | // start the benchmark phase at roughly the same time 824 | barrier.wait(); 825 | let start = Instant::now(); 826 | 827 | let rate_limiter = match benchmark.rate_limit { 828 | 0 => None, 829 | r => Some(RateLimiter::new(r, thread_info.1, start)), 830 | }; 831 | 832 | // start benchmark 833 | loop { 834 | // first clear the requests vector 835 | requests.clear(); 836 | // sample requests 837 | for _ in 0..benchmark.batch { 838 | // stop the batch generation if the repeat is done 839 | if bench_repeat_should_break(&benchmark.len, *counter, start, &mut workload) { 840 | break; 841 | } 842 | 843 | let op = workload.next(&mut rng); 844 | requests.push(Request { id: rid, op }); 845 | rid += 1; 846 | // need to add to count here, instead of after this loop 847 | // otherwise the last check may fail because the time check is after a certain 848 | // interval, but the mod is never 0 849 | *counter += 1; 850 | } 851 | 852 | // now we have a batch, send it all, whatever its size is 853 | let len = requests.len(); 854 | handle.submit(&requests); 855 | pending += len; 856 | if let Some(l) = &mut latency { 857 | let submit = Instant::now(); 858 | for r in requests.iter() { 859 | l.async_register(r.id, submit); 860 | } 861 | } 862 | 863 | if bench_repeat_should_break(&benchmark.len, *counter, start, &mut workload) { 864 | workload.reset(); 865 | break; 866 | } 867 | 868 | // use a loop to make sure that pending is under qd, only drain the handle if the bench 869 | // phase is not ending 870 | loop { 871 | // do not drain if the pending queue is empty 872 | if pending > 0 { 873 | handle.drain(); 874 | let responses = responder.replace_with(|_| Vec::new()); 875 | pending -= responses.len(); 876 | if let Some(l) = &mut latency { 877 | let submit = Instant::now(); 878 | for r in responses.iter() { 879 | l.async_record(r.id, submit); 880 | } 881 | } 882 | } 883 | let backoff_free = match &rate_limiter { 884 | None => true, 885 | Some(r) => { 886 | if r.try_backoff(*counter) { 887 | true 888 | } else { 889 | false 890 | } 891 | } 892 | }; 893 | // if the pending queue is under depth (can be 0) and no further backoff is needed 894 | if pending <= benchmark.qd && backoff_free { 895 | break; 896 | } 897 | } 898 | } 899 | 900 | // after the execution, counter is up-to-date, so it's time to update duration 901 | let end = Instant::now(); 902 | *measurements[id].durations[i].lock() = Some(end.duration_since(start.clone())); 903 | 904 | // for non time-limited benchmarks, sync first to make sure that all threads have finished 905 | // if a benchmark is time limited, loosely evaluate the metrics 906 | if !matches!(benchmark.len, Length::Timeout(_)) { 907 | barrier.wait(); 908 | } 909 | 910 | // master is 0, it will aggregate data and print info inside this call 911 | if id == 0 { 912 | bench_stat_repeat( 913 | &benchmark, 914 | phase, 915 | i, 916 | since, 917 | start, 918 | end, 919 | thread_info, 920 | &measurements, 921 | ); 922 | } 923 | } 924 | 925 | // wait until all requests are back 926 | loop { 927 | if pending == 0 { 928 | break; 929 | } 930 | handle.drain(); 931 | let responses = responder.replace_with(|_| Vec::new()); 932 | pending -= responses.len(); 933 | if let Some(l) = &mut latency { 934 | let submit = Instant::now(); 935 | for r in responses.iter() { 936 | l.async_record(r.id, submit); 937 | } 938 | } 939 | } 940 | 941 | drop(latency); 942 | 943 | // every thread will sync on this 944 | barrier.wait(); 945 | 946 | if id == 0 { 947 | let end = Instant::now(); 948 | bench_stat_final( 949 | &benchmark, 950 | phase, 951 | since, 952 | start, 953 | end, 954 | thread_info, 955 | &measurements, 956 | ); 957 | } 958 | } 959 | 960 | fn bench_phase_regular( 961 | map: Arc>, 962 | benchmark: Arc, 963 | phase: usize, 964 | since: Arc, 965 | ) { 966 | let thread = map.thread(); 967 | let barrier = Arc::new(Barrier::new(benchmark.threads.try_into().unwrap())); 968 | let measurements: Vec> = (0..benchmark.threads) 969 | .map(|_| Arc::new(Measurement::new(benchmark.repeat))) 970 | .collect(); 971 | let mut handles = Vec::new(); 972 | for t in 0..benchmark.threads { 973 | let map = map.clone(); 974 | let benchmark = benchmark.clone(); 975 | let barrier = barrier.clone(); 976 | let thread_info = (t, benchmark.threads); 977 | let context = WorkerContext { 978 | benchmark, 979 | phase, 980 | measurements: measurements.clone(), 981 | barrier, 982 | since: *since, 983 | thread_info, 984 | }; 985 | let handle = thread.spawn(Box::new(move || { 986 | bench_worker_regular(map, context); 987 | })); 988 | handles.push(handle); 989 | } 990 | 991 | // join thread 0 992 | handles.pop().unwrap().join(); 993 | 994 | while let Some(handle) = handles.pop() { 995 | handle.join(); 996 | } 997 | } 998 | 999 | fn bench_phase_async( 1000 | map: Arc>, 1001 | benchmark: Arc, 1002 | phase: usize, 1003 | since: Arc, 1004 | ) { 1005 | let thread = map.thread(); 1006 | let barrier = Arc::new(Barrier::new((benchmark.threads).try_into().unwrap())); 1007 | let measurements: Vec> = (0..benchmark.threads) 1008 | .map(|_| Arc::new(Measurement::new(benchmark.repeat))) 1009 | .collect(); 1010 | let mut handles = Vec::new(); 1011 | for t in 0..benchmark.threads { 1012 | let map = map.clone(); 1013 | let benchmark = benchmark.clone(); 1014 | let barrier = barrier.clone(); 1015 | let thread_info = (t, benchmark.threads); 1016 | let context = WorkerContext { 1017 | benchmark, 1018 | phase, 1019 | measurements: measurements.clone(), 1020 | barrier, 1021 | since: *since, 1022 | thread_info, 1023 | }; 1024 | let handle = thread.spawn(Box::new(move || { 1025 | bench_worker_async(map, context); 1026 | })); 1027 | handles.push(handle); 1028 | } 1029 | 1030 | handles.pop().unwrap().join(); 1031 | 1032 | while let Some(handle) = handles.pop() { 1033 | handle.join(); 1034 | } 1035 | } 1036 | 1037 | /// The real benchmark function for [`KVMap`]. 1038 | /// 1039 | /// **You may not need to check this if it is OK to run benchmarks with [`std::thread`].** 1040 | pub(crate) fn bench_regular(map: Arc>, phases: &Vec>) { 1041 | debug!("Running regular bencher"); 1042 | let start = Arc::new(Instant::now()); 1043 | for (i, p) in phases.iter().enumerate() { 1044 | bench_phase_regular(map.clone(), p.clone(), i, start.clone()); 1045 | } 1046 | } 1047 | 1048 | /// The real benchmark function for [`AsyncKVMap`]. 1049 | /// 1050 | /// **You may not need to check this if it is OK to run benchmarks with [`std::thread`].** 1051 | pub(crate) fn bench_async(map: Arc>, phases: &Vec>) { 1052 | debug!("Running async bencher"); 1053 | let start = Arc::new(Instant::now()); 1054 | for (i, p) in phases.iter().enumerate() { 1055 | bench_phase_async(map.clone(), p.clone(), i, start.clone()); 1056 | } 1057 | } 1058 | 1059 | // }}} bencher 1060 | 1061 | // {{{ tests 1062 | 1063 | #[cfg(test)] 1064 | mod tests { 1065 | use super::*; 1066 | 1067 | #[test] 1068 | fn global_options_are_applied() { 1069 | let opt = r#" 1070 | [map] 1071 | name = "nullmap" 1072 | 1073 | [global] 1074 | threads = 8 1075 | repeat = 10 1076 | qd = 10 1077 | batch = 15 1078 | report = "finish" 1079 | latency = true 1080 | cdf = true 1081 | rate_limit = 5 1082 | klen = 8 1083 | vlen = 16 1084 | kmin = 100 1085 | kmax = 1000 1086 | 1087 | [[benchmark]] 1088 | timeout = 10.0 1089 | set_perc = 50 1090 | get_perc = 30 1091 | del_perc = 10 1092 | scan_perc = 10 1093 | dist = "incrementp" 1094 | "#; 1095 | 1096 | let (_, bg) = init(opt); 1097 | assert_eq!(bg.len(), 1); 1098 | 1099 | let wopt = WorkloadOpt { 1100 | set_perc: Some(50), 1101 | get_perc: Some(30), 1102 | del_perc: Some(10), 1103 | scan_perc: Some(10), 1104 | dist: "incrementp".to_string(), 1105 | scan_n: None, 1106 | klen: Some(8), 1107 | vlen: Some(16), 1108 | kmin: Some(100), 1109 | kmax: Some(1000), 1110 | zipf_theta: None, 1111 | zipf_hotspot: None, 1112 | }; 1113 | 1114 | let benchmark = Benchmark { 1115 | threads: 8, 1116 | repeat: 10, 1117 | qd: 10, 1118 | batch: 15, 1119 | report: ReportMode::Finish, 1120 | latency: true, 1121 | cdf: true, 1122 | rate_limit: 5, 1123 | len: Length::Timeout(Duration::from_secs_f32(10.0)), 1124 | wopt, 1125 | }; 1126 | 1127 | assert_eq!(*bg[0], benchmark) 1128 | } 1129 | 1130 | #[test] 1131 | fn global_options_defaults_are_applied() { 1132 | let opt = r#" 1133 | [map] 1134 | name = "nullmap" 1135 | 1136 | [[benchmark]] 1137 | set_perc = 50 1138 | get_perc = 30 1139 | del_perc = 10 1140 | scan_perc = 10 1141 | klen = 8 1142 | vlen = 16 1143 | kmin = 1 1144 | kmax = 1000 1145 | dist = "shufflep" 1146 | "#; 1147 | 1148 | let (_, bg) = init(opt); 1149 | assert_eq!(bg.len(), 1); 1150 | 1151 | let wopt = WorkloadOpt { 1152 | set_perc: Some(50), 1153 | get_perc: Some(30), 1154 | del_perc: Some(10), 1155 | scan_perc: Some(10), 1156 | scan_n: None, 1157 | dist: "shufflep".to_string(), 1158 | klen: Some(8), 1159 | vlen: Some(16), 1160 | kmin: Some(1), 1161 | kmax: Some(1000), 1162 | zipf_theta: None, 1163 | zipf_hotspot: None, 1164 | }; 1165 | 1166 | let benchmark = Benchmark { 1167 | threads: 1, 1168 | repeat: 1, 1169 | qd: 1, 1170 | batch: 1, 1171 | report: ReportMode::All, 1172 | latency: false, 1173 | cdf: false, 1174 | rate_limit: 0, 1175 | len: Length::Exhaust, 1176 | wopt, 1177 | }; 1178 | 1179 | assert_eq!(*bg[0], benchmark) 1180 | } 1181 | 1182 | #[test] 1183 | #[should_panic(expected = "should be positive")] 1184 | fn invalid_threads() { 1185 | let opt = r#" 1186 | [map] 1187 | name = "nullmap" 1188 | 1189 | [global] 1190 | klen = 8 1191 | vlen = 16 1192 | kmin = 0 1193 | kmax = 1000 1194 | 1195 | [[benchmark]] 1196 | threads = 0 1197 | timeout = 1.0 1198 | set_perc = 100 1199 | get_perc = 0 1200 | del_perc = 0 1201 | scan_perc = 0 1202 | dist = "incrementp" 1203 | "#; 1204 | 1205 | let (_, _) = init(opt); 1206 | } 1207 | 1208 | #[test] 1209 | #[should_panic(expected = "should be positive")] 1210 | fn invalid_repeat() { 1211 | let opt = r#" 1212 | [map] 1213 | name = "nullmap" 1214 | 1215 | [global] 1216 | klen = 8 1217 | vlen = 16 1218 | kmin = 0 1219 | kmax = 1000 1220 | 1221 | [[benchmark]] 1222 | repeat = 0 1223 | timeout = 1.0 1224 | set_perc = 100 1225 | get_perc = 0 1226 | del_perc = 0 1227 | scan_perc = 0 1228 | dist = "incrementp" 1229 | "#; 1230 | 1231 | let (_, _) = init(opt); 1232 | } 1233 | 1234 | #[test] 1235 | #[should_panic(expected = "report mode should be one of")] 1236 | fn invalid_report() { 1237 | let opt = r#" 1238 | [map] 1239 | name = "nullmap" 1240 | 1241 | [global] 1242 | klen = 8 1243 | vlen = 16 1244 | kmin = 0 1245 | kmax = 1000 1246 | 1247 | [[benchmark]] 1248 | timeout = 1.0 1249 | set_perc = 100 1250 | get_perc = 0 1251 | del_perc = 0 1252 | scan_perc = 0 1253 | dist = "incrementp" 1254 | report = "alll" 1255 | "#; 1256 | 1257 | let (_, _) = init(opt); 1258 | } 1259 | 1260 | #[test] 1261 | #[should_panic(expected = "cannot be provided at the same time")] 1262 | fn invalid_length() { 1263 | let opt = r#" 1264 | [map] 1265 | name = "nullmap" 1266 | 1267 | [global] 1268 | klen = 8 1269 | vlen = 16 1270 | kmin = 0 1271 | kmax = 1000 1272 | 1273 | [[benchmark]] 1274 | timeout = 1.0 1275 | ops = 1000 1276 | set_perc = 100 1277 | get_perc = 0 1278 | del_perc = 0 1279 | scan_perc = 0 1280 | dist = "incrementp" 1281 | "#; 1282 | 1283 | let (_, _) = init(opt); 1284 | } 1285 | 1286 | #[test] 1287 | #[should_panic(expected = "latency must also be true")] 1288 | fn invalid_latency_cdf() { 1289 | let opt = r#" 1290 | [map] 1291 | name = "nullmap" 1292 | 1293 | [global] 1294 | klen = 8 1295 | vlen = 16 1296 | kmin = 0 1297 | kmax = 1000 1298 | 1299 | [[benchmark]] 1300 | timeout = 1.0 1301 | cdf = true 1302 | set_perc = 100 1303 | get_perc = 0 1304 | del_perc = 0 1305 | scan_perc = 0 1306 | dist = "incrementp" 1307 | "#; 1308 | 1309 | let (_, _) = init(opt); 1310 | } 1311 | 1312 | #[test] 1313 | #[should_panic(expected = "latency must be true")] 1314 | fn invalid_latency_rate_limit() { 1315 | let opt = r#" 1316 | [map] 1317 | name = "nullmap" 1318 | 1319 | [global] 1320 | klen = 8 1321 | vlen = 16 1322 | kmin = 0 1323 | kmax = 1000 1324 | 1325 | [[benchmark]] 1326 | timeout = 1.0 1327 | rate_limit = 1000 1328 | set_perc = 100 1329 | get_perc = 0 1330 | del_perc = 0 1331 | scan_perc = 0 1332 | dist = "incrementp" 1333 | "#; 1334 | 1335 | let (_, _) = init(opt); 1336 | } 1337 | 1338 | const EXAMPLE_BENCH: &str = include_str!(concat!( 1339 | env!("CARGO_MANIFEST_DIR"), 1340 | "/presets/benchmarks/example.toml" 1341 | )); 1342 | 1343 | const EXAMPLE_SCAN_BENCH: &str = include_str!(concat!( 1344 | env!("CARGO_MANIFEST_DIR"), 1345 | "/presets/benchmarks/example_scan.toml" 1346 | )); 1347 | 1348 | fn example(map_opt: &str, check: bool) { 1349 | let _ = env_logger::try_init(); 1350 | let opt = map_opt.to_string() + "\n" + EXAMPLE_BENCH; 1351 | let (map, phases) = init(&opt); 1352 | map.bench(&phases); 1353 | if check { 1354 | if let BenchKVMap::Regular(m) = map { 1355 | let mut handle = m.handle(); 1356 | // set 0-30000 and delete 5000-25000 1357 | for k in 0..5000u64 { 1358 | let key = k.to_be_bytes(); 1359 | assert!(handle.get(&key).is_some()); 1360 | } 1361 | for k in 5000..25000u64 { 1362 | let key = k.to_be_bytes(); 1363 | assert!(handle.get(&key).is_none()); 1364 | } 1365 | for k in 25000..30000u64 { 1366 | let key = k.to_be_bytes(); 1367 | assert!(handle.get(&key).is_some()); 1368 | } 1369 | for k in 30000..50000u64 { 1370 | let key = k.to_be_bytes(); 1371 | assert!(handle.get(&key).is_none()); 1372 | } 1373 | } 1374 | } 1375 | } 1376 | 1377 | fn example_scan(map_opt: &str) { 1378 | let _ = env_logger::try_init(); 1379 | let opt = map_opt.to_string() + "\n" + EXAMPLE_SCAN_BENCH; 1380 | let (map, phases) = init(&opt); 1381 | map.bench(&phases); 1382 | } 1383 | 1384 | #[test] 1385 | fn example_null() { 1386 | const OPT: &str = include_str!(concat!( 1387 | env!("CARGO_MANIFEST_DIR"), 1388 | "/presets/stores/null.toml" 1389 | )); 1390 | example(OPT, false); 1391 | } 1392 | 1393 | #[test] 1394 | fn example_scan_null() { 1395 | const OPT: &str = include_str!(concat!( 1396 | env!("CARGO_MANIFEST_DIR"), 1397 | "/presets/stores/null.toml" 1398 | )); 1399 | example_scan(OPT); 1400 | } 1401 | 1402 | #[test] 1403 | fn example_null_async() { 1404 | const OPT: &str = include_str!(concat!( 1405 | env!("CARGO_MANIFEST_DIR"), 1406 | "/presets/stores/null_async.toml" 1407 | )); 1408 | example(OPT, false); 1409 | } 1410 | 1411 | #[test] 1412 | fn example_scan_null_async() { 1413 | const OPT: &str = include_str!(concat!( 1414 | env!("CARGO_MANIFEST_DIR"), 1415 | "/presets/stores/null_async.toml" 1416 | )); 1417 | example_scan(OPT); 1418 | } 1419 | 1420 | #[test] 1421 | fn example_mutex_hashmap() { 1422 | const OPT: &str = include_str!(concat!( 1423 | env!("CARGO_MANIFEST_DIR"), 1424 | "/presets/stores/mutex_hashmap.toml" 1425 | )); 1426 | example(OPT, true); 1427 | } 1428 | 1429 | #[test] 1430 | fn example_rwlock_hashmap() { 1431 | const OPT: &str = include_str!(concat!( 1432 | env!("CARGO_MANIFEST_DIR"), 1433 | "/presets/stores/rwlock_hashmap.toml" 1434 | )); 1435 | example(OPT, true); 1436 | } 1437 | 1438 | #[test] 1439 | #[cfg(feature = "dashmap")] 1440 | fn example_dashmap() { 1441 | const OPT: &str = include_str!(concat!( 1442 | env!("CARGO_MANIFEST_DIR"), 1443 | "/presets/stores/dashmap.toml" 1444 | )); 1445 | example(OPT, true); 1446 | } 1447 | 1448 | #[test] 1449 | #[cfg(feature = "contrie")] 1450 | fn example_contrie() { 1451 | const OPT: &str = include_str!(concat!( 1452 | env!("CARGO_MANIFEST_DIR"), 1453 | "/presets/stores/contrie.toml" 1454 | )); 1455 | example(OPT, true); 1456 | } 1457 | 1458 | #[test] 1459 | #[cfg(feature = "chashmap")] 1460 | fn example_chashmap() { 1461 | const OPT: &str = include_str!(concat!( 1462 | env!("CARGO_MANIFEST_DIR"), 1463 | "/presets/stores/chashmap.toml" 1464 | )); 1465 | example(OPT, true); 1466 | } 1467 | 1468 | #[test] 1469 | #[cfg(feature = "scc")] 1470 | fn example_scchashmap() { 1471 | const OPT: &str = include_str!(concat!( 1472 | env!("CARGO_MANIFEST_DIR"), 1473 | "/presets/stores/scchashmap.toml" 1474 | )); 1475 | example(OPT, true); 1476 | } 1477 | 1478 | #[test] 1479 | #[cfg(feature = "flurry")] 1480 | fn example_flurry() { 1481 | const OPT: &str = include_str!(concat!( 1482 | env!("CARGO_MANIFEST_DIR"), 1483 | "/presets/stores/flurry.toml" 1484 | )); 1485 | example(OPT, true); 1486 | } 1487 | 1488 | #[test] 1489 | #[cfg(feature = "papaya")] 1490 | fn example_papaya() { 1491 | const OPT: &str = include_str!(concat!( 1492 | env!("CARGO_MANIFEST_DIR"), 1493 | "/presets/stores/papaya.toml" 1494 | )); 1495 | example(OPT, true); 1496 | } 1497 | 1498 | #[test] 1499 | fn example_mutex_btreemap() { 1500 | const OPT: &str = include_str!(concat!( 1501 | env!("CARGO_MANIFEST_DIR"), 1502 | "/presets/stores/mutex_btreemap.toml" 1503 | )); 1504 | example(OPT, true); 1505 | } 1506 | 1507 | #[test] 1508 | fn example_rwlock_btreemap() { 1509 | const OPT: &str = include_str!(concat!( 1510 | env!("CARGO_MANIFEST_DIR"), 1511 | "/presets/stores/rwlock_btreemap.toml" 1512 | )); 1513 | example(OPT, true); 1514 | } 1515 | 1516 | #[test] 1517 | #[cfg(feature = "rocksdb")] 1518 | fn example_rocksdb() { 1519 | let tmp_dir = tempfile::tempdir().unwrap(); 1520 | let opt = format!( 1521 | r#" 1522 | [map] 1523 | name = "rocksdb" 1524 | path = "{}" 1525 | "#, 1526 | tmp_dir.path().to_str().unwrap().to_string() 1527 | ); 1528 | example(&opt, true); 1529 | } 1530 | 1531 | #[test] 1532 | #[cfg(feature = "rocksdb")] 1533 | fn example_scan_rocksdb() { 1534 | let tmp_dir = tempfile::tempdir().unwrap(); 1535 | let opt = format!( 1536 | r#" 1537 | [map] 1538 | name = "rocksdb" 1539 | path = "{}" 1540 | "#, 1541 | tmp_dir.path().to_str().unwrap().to_string() 1542 | ); 1543 | example_scan(&opt); 1544 | } 1545 | } 1546 | 1547 | // }}} tests 1548 | --------------------------------------------------------------------------------