├── .editorconfig ├── .github └── workflows │ ├── benchmark.yml │ ├── ci.yml │ └── nightly.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── img └── hero.png └── src ├── allocator.rs ├── arangodb.rs ├── benchmark.rs ├── database.rs ├── dialect.rs ├── docker.rs ├── dragonfly.rs ├── dry.rs ├── echodb.rs ├── engine.rs ├── fjall.rs ├── keydb.rs ├── keyprovider.rs ├── lmdb.rs ├── main.rs ├── map.rs ├── memodb.rs ├── mongodb.rs ├── mysql.rs ├── neo4j.rs ├── postgres.rs ├── profiling.rs ├── redb.rs ├── redis.rs ├── result.rs ├── rocksdb.rs ├── scylladb.rs ├── sqlite.rs ├── surrealdb.rs ├── surrealkv.rs ├── terminal.rs └── valueprovider.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig coding styles definitions. For more information about the 2 | # properties used in this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = LF 10 | indent_size = 4 11 | indent_style = tab 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.toml] 16 | indent_size = 4 17 | indent_style = space 18 | 19 | [*.{yml,json}] 20 | indent_size = 2 21 | indent_style = space 22 | 23 | [*.{md,diff}] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 6 * * *" 7 | 8 | concurrency: 9 | # Use github.run_id on main branch 10 | # Use github.event.pull_request.number on pull requests, so it's unique per pull request 11 | # Use github.ref on other branches, so it's unique per branch 12 | group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }} 13 | cancel-in-progress: true 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | jobs: 20 | build: 21 | name: Build crud-bench 22 | runs-on: [runner-amd64-large] 23 | steps: 24 | - name: Install stable toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | 27 | - name: Checkout sources 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup cache 31 | uses: Swatinem/rust-cache@v2 32 | with: 33 | cache-on-failure: true 34 | save-if: ${{ github.ref == 'refs/heads/main' }} 35 | 36 | - name: Build benchmark 37 | run: cargo build --release --target x86_64-unknown-linux-gnu 38 | 39 | - name: Store artifacts 40 | run: cp target/x86_64-unknown-linux-gnu/release/crud-bench crud-bench 41 | 42 | - name: Upload artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: crud-bench 46 | path: crud-bench 47 | 48 | benchmark: 49 | name: Benchmark ${{ matrix.description }} 50 | needs: build 51 | runs-on: [runner-amd64-4xlarge] 52 | continue-on-error: true 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | include: 57 | # ArangoDB 58 | - name: arangodb 59 | database: arangodb 60 | enabled: true 61 | description: ArangoDB 62 | # Cassandra 63 | - name: cassandra 64 | database: cassandra 65 | enabled: false 66 | description: Cassandra 67 | skipped: Cassandra benchmark not yet implemented 68 | # Dragonfly 69 | - name: dragonfly 70 | database: dragonfly 71 | enabled: true 72 | description: Dragonfly 73 | # Dry 74 | - name: dry 75 | database: dry 76 | enabled: true 77 | description: Dry 78 | # EchoDB 79 | - name: echodb 80 | database: echodb 81 | enabled: true 82 | description: EchoDB 83 | # Fjall 84 | - name: fjall 85 | database: fjall 86 | enabled: true 87 | description: Fjall 88 | # KeyDB 89 | - name: keydb 90 | database: keydb 91 | enabled: true 92 | description: KeyDB 93 | # LMDB 94 | - name: lmdb 95 | database: lmdb 96 | enabled: true 97 | description: LMDB 98 | # Map 99 | - name: map 100 | database: map 101 | enabled: true 102 | description: Map 103 | # MemoDB 104 | - name: memodb 105 | database: memodb 106 | enabled: true 107 | description: MemoDB 108 | # MongoDB 109 | - name: mongodb 110 | database: mongodb 111 | enabled: true 112 | description: MongoDB 113 | # MySQL 114 | - name: mysql 115 | database: mysql 116 | enabled: true 117 | description: MySQL 118 | # Neo4j 119 | - name: neo4j 120 | database: neo4j 121 | enabled: true 122 | description: Neo4j 123 | # Postgres 124 | - name: postgres 125 | database: postgres 126 | enabled: true 127 | description: Postgres 128 | # Redb 129 | - name: redb 130 | database: redb 131 | enabled: false 132 | description: ReDB 133 | skipped: ReDB benchmark skipped due to excessive benchmark time 134 | # Redis 135 | - name: redis 136 | database: redis 137 | enabled: true 138 | description: Redis 139 | # RocksDB 140 | - name: rocksdb 141 | database: rocksdb 142 | enabled: true 143 | description: RocksDB 144 | # Scylladb 145 | - name: scylladb 146 | database: scylladb 147 | enabled: false 148 | description: ScyllaDB 149 | skipped: ScyllaDB benchmark not yet implemented 150 | # SQLite 151 | - name: sqlite 152 | database: sqlite 153 | enabled: true 154 | description: SQLite 155 | # SurrealDB + Memory 156 | - name: surrealdb-memory 157 | database: surrealdb-memory 158 | enabled: true 159 | description: SurrealDB with in-memory storage 160 | # SurrealDB + RocksDB 161 | - name: surrealdb-rocksdb 162 | database: surrealdb-rocksdb 163 | enabled: true 164 | description: SurrealDB with RocksDB storage 165 | DOCKER_PRE_ARGS: -e SURREAL_ROCKSDB_BACKGROUND_FLUSH=true 166 | # SurrealDB + SurrealKV 167 | - name: surrealdb-surrealkv 168 | database: surrealdb-surrealkv 169 | enabled: true 170 | description: SurrealDB with SurrealKV storage 171 | # SurrealDB Memory Engine 172 | - name: surrealdb-embedded-memory 173 | database: surrealdb 174 | enabled: true 175 | endpoint: -e memory 176 | description: SurrealDB embedded with in-memory storage 177 | # SurrealDB RocksDB Engine 178 | - name: surrealdb-embedded-rocksdb 179 | database: surrealdb 180 | enabled: true 181 | endpoint: -e rocksdb:~/crud-bench 182 | description: SurrealDB embedded with RocksDB storage 183 | # SurrealDB SurrealKV Engine 184 | - name: surrealdb-embedded-surrealkv 185 | database: surrealdb 186 | enabled: true 187 | endpoint: -e surrealkv:~/crud-bench 188 | description: SurrealDB embedded with SurrealKV storage 189 | # SurrealKV 190 | - name: surrealkv 191 | database: surrealkv 192 | enabled: true 193 | description: SurrealKV 194 | # SurrealKV Memory 195 | - name: surrealkv-memory 196 | database: surrealkv-memory 197 | enabled: true 198 | description: SurrealKV with in-memory storage 199 | steps: 200 | - name: Download artifacts 201 | uses: actions/download-artifact@v4 202 | with: 203 | path: ${{ github.workspace }}/artifacts 204 | merge-multiple: true 205 | 206 | - name: Set file permissions 207 | run: chmod +x ${{ github.workspace }}/artifacts/crud-bench 208 | 209 | - name: Login to Docker Hub 210 | uses: docker/login-action@v3 211 | with: 212 | username: ${{ secrets.DOCKER_USER }} 213 | password: ${{ secrets.DOCKER_TOKEN }} 214 | 215 | - name: System Information 216 | run: | 217 | echo "=== Environment Variables ===" 218 | env 219 | echo "=== Kernel & OS Info ===" 220 | uname -a 221 | echo "=== CPU Details (lscpu) ===" 222 | lscpu 223 | echo "=== First 50 lines of /proc/cpuinfo ===" 224 | head -n 50 /proc/cpuinfo 225 | echo "=== First 50 lines of /proc/meminfo ===" 226 | head -n 50 /proc/meminfo 227 | echo "=== Cgroup Information (/proc/self/cgroup) ===" 228 | cat /proc/self/cgroup 229 | 230 | - name: ${{ matrix.skipped || 'Benchmark processing' }} 231 | if: ${{ !matrix.enabled }} 232 | run: echo "${{ matrix.skipped }}" 233 | 234 | - name: Clean up environment 235 | if: ${{ matrix.enabled }} 236 | run: | 237 | # Clean up data directory 238 | rm -rf ~/crud-bench 239 | mkdir -p ~/crud-bench 240 | chmod 777 ~/crud-bench 241 | # Remove old results 242 | rm -f result*.json 243 | rm -f result*.csv 244 | 245 | - name: Optimise system 246 | if: ${{ matrix.enabled }} 247 | run: | 248 | # Flush disk writes 249 | sync 250 | # Increase max limits 251 | ulimit -n 65536 252 | ulimit -u unlimited 253 | ulimit -l unlimited 254 | 255 | - name: Run benchmarks (1,000,000 samples / 128 clients / 48 threads / key integer / random) 256 | timeout-minutes: 60 257 | if: ${{ matrix.enabled && (success() || failure()) }} 258 | run: | 259 | ${{ github.workspace }}/artifacts/crud-bench -d ${{ matrix.database }} ${{ matrix.endpoint || '' }} -s 1000000 -c 128 -t 48 -k integer -r -n integer-random 260 | docker container kill crud-bench &>/dev/null || docker container prune --force &>/dev/null || docker volume prune --all --force &>/dev/null || true 261 | env: 262 | CRUD_BENCH_LMDB_DATABASE_SIZE: 1073741824 # 1 GiB 263 | 264 | - name: Run benchmarks (1,000,000 samples / 128 clients / 48 threads / key string26 / random) 265 | timeout-minutes: 60 266 | if: ${{ matrix.enabled && (success() || failure()) }} 267 | run: | 268 | ${{ github.workspace }}/artifacts/crud-bench -d ${{ matrix.database }} ${{ matrix.endpoint || '' }} -s 1000000 -c 128 -t 48 -k string26 -r -n string26-random 269 | docker container kill crud-bench &>/dev/null || docker container prune --force &>/dev/null || docker volume prune --all --force &>/dev/null || true 270 | env: 271 | CRUD_BENCH_LMDB_DATABASE_SIZE: 1073741824 # 1 GiB 272 | 273 | - name: Run benchmarks (5,000,000 samples / 128 clients / 48 threads / key string26 / random / 1.5KiB row and object size) 274 | timeout-minutes: 60 275 | if: ${{ matrix.enabled && (success() || failure()) }} 276 | run: | 277 | ${{ github.workspace }}/artifacts/crud-bench -d ${{ matrix.database }} ${{ matrix.endpoint || '' }} -s 5000000 -c 128 -t 48 -k string26 -r -n string26-random-1k 278 | docker container kill crud-bench &>/dev/null || docker container prune --force &>/dev/null || docker volume prune --all --force &>/dev/null || true 279 | env: 280 | CRUD_BENCH_LMDB_DATABASE_SIZE: 32212254720 # 30 GiB 281 | CRUD_BENCH_VALUE: '{ "text": "text:50", "integer": "int", "nested": { "text": "text:1000", "array": [ "string:50", "string:50", "string:50", "string:50", "string:50" ] } }' 282 | 283 | - name: Wait for system cool down 284 | if: ${{ matrix.enabled && (success() || failure()) }} 285 | run: sleep 5m 286 | 287 | - name: Upload result artifacts 288 | uses: actions/upload-artifact@v4 289 | if: ${{ matrix.enabled && (success() || failure()) }} 290 | with: 291 | name: results ${{ matrix.name }} 292 | path: | 293 | result*.json 294 | result*.csv 295 | 296 | - name: Finish benchmarking 297 | run: echo "Complete" 298 | if: success() || failure() 299 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ----------------------------------- 2 | # OS X 3 | # ----------------------------------- 4 | 5 | # Directory files 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Thumbnail files 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | # Directories potentially created on remote AFP share 18 | .AppleDB 19 | .AppleDesktop 20 | Network Trash Folder 21 | Temporary Items 22 | .apdisk 23 | 24 | # ----------------------------------- 25 | # Files 26 | # ----------------------------------- 27 | 28 | Cargo.lock 29 | **/*.rs.bk 30 | *.db 31 | *.skv 32 | *.sw? 33 | *.skv 34 | 35 | # ----------------------------------- 36 | # Folders 37 | # ----------------------------------- 38 | 39 | /debug/ 40 | /target/ 41 | /lib/cache/ 42 | /lib/store/ 43 | /lib/target/ 44 | /tests/sdk/*/Cargo.lock 45 | /tests/sdk/*/target 46 | 47 | .idea/ 48 | .vscode/ 49 | /result 50 | /bin/ 51 | /.direnv/ 52 | 53 | # --- Ignore generated result files 54 | result.json 55 | result-*.json 56 | result.csv 57 | result-*.csv -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | hard_tabs = true 3 | merge_derives = true 4 | reorder_imports = true 5 | reorder_modules = true 6 | use_field_init_shorthand = true 7 | use_small_heuristics = "Off" 8 | 9 | # ----------------------------------- 10 | # Unstable options we would like to use in future 11 | # ----------------------------------- 12 | 13 | #blank_lines_lower_bound = 1 14 | #group_imports = "One" 15 | #indent_style = "Block" 16 | #match_arm_blocks = true 17 | #reorder_impl_items = true 18 | #wrap_comments = true 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crud-bench" 3 | edition = "2021" 4 | version = "0.1.0" 5 | license = "Apache-2.0" 6 | readme = "README.md" 7 | 8 | [features] 9 | default = [ 10 | "arangodb", 11 | "dragonfly", 12 | "echodb", 13 | "fjall", 14 | "keydb", 15 | "lmdb", 16 | "memodb", 17 | "mongodb", 18 | "mysql", 19 | "neo4j", 20 | "postgres", 21 | "redb", 22 | "redis", 23 | "rocksdb", 24 | "scylladb", 25 | "sqlite", 26 | "surrealkv", 27 | "surrealdb", 28 | ] 29 | arangodb = ["dep:arangors"] 30 | dragonfly = ["dep:redis"] 31 | echodb = ["dep:echodb"] 32 | keydb = ["dep:redis"] 33 | fjall = ["dep:fjall"] 34 | lmdb = ["dep:heed"] 35 | memodb = ["dep:memodb"] 36 | mongodb = ["dep:mongodb"] 37 | mysql = ["dep:mysql_async"] 38 | neo4j = ["dep:neo4rs"] 39 | postgres = ["dep:tokio-postgres"] 40 | redb = ["dep:redb"] 41 | redis = ["dep:redis"] 42 | rocksdb = ["dep:rocksdb"] 43 | scylladb = ["dep:scylla"] 44 | sqlite = ["dep:tokio-rusqlite"] 45 | surrealdb = ["dep:surrealdb", "surrealdb/kv-mem", "surrealdb/kv-rocksdb", "surrealdb/kv-surrealkv", "surrealdb/protocol-http", "surrealdb/protocol-ws", "surrealdb/rustls"] 46 | surrealkv = ["dep:surrealkv"] 47 | 48 | [profile.release] 49 | lto = true 50 | strip = "debuginfo" 51 | opt-level = 3 52 | panic = 'abort' 53 | codegen-units = 1 54 | 55 | [dependencies] 56 | affinitypool = "0.3.1" 57 | anyhow = "1.0.98" 58 | arangors = { version = "0.6.0", optional = true } 59 | bincode = "1.3.3" 60 | bytesize = "2.0.1" 61 | comfy-table = "7.1.4" 62 | chrono = "0.4.40" 63 | clap = { version = "4.5.37", features = ["derive", "string", "env", "color"] } 64 | csv = "1.3.1" 65 | dashmap = "6.1.0" 66 | echodb = { version = "0.8.0", optional = true } 67 | env_logger = "0.11.8" 68 | fjall = { version = "2.9.0", optional = true } 69 | flatten-json-object = "0.6.1" 70 | futures = "0.3.31" 71 | hdrhistogram = "7.5.4" 72 | heed = { version = "0.21.0", optional = true } 73 | log = "0.4.27" 74 | memodb = { version = "0.8.0", optional = true } 75 | mimalloc = "0.1.46" 76 | mongodb = { version = "3.2.3", optional = true } 77 | mysql_async = { version = "0.35.1", default-features = false, features = ["bigdecimal", "binlog", "derive", "frunk", "rust_decimal", "time"], optional = true } 78 | neo4rs = { version = "0.8.0", optional = true } 79 | num_cpus = "1.16.0" 80 | pprof = { version = "0.14.0", features = ["flamegraph", "prost-codec"] } 81 | rand = { version = "0.8.5", features = ["small_rng"] } 82 | redb = { version = "2.4.0", optional = true } 83 | redis = { version = "0.29.5", features = ["tokio-comp"], optional = true } 84 | rocksdb = { version = "0.23.0", features = ["lz4", "snappy"], optional = true } 85 | scylla = { version = "0.15.1", optional = true } 86 | serde = { version = "1.0.219", features = ["derive"] } 87 | serde_json = "1.0.140" 88 | serial_test = "3.2.0" 89 | surrealdb = { version = "3", package = "surrealdb-nightly", default-features = false, optional = true } 90 | surrealkv = { version = "0.9.1", optional = true } 91 | sysinfo = { version = "0.34.2", features = ["serde"] } 92 | tokio = { version = "1.44.2", features = ["macros", "time", "rt-multi-thread"] } 93 | tokio-postgres = { version = "0.7.13", optional = true, features = ["with-serde_json-1", "with-uuid-1"] } 94 | tokio-rusqlite = { version = "0.6.0", optional = true, features = ["bundled"] } 95 | twox-hash = "2.1.0" 96 | uuid = { version = "1.16.0", features = ["v4"] } 97 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cgr.dev/chainguard/glibc-dynamic:latest 2 | 3 | ARG TARGETARCH 4 | COPY artifacts/crud-bench-${TARGETARCH}/crud-bench /crud-bench 5 | 6 | ENTRYPOINT ["/crud-bench"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright © 2021-2025 SurrealDB Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | define CRUD_BENCH_SCANS 2 | [ 3 | { "name": "count_all", "samples": 100, "projection": "COUNT" }, 4 | { "name": "limit_id", "samples": 100, "projection": "ID", "limit": 100, "expect": 100 }, 5 | { "name": "limit_all", "samples": 100, "projection": "FULL", "limit": 100, "expect": 100 }, 6 | { "name": "limit_count", "samples": 100, "projection": "COUNT", "limit": 100, "expect": 100 }, 7 | { "name": "limit_start_id", "samples": 100, "projection": "ID", "start": 5000, "limit": 100, "expect": 100 }, 8 | { "name": "limit_start_all", "samples": 100, "projection": "FULL", "start": 5000, "limit": 100, "expect": 100 }, 9 | { "name": "limit_start_count", "samples": 100, "projection": "COUNT", "start": 5000, "limit": 100, "expect": 100 } 10 | ] 11 | endef 12 | 13 | define CRUD_BENCH_VALUE 14 | { 15 | "text": "text:50", 16 | "integer": "int", 17 | "nested": { 18 | "text": "text:1000", 19 | "array": [ 20 | "string:50", 21 | "string:50", 22 | "string:50", 23 | "string:50", 24 | "string:50" 25 | ] 26 | } 27 | } 28 | endef 29 | 30 | database ?= surrealdb 31 | 32 | .PHONY: default 33 | default: 34 | @echo "Choose a Makefile target:" 35 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print " - " $$1}}' | sort 36 | 37 | .PHONY: build 38 | build: 39 | cargo build -r 40 | 41 | export CRUD_BENCH_SCANS 42 | export CRUD_BENCH_VALUE 43 | .PHONY: dev 44 | dev: 45 | cargo run -- -d $(database) -s 100000 -c 128 -t 48 -k string26 -r 46 | 47 | export CRUD_BENCH_SCANS 48 | export CRUD_BENCH_VALUE 49 | .PHONY: test 50 | test: 51 | target/release/crud-bench -d $(database) -s 5000000 -c 128 -t 48 -k string26 -r 52 | -------------------------------------------------------------------------------- /img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surrealdb/crud-bench/687ca0b921005093ae76a787be75afb99373d66b/img/hero.png -------------------------------------------------------------------------------- /src/allocator.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 3 | -------------------------------------------------------------------------------- /src/arangodb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "arangodb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::{bail, Result}; 9 | use arangors::client::reqwest::ReqwestClient; 10 | use arangors::document::options::InsertOptions; 11 | use arangors::document::options::RemoveOptions; 12 | use arangors::{Collection, Connection, Database, GenericConnection}; 13 | use serde_json::Value; 14 | use std::hint::black_box; 15 | use std::time::Duration; 16 | use tokio::sync::Mutex; 17 | 18 | pub const DEFAULT: &str = "http://127.0.0.1:8529"; 19 | 20 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 21 | DockerParams { 22 | image: "arangodb", 23 | pre_args: "--ulimit nofile=65536:65536 -p 127.0.0.1:8529:8529 -e ARANGO_NO_AUTH=1", 24 | post_args: "--server.scheduler-queue-size 8192 --server.prio1-size 8192 --server.prio2-size 8192 --server.maximal-queue-size 8192", 25 | } 26 | } 27 | 28 | pub(crate) struct ArangoDBClientProvider { 29 | sync: bool, 30 | key: KeyType, 31 | url: String, 32 | } 33 | 34 | impl BenchmarkEngine for ArangoDBClientProvider { 35 | /// Initiates a new datastore benchmarking engine 36 | async fn setup(kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 37 | Ok(Self { 38 | sync: options.sync, 39 | key: kt, 40 | url: options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(), 41 | }) 42 | } 43 | /// Creates a new client for this benchmarking engine 44 | async fn create_client(&self) -> Result { 45 | let (conn, db, co) = create_arango_client(&self.url).await?; 46 | Ok(ArangoDBClient { 47 | sync: self.sync, 48 | keytype: self.key, 49 | connection: conn, 50 | database: Mutex::new(db), 51 | collection: Mutex::new(co), 52 | }) 53 | } 54 | /// The number of seconds to wait before connecting 55 | fn wait_timeout(&self) -> Option { 56 | Some(Duration::from_secs(15)) 57 | } 58 | } 59 | 60 | pub(crate) struct ArangoDBClient { 61 | sync: bool, 62 | keytype: KeyType, 63 | connection: GenericConnection, 64 | database: Mutex>, 65 | collection: Mutex>, 66 | } 67 | 68 | async fn create_arango_client( 69 | url: &str, 70 | ) -> Result<(GenericConnection, Database, Collection)> 71 | { 72 | // Create the connection to the database 73 | let conn = Connection::establish_without_auth(url).await.unwrap(); 74 | // Create the benchmarking database 75 | let db = match conn.create_database("crud-bench").await { 76 | Err(_) => conn.db("crud-bench").await.unwrap(), 77 | Ok(db) => db, 78 | }; 79 | // Create the becnhmark record collection 80 | let co = match db.create_collection("record").await { 81 | Err(_) => db.collection("record").await.unwrap(), 82 | Ok(db) => db, 83 | }; 84 | Ok((conn, db, co)) 85 | } 86 | 87 | impl BenchmarkClient for ArangoDBClient { 88 | async fn startup(&self) -> Result<()> { 89 | // Ensure we drop the database first. 90 | // We can drop the database initially 91 | // because the other clients will be 92 | // created subsequently, and will then 93 | // create the database as necessary. 94 | self.connection.drop_database("crud-bench").await?; 95 | // Everything ok 96 | Ok(()) 97 | } 98 | 99 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 100 | match self.keytype { 101 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 102 | _ => self.create(key.to_string(), val).await, 103 | } 104 | } 105 | 106 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 107 | match self.keytype { 108 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 109 | _ => self.create(key, val).await, 110 | } 111 | } 112 | 113 | async fn read_u32(&self, key: u32) -> Result<()> { 114 | match self.keytype { 115 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 116 | _ => self.read(key.to_string()).await, 117 | } 118 | } 119 | 120 | async fn read_string(&self, key: String) -> Result<()> { 121 | match self.keytype { 122 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 123 | _ => self.read(key).await, 124 | } 125 | } 126 | 127 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 128 | match self.keytype { 129 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 130 | _ => self.update(key.to_string(), val).await, 131 | } 132 | } 133 | 134 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 135 | match self.keytype { 136 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 137 | _ => self.update(key, val).await, 138 | } 139 | } 140 | 141 | async fn delete_u32(&self, key: u32) -> Result<()> { 142 | match self.keytype { 143 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 144 | _ => self.delete(key.to_string()).await, 145 | } 146 | } 147 | 148 | async fn delete_string(&self, key: String) -> Result<()> { 149 | match self.keytype { 150 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 151 | _ => self.delete(key).await, 152 | } 153 | } 154 | 155 | async fn scan_u32(&self, scan: &Scan) -> Result { 156 | match self.keytype { 157 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 158 | _ => self.scan(scan).await, 159 | } 160 | } 161 | 162 | async fn scan_string(&self, scan: &Scan) -> Result { 163 | match self.keytype { 164 | KeyType::String506 => bail!(NOT_SUPPORTED_ERROR), 165 | _ => self.scan(scan).await, 166 | } 167 | } 168 | } 169 | 170 | impl ArangoDBClient { 171 | fn to_doc(key: String, mut val: Value) -> Result { 172 | let obj = val.as_object_mut().unwrap(); 173 | obj.insert("_key".to_string(), key.into()); 174 | Ok(val) 175 | } 176 | 177 | async fn create(&self, key: String, val: Value) -> Result<()> { 178 | let val = Self::to_doc(key, val)?; 179 | let opt = InsertOptions::builder() 180 | .wait_for_sync(self.sync) 181 | .return_new(true) 182 | .overwrite(false) 183 | .build(); 184 | let res = { self.collection.lock().await.create_document(val, opt).await? }; 185 | assert!(res.new_doc().is_some()); 186 | Ok(()) 187 | } 188 | 189 | async fn read(&self, key: String) -> Result<()> { 190 | let doc = { self.collection.lock().await.document::(&key).await? }; 191 | assert!(doc.is_object()); 192 | assert_eq!(doc.get("_key").unwrap().as_str().unwrap(), key); 193 | Ok(()) 194 | } 195 | 196 | async fn update(&self, key: String, val: Value) -> Result<()> { 197 | let val = Self::to_doc(key, val)?; 198 | let opt = InsertOptions::builder() 199 | .wait_for_sync(self.sync) 200 | .return_new(true) 201 | .overwrite(true) 202 | .build(); 203 | let res = { self.collection.lock().await.create_document(val, opt).await? }; 204 | assert!(res.new_doc().is_some()); 205 | Ok(()) 206 | } 207 | 208 | async fn delete(&self, key: String) -> Result<()> { 209 | let opt = RemoveOptions::builder().wait_for_sync(self.sync).build(); 210 | let res = { self.collection.lock().await.remove_document::(&key, opt, None).await? }; 211 | assert!(res.has_response()); 212 | Ok(()) 213 | } 214 | 215 | async fn scan(&self, scan: &Scan) -> Result { 216 | // Contional scans are not supported 217 | if scan.condition.is_some() { 218 | bail!(NOT_SUPPORTED_ERROR); 219 | } 220 | // Extract parameters 221 | let s = scan.start.unwrap_or(0); 222 | let l = scan.limit.unwrap_or(i64::MAX as usize); 223 | let p = scan.projection()?; 224 | // Perform the relevant projection scan type 225 | match p { 226 | Projection::Id => { 227 | let stm = format!("FOR doc IN record LIMIT {s}, {l} RETURN {{ _id: doc._id }}"); 228 | let res: Vec = { self.database.lock().await.aql_str(&stm).await.unwrap() }; 229 | // We use a for loop to iterate over the results, while 230 | // calling black_box internally. This is necessary as 231 | // an iterator with `filter_map` or `map` is optimised 232 | // out by the compiler when calling `count` at the end. 233 | let mut count = 0; 234 | for v in res { 235 | black_box(v); 236 | count += 1; 237 | } 238 | Ok(count) 239 | } 240 | Projection::Full => { 241 | let stm = format!("FOR doc IN record LIMIT {s}, {l} RETURN doc"); 242 | let res: Vec = { self.database.lock().await.aql_str(&stm).await.unwrap() }; 243 | // We use a for loop to iterate over the results, while 244 | // calling black_box internally. This is necessary as 245 | // an iterator with `filter_map` or `map` is optimised 246 | // out by the compiler when calling `count` at the end. 247 | let mut count = 0; 248 | for v in res { 249 | black_box(v); 250 | count += 1; 251 | } 252 | Ok(count) 253 | } 254 | Projection::Count => { 255 | let stm = format!( 256 | "FOR doc IN record LIMIT {s}, {l} COLLECT WITH COUNT INTO count RETURN count" 257 | ); 258 | let res: Vec = { self.database.lock().await.aql_str(&stm).await.unwrap() }; 259 | let count = res.first().unwrap().as_i64().unwrap(); 260 | Ok(count as usize) 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use crate::benchmark::Benchmark; 2 | use crate::dialect::{AnsiSqlDialect, DefaultDialect, MySqlDialect, Neo4jDialect}; 3 | use crate::docker::{Container, DockerParams}; 4 | use crate::dry::DryClientProvider; 5 | use crate::engine::BenchmarkEngine; 6 | use crate::keyprovider::KeyProvider; 7 | use crate::map::MapClientProvider; 8 | use crate::result::BenchmarkResult; 9 | use crate::valueprovider::ValueProvider; 10 | use crate::KeyType; 11 | use anyhow::Result; 12 | use clap::ValueEnum; 13 | 14 | #[derive(ValueEnum, Debug, Clone, Copy)] 15 | pub(crate) enum Database { 16 | Dry, 17 | Map, 18 | #[cfg(feature = "arangodb")] 19 | Arangodb, 20 | #[cfg(feature = "dragonfly")] 21 | Dragonfly, 22 | #[cfg(feature = "echodb")] 23 | Echodb, 24 | #[cfg(feature = "fjall")] 25 | Fjall, 26 | #[cfg(feature = "keydb")] 27 | Keydb, 28 | #[cfg(feature = "lmdb")] 29 | Lmdb, 30 | #[cfg(feature = "memodb")] 31 | Memodb, 32 | #[cfg(feature = "mongodb")] 33 | Mongodb, 34 | #[cfg(feature = "mysql")] 35 | Mysql, 36 | #[cfg(feature = "neo4j")] 37 | Neo4j, 38 | #[cfg(feature = "postgres")] 39 | Postgres, 40 | #[cfg(feature = "redb")] 41 | Redb, 42 | #[cfg(feature = "redis")] 43 | Redis, 44 | #[cfg(feature = "rocksdb")] 45 | Rocksdb, 46 | #[cfg(feature = "scylladb")] 47 | Scylladb, 48 | #[cfg(feature = "sqlite")] 49 | Sqlite, 50 | #[cfg(feature = "surrealkv")] 51 | Surrealkv, 52 | #[cfg(feature = "surrealdb")] 53 | Surrealdb, 54 | #[cfg(feature = "surrealdb")] 55 | SurrealdbMemory, 56 | #[cfg(feature = "surrealdb")] 57 | SurrealdbRocksdb, 58 | #[cfg(feature = "surrealdb")] 59 | SurrealdbSurrealkv, 60 | #[cfg(feature = "surrealkv")] 61 | SurrealkvMemory, 62 | } 63 | 64 | impl Database { 65 | /// Start the Docker container if necessary 66 | pub(crate) fn start_docker(&self, options: &Benchmark) -> Option { 67 | // Get any pre-defined Docker configuration 68 | let params: DockerParams = match self { 69 | #[cfg(feature = "arangodb")] 70 | Self::Arangodb => crate::arangodb::docker(options), 71 | #[cfg(feature = "dragonfly")] 72 | Self::Dragonfly => crate::dragonfly::docker(options), 73 | #[cfg(feature = "keydb")] 74 | Self::Keydb => crate::keydb::docker(options), 75 | #[cfg(feature = "mongodb")] 76 | Self::Mongodb => crate::mongodb::docker(options), 77 | #[cfg(feature = "mysql")] 78 | Self::Mysql => crate::mysql::docker(options), 79 | #[cfg(feature = "mysql")] 80 | Self::Neo4j => crate::neo4j::docker(options), 81 | #[cfg(feature = "postgres")] 82 | Self::Postgres => crate::postgres::docker(options), 83 | #[cfg(feature = "redis")] 84 | Self::Redis => crate::redis::docker(options), 85 | #[cfg(feature = "scylladb")] 86 | Self::Scylladb => crate::scylladb::docker(options), 87 | #[cfg(feature = "surrealdb")] 88 | Self::SurrealdbMemory => crate::surrealdb::docker(options), 89 | #[cfg(feature = "surrealdb")] 90 | Self::SurrealdbRocksdb => crate::surrealdb::docker(options), 91 | #[cfg(feature = "surrealdb")] 92 | Self::SurrealdbSurrealkv => crate::surrealdb::docker(options), 93 | #[allow(unreachable_patterns)] 94 | _ => return None, 95 | }; 96 | // Check if a custom image has been specified 97 | let image = options.image.clone().unwrap_or(params.image.to_string()); 98 | // Start the specified container with arguments 99 | let container = Container::start(image, params.pre_args, params.post_args, options); 100 | // Return the container reference 101 | Some(container) 102 | } 103 | 104 | /// Run the benchmarks for the chosen database 105 | pub(crate) async fn run( 106 | &self, 107 | benchmark: &mut Benchmark, 108 | kt: KeyType, 109 | kp: KeyProvider, 110 | vp: ValueProvider, 111 | scans: &str, 112 | ) -> Result { 113 | let scans = serde_json::from_str(scans)?; 114 | match self { 115 | Database::Dry => { 116 | benchmark 117 | .run::<_, DefaultDialect, _>( 118 | DryClientProvider::setup(kt, vp.columns(), benchmark).await?, 119 | kp, 120 | vp, 121 | scans, 122 | ) 123 | .await 124 | } 125 | #[cfg(feature = "arangodb")] 126 | Database::Arangodb => { 127 | benchmark 128 | .run::<_, DefaultDialect, _>( 129 | crate::arangodb::ArangoDBClientProvider::setup(kt, vp.columns(), benchmark) 130 | .await?, 131 | kp, 132 | vp, 133 | scans, 134 | ) 135 | .await 136 | } 137 | #[cfg(feature = "dragonfly")] 138 | Database::Dragonfly => { 139 | benchmark 140 | .run::<_, DefaultDialect, _>( 141 | crate::dragonfly::DragonflyClientProvider::setup( 142 | kt, 143 | vp.columns(), 144 | benchmark, 145 | ) 146 | .await?, 147 | kp, 148 | vp, 149 | scans, 150 | ) 151 | .await 152 | } 153 | #[cfg(feature = "echodb")] 154 | Database::Echodb => { 155 | benchmark 156 | .run::<_, DefaultDialect, _>( 157 | crate::echodb::EchoDBClientProvider::setup(kt, vp.columns(), benchmark) 158 | .await?, 159 | kp, 160 | vp, 161 | scans, 162 | ) 163 | .await 164 | } 165 | #[cfg(feature = "fjall")] 166 | Database::Fjall => { 167 | benchmark 168 | .run::<_, DefaultDialect, _>( 169 | crate::fjall::FjallClientProvider::setup(kt, vp.columns(), benchmark) 170 | .await?, 171 | kp, 172 | vp, 173 | scans, 174 | ) 175 | .await 176 | } 177 | #[cfg(feature = "keydb")] 178 | Database::Keydb => { 179 | benchmark 180 | .run::<_, DefaultDialect, _>( 181 | crate::keydb::KeydbClientProvider::setup(kt, vp.columns(), benchmark) 182 | .await?, 183 | kp, 184 | vp, 185 | scans, 186 | ) 187 | .await 188 | } 189 | #[cfg(feature = "lmdb")] 190 | Database::Lmdb => { 191 | benchmark 192 | .run::<_, DefaultDialect, _>( 193 | crate::lmdb::LmDBClientProvider::setup(kt, vp.columns(), benchmark).await?, 194 | kp, 195 | vp, 196 | scans, 197 | ) 198 | .await 199 | } 200 | Database::Map => { 201 | benchmark 202 | .run::<_, DefaultDialect, _>( 203 | MapClientProvider::setup(kt, vp.columns(), benchmark).await?, 204 | kp, 205 | vp, 206 | scans, 207 | ) 208 | .await 209 | } 210 | #[cfg(feature = "memodb")] 211 | Database::Memodb => { 212 | benchmark 213 | .run::<_, DefaultDialect, _>( 214 | crate::memodb::MemoDBClientProvider::setup(kt, vp.columns(), benchmark) 215 | .await?, 216 | kp, 217 | vp, 218 | scans, 219 | ) 220 | .await 221 | } 222 | #[cfg(feature = "mongodb")] 223 | Database::Mongodb => { 224 | benchmark 225 | .run::<_, DefaultDialect, _>( 226 | crate::mongodb::MongoDBClientProvider::setup(kt, vp.columns(), benchmark) 227 | .await?, 228 | kp, 229 | vp, 230 | scans, 231 | ) 232 | .await 233 | } 234 | #[cfg(feature = "mysql")] 235 | Database::Mysql => { 236 | benchmark 237 | .run::<_, MySqlDialect, _>( 238 | crate::mysql::MysqlClientProvider::setup(kt, vp.columns(), benchmark) 239 | .await?, 240 | kp, 241 | vp, 242 | scans, 243 | ) 244 | .await 245 | } 246 | #[cfg(feature = "neo4j")] 247 | Database::Neo4j => { 248 | benchmark 249 | .run::<_, Neo4jDialect, _>( 250 | crate::neo4j::Neo4jClientProvider::setup(kt, vp.columns(), benchmark) 251 | .await?, 252 | kp, 253 | vp, 254 | scans, 255 | ) 256 | .await 257 | } 258 | #[cfg(feature = "postgres")] 259 | Database::Postgres => { 260 | benchmark 261 | .run::<_, AnsiSqlDialect, _>( 262 | crate::postgres::PostgresClientProvider::setup(kt, vp.columns(), benchmark) 263 | .await?, 264 | kp, 265 | vp, 266 | scans, 267 | ) 268 | .await 269 | } 270 | #[cfg(feature = "redb")] 271 | Database::Redb => { 272 | benchmark 273 | .run::<_, DefaultDialect, _>( 274 | crate::redb::ReDBClientProvider::setup(kt, vp.columns(), benchmark).await?, 275 | kp, 276 | vp, 277 | scans, 278 | ) 279 | .await 280 | } 281 | #[cfg(feature = "redis")] 282 | Database::Redis => { 283 | benchmark 284 | .run::<_, DefaultDialect, _>( 285 | crate::redis::RedisClientProvider::setup(kt, vp.columns(), benchmark) 286 | .await?, 287 | kp, 288 | vp, 289 | scans, 290 | ) 291 | .await 292 | } 293 | #[cfg(feature = "rocksdb")] 294 | Database::Rocksdb => { 295 | benchmark 296 | .run::<_, DefaultDialect, _>( 297 | crate::rocksdb::RocksDBClientProvider::setup(kt, vp.columns(), benchmark) 298 | .await?, 299 | kp, 300 | vp, 301 | scans, 302 | ) 303 | .await 304 | } 305 | #[cfg(feature = "scylladb")] 306 | Database::Scylladb => { 307 | benchmark 308 | .run::<_, AnsiSqlDialect, _>( 309 | crate::scylladb::ScyllaDBClientProvider::setup(kt, vp.columns(), benchmark) 310 | .await?, 311 | kp, 312 | vp, 313 | scans, 314 | ) 315 | .await 316 | } 317 | #[cfg(feature = "sqlite")] 318 | Database::Sqlite => { 319 | benchmark 320 | .run::<_, DefaultDialect, _>( 321 | crate::sqlite::SqliteClientProvider::setup(kt, vp.columns(), benchmark) 322 | .await?, 323 | kp, 324 | vp, 325 | scans, 326 | ) 327 | .await 328 | } 329 | #[cfg(feature = "surrealdb")] 330 | Database::Surrealdb => { 331 | benchmark 332 | .run::<_, DefaultDialect, _>( 333 | crate::surrealdb::SurrealDBClientProvider::setup( 334 | kt, 335 | vp.columns(), 336 | benchmark, 337 | ) 338 | .await?, 339 | kp, 340 | vp, 341 | scans, 342 | ) 343 | .await 344 | } 345 | #[cfg(feature = "surrealdb")] 346 | Database::SurrealdbMemory => { 347 | benchmark 348 | .run::<_, DefaultDialect, _>( 349 | crate::surrealdb::SurrealDBClientProvider::setup( 350 | kt, 351 | vp.columns(), 352 | benchmark, 353 | ) 354 | .await?, 355 | kp, 356 | vp, 357 | scans, 358 | ) 359 | .await 360 | } 361 | #[cfg(feature = "surrealdb")] 362 | Database::SurrealdbRocksdb => { 363 | benchmark 364 | .run::<_, DefaultDialect, _>( 365 | crate::surrealdb::SurrealDBClientProvider::setup( 366 | kt, 367 | vp.columns(), 368 | benchmark, 369 | ) 370 | .await?, 371 | kp, 372 | vp, 373 | scans, 374 | ) 375 | .await 376 | } 377 | #[cfg(feature = "surrealdb")] 378 | Database::SurrealdbSurrealkv => { 379 | benchmark 380 | .run::<_, DefaultDialect, _>( 381 | crate::surrealdb::SurrealDBClientProvider::setup( 382 | kt, 383 | vp.columns(), 384 | benchmark, 385 | ) 386 | .await?, 387 | kp, 388 | vp, 389 | scans, 390 | ) 391 | .await 392 | } 393 | #[cfg(feature = "surrealkv")] 394 | Database::Surrealkv => { 395 | benchmark 396 | .run::<_, DefaultDialect, _>( 397 | crate::surrealkv::SurrealKVClientProvider::setup( 398 | kt, 399 | vp.columns(), 400 | benchmark, 401 | ) 402 | .await?, 403 | kp, 404 | vp, 405 | scans, 406 | ) 407 | .await 408 | } 409 | #[cfg(feature = "surrealkv")] 410 | Database::SurrealkvMemory => { 411 | benchmark.disk_persistence = false; 412 | benchmark 413 | .run::<_, DefaultDialect, _>( 414 | crate::surrealkv::SurrealKVClientProvider::setup( 415 | kt, 416 | vp.columns(), 417 | benchmark, 418 | ) 419 | .await?, 420 | kp, 421 | vp, 422 | scans, 423 | ) 424 | .await 425 | } 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/dialect.rs: -------------------------------------------------------------------------------- 1 | use crate::valueprovider::Columns; 2 | use anyhow::Result; 3 | use chrono::{DateTime, TimeZone, Utc}; 4 | use flatten_json_object::ArrayFormatting; 5 | use flatten_json_object::Flattener; 6 | use serde_json::Value; 7 | use uuid::Uuid; 8 | 9 | /// Help converting generated values to the right database representation 10 | pub(crate) trait Dialect { 11 | fn uuid(u: Uuid) -> Value { 12 | Value::String(u.to_string()) 13 | } 14 | fn date_time(secs_from_epoch: i64) -> Value { 15 | // Get the current UTC time 16 | let datetime: DateTime = Utc.timestamp_opt(secs_from_epoch, 0).unwrap(); 17 | // Format it to the SQL-friendly ISO 8601 format 18 | let formatted = datetime.to_rfc3339(); 19 | Value::String(formatted) 20 | } 21 | fn escape_field(field: String) -> String { 22 | field 23 | } 24 | fn arg_string(val: Value) -> String { 25 | val.to_string() 26 | } 27 | } 28 | 29 | // 30 | // 31 | // 32 | 33 | pub(crate) struct DefaultDialect(); 34 | 35 | impl Dialect for DefaultDialect {} 36 | 37 | // 38 | // 39 | // 40 | 41 | pub(crate) struct AnsiSqlDialect(); 42 | 43 | impl Dialect for AnsiSqlDialect { 44 | fn escape_field(field: String) -> String { 45 | format!("\"{field}\"") 46 | } 47 | 48 | fn arg_string(val: Value) -> String { 49 | match val { 50 | Value::Null => "null".to_string(), 51 | Value::Bool(b) => b.to_string(), 52 | Value::Number(n) => n.to_string(), 53 | Value::String(s) => format!("'{s}'"), 54 | Value::Array(a) => serde_json::to_string(&a).unwrap(), 55 | Value::Object(o) => format!("'{}'", serde_json::to_string(&o).unwrap()), 56 | } 57 | } 58 | } 59 | 60 | impl AnsiSqlDialect { 61 | /// Constructs the field clauses for the [C]reate tests 62 | pub fn create_clause(cols: &Columns, val: Value) -> (String, String) { 63 | let mut fields = Vec::with_capacity(cols.0.len()); 64 | let mut values = Vec::with_capacity(cols.0.len()); 65 | if let Value::Object(map) = val { 66 | for (f, v) in map { 67 | fields.push(Self::escape_field(f)); 68 | values.push(Self::arg_string(v)); 69 | } 70 | } 71 | let fields = fields.join(", "); 72 | let values = values.join(", "); 73 | (fields, values) 74 | } 75 | /// Constructs the field clauses for the [U]pdate tests 76 | pub fn update_clause(cols: &Columns, val: Value) -> String { 77 | let mut updates = Vec::with_capacity(cols.0.len()); 78 | if let Value::Object(map) = val { 79 | for (f, v) in map { 80 | let f = Self::escape_field(f); 81 | let v = Self::arg_string(v); 82 | updates.push(format!("{f} = {v}")); 83 | } 84 | } 85 | updates.join(", ") 86 | } 87 | } 88 | 89 | // 90 | // 91 | // 92 | 93 | pub(crate) struct MySqlDialect(); 94 | 95 | impl Dialect for MySqlDialect { 96 | fn escape_field(field: String) -> String { 97 | format!("`{field}`") 98 | } 99 | 100 | fn arg_string(val: Value) -> String { 101 | match val { 102 | Value::Null => "null".to_string(), 103 | Value::Bool(b) => b.to_string(), 104 | Value::Number(n) => n.to_string(), 105 | Value::String(s) => format!("'{s}'"), 106 | Value::Array(a) => serde_json::to_string(&a).unwrap(), 107 | Value::Object(o) => format!("'{}'", serde_json::to_string(&o).unwrap()), 108 | } 109 | } 110 | } 111 | 112 | impl MySqlDialect { 113 | /// Constructs the field clauses for the [C]reate tests 114 | pub fn create_clause(cols: &Columns, val: Value) -> (String, String) { 115 | let mut fields = Vec::with_capacity(cols.0.len()); 116 | let mut values = Vec::with_capacity(cols.0.len()); 117 | if let Value::Object(map) = val { 118 | for (f, v) in map { 119 | fields.push(Self::escape_field(f)); 120 | values.push(Self::arg_string(v)); 121 | } 122 | } 123 | let fields = fields.join(", "); 124 | let values = values.join(", "); 125 | (fields, values) 126 | } 127 | /// Constructs the field clauses for the [U]pdate tests 128 | pub fn update_clause(cols: &Columns, val: Value) -> String { 129 | let mut updates = Vec::with_capacity(cols.0.len()); 130 | if let Value::Object(map) = val { 131 | for (f, v) in map { 132 | let f = Self::escape_field(f); 133 | let v = Self::arg_string(v); 134 | updates.push(format!("{f} = {v}")); 135 | } 136 | } 137 | updates.join(", ") 138 | } 139 | } 140 | 141 | // 142 | // 143 | // 144 | 145 | pub(crate) struct Neo4jDialect(); 146 | 147 | impl Dialect for Neo4jDialect {} 148 | 149 | impl Neo4jDialect { 150 | /// Constructs the field clauses for the [C]reate tests 151 | pub fn create_clause(val: Value) -> Result { 152 | let val = Flattener::new() 153 | .set_key_separator("_") 154 | .set_array_formatting(ArrayFormatting::Surrounded { 155 | start: "_".to_string(), 156 | end: "".to_string(), 157 | }) 158 | .set_preserve_empty_arrays(false) 159 | .set_preserve_empty_objects(false) 160 | .flatten(&val)?; 161 | let obj = val.as_object().unwrap(); 162 | let mut fields = Vec::with_capacity(obj.len()); 163 | if let Value::Object(map) = val { 164 | for (f, v) in map { 165 | let f = Self::escape_field(f); 166 | let v = Self::arg_string(v); 167 | fields.push(format!("{f}: {v}")); 168 | } 169 | } 170 | Ok(fields.join(", ")) 171 | } 172 | /// Constructs the field clauses for the [U]pdate tests 173 | pub fn update_clause(val: Value) -> Result { 174 | let val = Flattener::new() 175 | .set_key_separator("_") 176 | .set_array_formatting(ArrayFormatting::Surrounded { 177 | start: "_".to_string(), 178 | end: "".to_string(), 179 | }) 180 | .set_preserve_empty_arrays(false) 181 | .set_preserve_empty_objects(false) 182 | .flatten(&val)?; 183 | let obj = val.as_object().unwrap(); 184 | let mut fields = Vec::with_capacity(obj.len()); 185 | if let Value::Object(map) = val { 186 | for (f, v) in map { 187 | let f = Self::escape_field(f); 188 | let v = Self::arg_string(v); 189 | fields.push(format!("r.{f} = {v}")); 190 | } 191 | } 192 | Ok(fields.join(", ")) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/docker.rs: -------------------------------------------------------------------------------- 1 | use crate::benchmark::Benchmark; 2 | use log::{debug, error, info}; 3 | use std::fmt; 4 | use std::process::{exit, Command}; 5 | use std::time::Duration; 6 | 7 | const RETRIES: i32 = 10; 8 | 9 | const TIMEOUT: Duration = Duration::from_secs(6); 10 | 11 | pub(crate) struct DockerParams { 12 | pub(crate) image: &'static str, 13 | pub(crate) pre_args: &'static str, 14 | pub(crate) post_args: &'static str, 15 | } 16 | 17 | pub(crate) struct Container { 18 | image: String, 19 | } 20 | 21 | impl Drop for Container { 22 | fn drop(&mut self) { 23 | let _ = Self::stop(); 24 | } 25 | } 26 | 27 | impl Container { 28 | /// Get the name of the Docker image 29 | pub(crate) fn image(&self) -> &str { 30 | &self.image 31 | } 32 | 33 | /// Start the Docker container 34 | pub(crate) fn start(image: String, pre: &str, post: &str, options: &Benchmark) -> Self { 35 | // Output debug information to the logs 36 | info!("Starting Docker image '{image}'"); 37 | // Attempt to start Docker 10 times 38 | for i in 1..=RETRIES { 39 | // Configure the Docker command arguments 40 | let mut args = Arguments::new(["run"]); 41 | // Configure the default pre arguments 42 | args.append(pre); 43 | // Configure any custom pre arguments 44 | if let Ok(v) = std::env::var("DOCKER_PRE_ARGS") { 45 | args.append(&v); 46 | } 47 | // Run in privileged mode if specified 48 | if options.privileged { 49 | args.add(["--privileged"]); 50 | } 51 | // Configure the Docker container options 52 | args.add(["--rm"]); 53 | args.add(["--quiet"]); 54 | args.add(["--pull", "always"]); 55 | args.add(["--name", "crud-bench"]); 56 | args.add(["--net", "host"]); 57 | args.add(["-d", &image]); 58 | // Configure the default post arguments 59 | args.append(post); 60 | // Configure any custom post arguments 61 | if let Ok(v) = std::env::var("DOCKER_POST_ARGS") { 62 | args.append(&v); 63 | } 64 | // Execute the Docker run command 65 | match Self::execute(args.clone()) { 66 | // The command executed successfully 67 | Ok(_) => break, 68 | // There was an error with the command 69 | Err(e) => match i { 70 | // This is the last attempt so exit fully 71 | RETRIES => { 72 | error!("Docker command failure: `docker {args}`"); 73 | error!("{e}"); 74 | exit(1); 75 | } 76 | // Let's log the output and retry the command 77 | _ => { 78 | debug!("Docker command failure: `docker {args}`"); 79 | debug!("{e}"); 80 | std::thread::sleep(TIMEOUT); 81 | } 82 | }, 83 | } 84 | } 85 | // Return the container name 86 | Self { 87 | image, 88 | } 89 | } 90 | 91 | /// Stop the Docker container 92 | pub(crate) fn stop() -> Result { 93 | info!("Stopping Docker container 'crud-bench'"); 94 | let args = ["container", "stop", "--time", "300", "crud-bench"]; 95 | Self::execute(Arguments::new(args)) 96 | } 97 | 98 | /// Output the container logs 99 | pub(crate) fn logs() -> Result { 100 | info!("Logging Docker container 'crud-bench'"); 101 | let args = ["container", "logs", "crud-bench"]; 102 | Self::execute(Arguments::new(args)) 103 | } 104 | 105 | fn execute(args: Arguments) -> Result { 106 | // Output debug information to the logs 107 | println!("Running command: `docker {args}`"); 108 | // Create a new process command 109 | let mut command = Command::new("docker"); 110 | // Set the arguments on the command 111 | let command = command.args(args.0.clone()); 112 | // Catch all output from the command 113 | let output = command.output().expect("Failed to execute process"); 114 | // Output command failure if errored 115 | match output.status.success() { 116 | // Get the stderr out from the command 117 | false => Err(String::from_utf8(output.stderr).unwrap().trim().to_string()), 118 | // Get the stdout out from the command 119 | true => Ok(String::from_utf8(output.stdout).unwrap().trim().to_string()), 120 | } 121 | } 122 | } 123 | 124 | #[derive(Clone)] 125 | pub(crate) struct Arguments(Vec); 126 | 127 | impl fmt::Display for Arguments { 128 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 129 | write!(f, "{}", self.0.join(" ")) 130 | } 131 | } 132 | 133 | impl Arguments { 134 | fn new(args: I) -> Self 135 | where 136 | I: IntoIterator, 137 | S: Into, 138 | { 139 | let mut a = Self(vec![]); 140 | a.add(args); 141 | a 142 | } 143 | 144 | fn add(&mut self, args: I) 145 | where 146 | I: IntoIterator, 147 | S: Into, 148 | { 149 | for arg in args { 150 | self.0.push(arg.into()); 151 | } 152 | } 153 | 154 | fn append(&mut self, args: &str) { 155 | let split: Vec<&str> = args.split(' ').filter(|a| !a.is_empty()).collect(); 156 | self.add(split); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/dragonfly.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "dragonfly")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::{bail, Result}; 9 | use futures::StreamExt; 10 | use redis::aio::MultiplexedConnection; 11 | use redis::{AsyncCommands, Client, ScanOptions}; 12 | use serde_json::Value; 13 | use std::hint::black_box; 14 | use tokio::sync::Mutex; 15 | 16 | pub const DEFAULT: &str = "redis://:root@127.0.0.1:6379/"; 17 | 18 | pub(crate) const fn docker(_: &Benchmark) -> DockerParams { 19 | DockerParams { 20 | image: "docker.dragonflydb.io/dragonflydb/dragonfly", 21 | pre_args: "-p 127.0.0.1:6379:6379 --ulimit memlock=-1", 22 | post_args: "--requirepass root", 23 | } 24 | } 25 | 26 | pub(crate) struct DragonflyClientProvider { 27 | url: String, 28 | } 29 | 30 | impl BenchmarkEngine for DragonflyClientProvider { 31 | /// Initiates a new datastore benchmarking engine 32 | async fn setup(_kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 33 | Ok(Self { 34 | url: options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(), 35 | }) 36 | } 37 | /// Creates a new client for this benchmarking engine 38 | async fn create_client(&self) -> Result { 39 | let client = Client::open(self.url.as_str())?; 40 | let conn_record = Mutex::new(client.get_multiplexed_async_connection().await?); 41 | let conn_iter = Mutex::new(client.get_multiplexed_async_connection().await?); 42 | Ok(DragonflyClient { 43 | conn_record, 44 | conn_iter, 45 | }) 46 | } 47 | } 48 | 49 | pub(crate) struct DragonflyClient { 50 | conn_iter: Mutex, 51 | conn_record: Mutex, 52 | } 53 | 54 | impl BenchmarkClient for DragonflyClient { 55 | #[allow(dependency_on_unit_never_type_fallback)] 56 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 57 | let val = bincode::serialize(&val)?; 58 | self.conn_record.lock().await.set(key, val).await?; 59 | Ok(()) 60 | } 61 | 62 | #[allow(dependency_on_unit_never_type_fallback)] 63 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 64 | let val = bincode::serialize(&val)?; 65 | self.conn_record.lock().await.set(key, val).await?; 66 | Ok(()) 67 | } 68 | 69 | #[allow(dependency_on_unit_never_type_fallback)] 70 | async fn read_u32(&self, key: u32) -> Result<()> { 71 | let val: Vec = self.conn_record.lock().await.get(key).await?; 72 | assert!(!val.is_empty()); 73 | black_box(val); 74 | Ok(()) 75 | } 76 | 77 | #[allow(dependency_on_unit_never_type_fallback)] 78 | async fn read_string(&self, key: String) -> Result<()> { 79 | let val: Vec = self.conn_record.lock().await.get(key).await?; 80 | assert!(!val.is_empty()); 81 | black_box(val); 82 | Ok(()) 83 | } 84 | 85 | #[allow(dependency_on_unit_never_type_fallback)] 86 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 87 | let val = bincode::serialize(&val)?; 88 | self.conn_record.lock().await.set(key, val).await?; 89 | Ok(()) 90 | } 91 | 92 | #[allow(dependency_on_unit_never_type_fallback)] 93 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 94 | let val = bincode::serialize(&val)?; 95 | self.conn_record.lock().await.set(key, val).await?; 96 | Ok(()) 97 | } 98 | 99 | #[allow(dependency_on_unit_never_type_fallback)] 100 | async fn delete_u32(&self, key: u32) -> Result<()> { 101 | self.conn_record.lock().await.del(key).await?; 102 | Ok(()) 103 | } 104 | 105 | #[allow(dependency_on_unit_never_type_fallback)] 106 | async fn delete_string(&self, key: String) -> Result<()> { 107 | self.conn_record.lock().await.del(key).await?; 108 | Ok(()) 109 | } 110 | 111 | async fn scan_u32(&self, scan: &Scan) -> Result { 112 | self.scan_bytes(scan).await 113 | } 114 | 115 | async fn scan_string(&self, scan: &Scan) -> Result { 116 | self.scan_bytes(scan).await 117 | } 118 | } 119 | 120 | impl DragonflyClient { 121 | async fn scan_bytes(&self, scan: &Scan) -> Result { 122 | // Conditional scans are not supported 123 | if scan.condition.is_some() { 124 | bail!(NOT_SUPPORTED_ERROR); 125 | } 126 | // Extract parameters 127 | let s = scan.start.unwrap_or(0); 128 | let l = scan.limit.unwrap_or(usize::MAX); 129 | let p = scan.projection()?; 130 | // Get the two connection types 131 | let mut conn_iter = self.conn_iter.lock().await; 132 | let mut conn_record = self.conn_record.lock().await; 133 | // Configure the scan options for improve iteration 134 | let opts = ScanOptions::default().with_count(5000); 135 | // Create an iterator starting at the beginning 136 | let mut iter = conn_iter.scan_options::(opts).await?.skip(s); 137 | // Perform the relevant projection scan type 138 | match p { 139 | Projection::Id => { 140 | // We use a for loop to iterate over the results, while 141 | // calling black_box internally. This is necessary as 142 | // an iterator with `filter_map` or `map` is optimised 143 | // out by the compiler when calling `count` at the end. 144 | let mut count = 0; 145 | for _ in 0..l { 146 | if let Some(k) = iter.next().await { 147 | black_box(k); 148 | count += 1; 149 | } else { 150 | break; 151 | } 152 | } 153 | Ok(count) 154 | } 155 | Projection::Full => { 156 | // We use a for loop to iterate over the results, while 157 | // calling black_box internally. This is necessary as 158 | // an iterator with `filter_map` or `map` is optimised 159 | // out by the compiler when calling `count` at the end. 160 | let mut count = 0; 161 | while let Some(k) = iter.next().await { 162 | let v: Vec = conn_record.get(k).await?; 163 | black_box(v); 164 | count += 1; 165 | if count >= l { 166 | break; 167 | } 168 | } 169 | Ok(count) 170 | } 171 | Projection::Count => match scan.limit { 172 | // Full count queries are too slow 173 | None => bail!(NOT_SUPPORTED_ERROR), 174 | Some(l) => Ok(iter.take(l).count().await), 175 | }, 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/dry.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 2 | use crate::valueprovider::Columns; 3 | use crate::{Benchmark, KeyType, Scan}; 4 | use anyhow::Result; 5 | use serde_json::Value; 6 | use std::hint::black_box; 7 | use std::time::Duration; 8 | 9 | pub(crate) struct DryClientProvider {} 10 | 11 | impl BenchmarkEngine for DryClientProvider { 12 | /// The number of seconds to wait before connecting 13 | fn wait_timeout(&self) -> Option { 14 | None 15 | } 16 | /// Initiates a new datastore benchmarking engine 17 | async fn setup(_kt: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 18 | Ok(Self {}) 19 | } 20 | /// Creates a new client for this benchmarking engine 21 | async fn create_client(&self) -> Result { 22 | Ok(DryClient {}) 23 | } 24 | } 25 | 26 | pub(crate) struct DryClient {} 27 | 28 | impl BenchmarkClient for DryClient { 29 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 30 | black_box((key, val)); 31 | Ok(()) 32 | } 33 | 34 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 35 | black_box((key, val)); 36 | Ok(()) 37 | } 38 | 39 | async fn read_u32(&self, key: u32) -> Result<()> { 40 | black_box(key); 41 | Ok(()) 42 | } 43 | 44 | async fn read_string(&self, key: String) -> Result<()> { 45 | black_box(key); 46 | Ok(()) 47 | } 48 | 49 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 50 | black_box((key, val)); 51 | Ok(()) 52 | } 53 | 54 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 55 | black_box((key, val)); 56 | Ok(()) 57 | } 58 | 59 | async fn delete_u32(&self, key: u32) -> Result<()> { 60 | black_box(key); 61 | Ok(()) 62 | } 63 | 64 | async fn delete_string(&self, key: String) -> Result<()> { 65 | black_box(key); 66 | Ok(()) 67 | } 68 | 69 | async fn scan_u32(&self, scan: &Scan) -> Result { 70 | black_box(scan); 71 | Ok(scan.expect.unwrap_or(0)) 72 | } 73 | 74 | async fn scan_string(&self, scan: &Scan) -> Result { 75 | black_box(scan); 76 | Ok(scan.expect.unwrap_or(0)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/echodb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "echodb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use echodb::{new, Database}; 9 | use serde_json::Value; 10 | use std::hint::black_box; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | 14 | type Key = Vec; 15 | type Val = Vec; 16 | 17 | pub(crate) struct EchoDBClientProvider(Arc>); 18 | 19 | impl BenchmarkEngine for EchoDBClientProvider { 20 | /// The number of seconds to wait before connecting 21 | fn wait_timeout(&self) -> Option { 22 | None 23 | } 24 | /// Initiates a new datastore benchmarking engine 25 | async fn setup(_: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 26 | // Create the store 27 | Ok(Self(Arc::new(new()))) 28 | } 29 | /// Creates a new client for this benchmarking engine 30 | async fn create_client(&self) -> Result { 31 | Ok(EchoDBClient { 32 | db: self.0.clone(), 33 | }) 34 | } 35 | } 36 | 37 | pub(crate) struct EchoDBClient { 38 | db: Arc>, 39 | } 40 | 41 | impl BenchmarkClient for EchoDBClient { 42 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 43 | self.create_bytes(key.to_ne_bytes().to_vec(), val).await 44 | } 45 | 46 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 47 | self.create_bytes(key.into_bytes(), val).await 48 | } 49 | 50 | async fn read_u32(&self, key: u32) -> Result<()> { 51 | self.read_bytes(key.to_ne_bytes().to_vec()).await 52 | } 53 | 54 | async fn read_string(&self, key: String) -> Result<()> { 55 | self.read_bytes(key.into_bytes()).await 56 | } 57 | 58 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 59 | self.update_bytes(key.to_ne_bytes().to_vec(), val).await 60 | } 61 | 62 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 63 | self.update_bytes(key.into_bytes(), val).await 64 | } 65 | 66 | async fn delete_u32(&self, key: u32) -> Result<()> { 67 | self.delete_bytes(key.to_ne_bytes().to_vec()).await 68 | } 69 | 70 | async fn delete_string(&self, key: String) -> Result<()> { 71 | self.delete_bytes(key.into_bytes()).await 72 | } 73 | 74 | async fn scan_u32(&self, scan: &Scan) -> Result { 75 | self.scan_bytes(scan).await 76 | } 77 | 78 | async fn scan_string(&self, scan: &Scan) -> Result { 79 | self.scan_bytes(scan).await 80 | } 81 | } 82 | 83 | impl EchoDBClient { 84 | async fn create_bytes(&self, key: Vec, val: Value) -> Result<()> { 85 | // Serialise the value 86 | let val = bincode::serialize(&val)?; 87 | // Create a new transaction 88 | let mut txn = self.db.begin(true).await; 89 | // Process the data 90 | txn.set(key, val)?; 91 | txn.commit()?; 92 | Ok(()) 93 | } 94 | 95 | async fn read_bytes(&self, key: Vec) -> Result<()> { 96 | // Create a new transaction 97 | let txn = self.db.begin(false).await; 98 | // Process the data 99 | let res = txn.get(key)?; 100 | // Check the value exists 101 | assert!(res.is_some()); 102 | // Deserialise the value 103 | black_box(res.unwrap()); 104 | // All ok 105 | Ok(()) 106 | } 107 | 108 | async fn update_bytes(&self, key: Vec, val: Value) -> Result<()> { 109 | // Serialise the value 110 | let val = bincode::serialize(&val)?; 111 | // Create a new transaction 112 | let mut txn = self.db.begin(true).await; 113 | // Process the data 114 | txn.set(key, val)?; 115 | txn.commit()?; 116 | Ok(()) 117 | } 118 | 119 | async fn delete_bytes(&self, key: Vec) -> Result<()> { 120 | // Create a new transaction 121 | let mut txn = self.db.begin(true).await; 122 | // Process the data 123 | txn.del(key)?; 124 | txn.commit()?; 125 | Ok(()) 126 | } 127 | 128 | async fn scan_bytes(&self, scan: &Scan) -> Result { 129 | // Contional scans are not supported 130 | if scan.condition.is_some() { 131 | bail!(NOT_SUPPORTED_ERROR); 132 | } 133 | // Extract parameters 134 | let s = scan.start.unwrap_or(0); 135 | let l = scan.limit.unwrap_or(usize::MAX); 136 | let p = scan.projection()?; 137 | // Create a new transaction 138 | let txn = self.db.begin(false).await; 139 | let beg = [0u8].to_vec(); 140 | let end = [255u8].to_vec(); 141 | // Perform the relevant projection scan type 142 | match p { 143 | Projection::Id => { 144 | // Scan the desired range of keys 145 | let iter = txn.keys(beg..end, s + l)?; 146 | // Create an iterator starting at the beginning 147 | let iter = iter.into_iter(); 148 | // We use a for loop to iterate over the results, while 149 | // calling black_box internally. This is necessary as 150 | // an iterator with `filter_map` or `map` is optimised 151 | // out by the compiler when calling `count` at the end. 152 | let mut count = 0; 153 | for v in iter.skip(s).take(l) { 154 | black_box(v); 155 | count += 1; 156 | } 157 | Ok(count) 158 | } 159 | Projection::Full => { 160 | // Scan the desired range of keys 161 | let iter = txn.scan(beg..end, s + l)?; 162 | // Create an iterator starting at the beginning 163 | let iter = iter.into_iter(); 164 | // We use a for loop to iterate over the results, while 165 | // calling black_box internally. This is necessary as 166 | // an iterator with `filter_map` or `map` is optimised 167 | // out by the compiler when calling `count` at the end. 168 | let mut count = 0; 169 | for v in iter.skip(s).take(l) { 170 | black_box(v.1); 171 | count += 1; 172 | } 173 | Ok(count) 174 | } 175 | Projection::Count => { 176 | Ok(txn 177 | .keys(beg..end, s + l)? 178 | .iter() 179 | .skip(s) // Skip the first `offset` entries 180 | .take(l) // Take the next `limit` entries 181 | .count()) 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::benchmark::NOT_SUPPORTED_ERROR; 2 | use crate::keyprovider::{IntegerKeyProvider, KeyProvider, StringKeyProvider}; 3 | use crate::valueprovider::Columns; 4 | use crate::Benchmark; 5 | use crate::{KeyType, Scan}; 6 | use anyhow::{bail, Result}; 7 | use serde_json::Value; 8 | use std::future::Future; 9 | use std::time::Duration; 10 | 11 | /// A trait for a database benchmark implementation 12 | /// setting up a database, and creating clients. 13 | pub(crate) trait BenchmarkEngine: Sized 14 | where 15 | C: BenchmarkClient + Send, 16 | { 17 | /// Initiates a new datastore benchmarking engine 18 | async fn setup(kt: KeyType, columns: Columns, endpoint: &Benchmark) -> Result; 19 | /// Creates a new client for this benchmarking engine 20 | async fn create_client(&self) -> Result; 21 | /// The number of seconds to wait before connecting 22 | fn wait_timeout(&self) -> Option { 23 | Some(Duration::from_secs(5)) 24 | } 25 | } 26 | 27 | /// A trait for a database benchmark implementation for 28 | /// running benchmark tests for a client or connection. 29 | pub(crate) trait BenchmarkClient: Sync + Send + 'static { 30 | /// Initialise the store at startup 31 | async fn startup(&self) -> Result<()> { 32 | Ok(()) 33 | } 34 | 35 | /// Cleanup the store at shutdown 36 | async fn shutdown(&self) -> Result<()> { 37 | Ok(()) 38 | } 39 | 40 | /// Compact the store for performance 41 | async fn compact(&self) -> Result<()> { 42 | Ok(()) 43 | } 44 | 45 | /// Create a single entry with the current client 46 | fn create( 47 | &self, 48 | n: u32, 49 | val: Value, 50 | kp: &mut KeyProvider, 51 | ) -> impl Future> + Send { 52 | async move { 53 | match kp { 54 | KeyProvider::OrderedInteger(p) => self.create_u32(p.key(n), val).await, 55 | KeyProvider::UnorderedInteger(p) => self.create_u32(p.key(n), val).await, 56 | KeyProvider::OrderedString(p) => self.create_string(p.key(n), val).await, 57 | KeyProvider::UnorderedString(p) => self.create_string(p.key(n), val).await, 58 | } 59 | } 60 | } 61 | 62 | /// Read a single entry with the current client 63 | fn read(&self, n: u32, kp: &mut KeyProvider) -> impl Future> + Send { 64 | async move { 65 | match kp { 66 | KeyProvider::OrderedInteger(p) => self.read_u32(p.key(n)).await, 67 | KeyProvider::UnorderedInteger(p) => self.read_u32(p.key(n)).await, 68 | KeyProvider::OrderedString(p) => self.read_string(p.key(n)).await, 69 | KeyProvider::UnorderedString(p) => self.read_string(p.key(n)).await, 70 | } 71 | } 72 | } 73 | 74 | /// Update a single entry with the current client 75 | fn update( 76 | &self, 77 | n: u32, 78 | val: Value, 79 | kp: &mut KeyProvider, 80 | ) -> impl Future> + Send { 81 | async move { 82 | match kp { 83 | KeyProvider::OrderedInteger(p) => self.update_u32(p.key(n), val).await, 84 | KeyProvider::UnorderedInteger(p) => self.update_u32(p.key(n), val).await, 85 | KeyProvider::OrderedString(p) => self.update_string(p.key(n), val).await, 86 | KeyProvider::UnorderedString(p) => self.update_string(p.key(n), val).await, 87 | } 88 | } 89 | } 90 | 91 | /// Delete a single entry with the current client 92 | fn delete(&self, n: u32, kp: &mut KeyProvider) -> impl Future> + Send { 93 | async move { 94 | match kp { 95 | KeyProvider::OrderedInteger(p) => self.delete_u32(p.key(n)).await, 96 | KeyProvider::UnorderedInteger(p) => self.delete_u32(p.key(n)).await, 97 | KeyProvider::OrderedString(p) => self.delete_string(p.key(n)).await, 98 | KeyProvider::UnorderedString(p) => self.delete_string(p.key(n)).await, 99 | } 100 | } 101 | } 102 | 103 | /// Scan a range of entries with the current client 104 | fn scan(&self, scan: &Scan, kp: &KeyProvider) -> impl Future> + Send { 105 | async move { 106 | let result = match kp { 107 | KeyProvider::OrderedInteger(_) | KeyProvider::UnorderedInteger(_) => { 108 | self.scan_u32(scan).await? 109 | } 110 | KeyProvider::OrderedString(_) | KeyProvider::UnorderedString(_) => { 111 | self.scan_string(scan).await? 112 | } 113 | }; 114 | if let Some(expect) = scan.expect { 115 | assert_eq!( 116 | expect, result, 117 | "Expected a length of {expect} but found {result} for {}", 118 | scan.name 119 | ); 120 | } 121 | Ok(()) 122 | } 123 | } 124 | 125 | /// Create a single entry with a numeric id 126 | fn create_u32(&self, key: u32, val: Value) -> impl Future> + Send; 127 | 128 | /// Create a single entry with a string id 129 | fn create_string(&self, key: String, val: Value) -> impl Future> + Send; 130 | 131 | /// Read a single entry with a numeric id 132 | fn read_u32(&self, key: u32) -> impl Future> + Send; 133 | 134 | /// Read a single entry with a string id 135 | fn read_string(&self, key: String) -> impl Future> + Send; 136 | 137 | /// Update a single entry with a numeric id 138 | fn update_u32(&self, key: u32, val: Value) -> impl Future> + Send; 139 | 140 | /// Update a single entry with a string id 141 | fn update_string(&self, key: String, val: Value) -> impl Future> + Send; 142 | 143 | /// Delete a single entry with a numeric id 144 | fn delete_u32(&self, key: u32) -> impl Future> + Send; 145 | 146 | /// Delete a single entry with a string id 147 | fn delete_string(&self, key: String) -> impl Future> + Send; 148 | 149 | /// Scan a range of entries with numeric ids 150 | fn scan_u32(&self, _scan: &Scan) -> impl Future> + Send { 151 | async move { bail!(NOT_SUPPORTED_ERROR) } 152 | } 153 | 154 | /// Scan a range of entries with string ids 155 | fn scan_string(&self, _scan: &Scan) -> impl Future> + Send { 156 | async move { bail!(NOT_SUPPORTED_ERROR) } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/fjall.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "fjall")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use fjall::{ 9 | Config, KvSeparationOptions, PartitionCreateOptions, PersistMode, TransactionalKeyspace, 10 | TxPartitionHandle, 11 | }; 12 | use serde_json::Value; 13 | use std::cmp::max; 14 | use std::hint::black_box; 15 | use std::sync::Arc; 16 | use std::time::Duration; 17 | use sysinfo::System; 18 | 19 | const DATABASE_DIR: &str = "fjall"; 20 | 21 | const MIN_CACHE_SIZE: u64 = 512 * 1024 * 1024; 22 | 23 | const DURABILITY: Option = Some(PersistMode::Buffer); 24 | 25 | pub(crate) struct FjallClientProvider { 26 | keyspace: Arc, 27 | partition: Arc, 28 | } 29 | 30 | impl BenchmarkEngine for FjallClientProvider { 31 | /// The number of seconds to wait before connecting 32 | fn wait_timeout(&self) -> Option { 33 | None 34 | } 35 | /// Initiates a new datastore benchmarking engine 36 | async fn setup(_kt: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 37 | // Cleanup the data directory 38 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 39 | // Load the system attributes 40 | let system = System::new_all(); 41 | // Get the total system memory 42 | let memory = system.total_memory(); 43 | // Divide the total memory into half 44 | let memory = memory.saturating_div(2); 45 | // Subtract 1 GiB from the memory size 46 | let memory = memory.saturating_sub(1024 * 1024 * 1024); 47 | // Fallback to the minimum memory cache size 48 | let memory = max(memory, MIN_CACHE_SIZE); 49 | // Configure the key-value separation 50 | let blobopts = KvSeparationOptions::default() 51 | // Separate values if larger than 1 KiB 52 | .separation_threshold(1024); 53 | // Configure and create the keyspace 54 | let keyspace = Config::new(DATABASE_DIR) 55 | // Fsync data every 100 milliseconds 56 | .fsync_ms(Some(100)) 57 | // Handle transaction flushed automatically 58 | .manual_journal_persist(false) 59 | // Set the amount of data to build up in memory 60 | .max_write_buffer_size(u64::MAX) 61 | // Set the cache size to 512 MiB 62 | .cache_size(memory) 63 | // Open a transactional keyspace 64 | .open_transactional()?; 65 | // Configure and create the partition 66 | let options = PartitionCreateOptions::default() 67 | // Set the data block size to 32 KiB 68 | .block_size(16 * 1_024) 69 | // Set the max memtable size to 256 MiB 70 | .max_memtable_size(256 * 1_024 * 1_024) 71 | // Separate values if larger than 4 KiB 72 | .with_kv_separation(blobopts); 73 | // Create a default data partition 74 | let partition = keyspace.open_partition("default", options)?; 75 | // Create the store 76 | Ok(Self { 77 | keyspace: Arc::new(keyspace), 78 | partition: Arc::new(partition), 79 | }) 80 | } 81 | /// Creates a new client for this benchmarking engine 82 | async fn create_client(&self) -> Result { 83 | Ok(FjallClient { 84 | keyspace: self.keyspace.clone(), 85 | partition: self.partition.clone(), 86 | }) 87 | } 88 | } 89 | 90 | pub(crate) struct FjallClient { 91 | keyspace: Arc, 92 | partition: Arc, 93 | } 94 | 95 | impl BenchmarkClient for FjallClient { 96 | async fn shutdown(&self) -> Result<()> { 97 | // Cleanup the data directory 98 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 99 | // Ok 100 | Ok(()) 101 | } 102 | 103 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 104 | self.create_bytes(&key.to_ne_bytes(), val).await 105 | } 106 | 107 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 108 | self.create_bytes(&key.into_bytes(), val).await 109 | } 110 | 111 | async fn read_u32(&self, key: u32) -> Result<()> { 112 | self.read_bytes(&key.to_ne_bytes()).await 113 | } 114 | 115 | async fn read_string(&self, key: String) -> Result<()> { 116 | self.read_bytes(&key.into_bytes()).await 117 | } 118 | 119 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 120 | self.update_bytes(&key.to_ne_bytes(), val).await 121 | } 122 | 123 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 124 | self.update_bytes(&key.into_bytes(), val).await 125 | } 126 | 127 | async fn delete_u32(&self, key: u32) -> Result<()> { 128 | self.delete_bytes(&key.to_ne_bytes()).await 129 | } 130 | 131 | async fn delete_string(&self, key: String) -> Result<()> { 132 | self.delete_bytes(&key.into_bytes()).await 133 | } 134 | 135 | async fn scan_u32(&self, scan: &Scan) -> Result { 136 | self.scan_bytes(scan).await 137 | } 138 | 139 | async fn scan_string(&self, scan: &Scan) -> Result { 140 | self.scan_bytes(scan).await 141 | } 142 | } 143 | 144 | impl FjallClient { 145 | async fn create_bytes(&self, key: &[u8], val: Value) -> Result<()> { 146 | // Serialise the value 147 | let val = bincode::serialize(&val)?; 148 | // Create a new transaction 149 | let mut txn = self.keyspace.write_tx().durability(DURABILITY); 150 | // Process the data 151 | txn.insert(&self.partition, key, val); 152 | txn.commit()?; 153 | Ok(()) 154 | } 155 | 156 | async fn read_bytes(&self, key: &[u8]) -> Result<()> { 157 | // Create a new transaction 158 | let txn = self.keyspace.read_tx(); 159 | // Process the data 160 | let res = txn.get(&self.partition, key)?; 161 | // Check the value exists 162 | assert!(res.is_some()); 163 | // Deserialise the value 164 | black_box(res.unwrap()); 165 | // All ok 166 | Ok(()) 167 | } 168 | 169 | async fn update_bytes(&self, key: &[u8], val: Value) -> Result<()> { 170 | // Serialise the value 171 | let val = bincode::serialize(&val)?; 172 | // Create a new transaction 173 | let mut txn = self.keyspace.write_tx().durability(DURABILITY); 174 | // Process the data 175 | txn.insert(&self.partition, key, val); 176 | txn.commit()?; 177 | Ok(()) 178 | } 179 | 180 | async fn delete_bytes(&self, key: &[u8]) -> Result<()> { 181 | // Create a new transaction 182 | let mut txn = self.keyspace.write_tx().durability(DURABILITY); 183 | // Process the data 184 | txn.remove(&self.partition, key); 185 | txn.commit()?; 186 | Ok(()) 187 | } 188 | 189 | async fn scan_bytes(&self, scan: &Scan) -> Result { 190 | // Contional scans are not supported 191 | if scan.condition.is_some() { 192 | bail!(NOT_SUPPORTED_ERROR); 193 | } 194 | // Extract parameters 195 | let s = scan.start.unwrap_or(0); 196 | let l = scan.limit.unwrap_or(usize::MAX); 197 | let p = scan.projection()?; 198 | // Create a new transaction 199 | let txn = self.keyspace.read_tx(); 200 | // Perform the relevant projection scan type 201 | match p { 202 | Projection::Id => { 203 | // Create an iterator starting at the beginning 204 | let iter = txn.keys(&self.partition); 205 | // We use a for loop to iterate over the results, while 206 | // calling black_box internally. This is necessary as 207 | // an iterator with `filter_map` or `map` is optimised 208 | // out by the compiler when calling `count` at the end. 209 | let mut count = 0; 210 | for v in iter.skip(s).take(l) { 211 | black_box(v.unwrap()); 212 | count += 1; 213 | } 214 | Ok(count) 215 | } 216 | Projection::Full => { 217 | // Create an iterator starting at the beginning 218 | let iter = txn.iter(&self.partition); 219 | // calling black_box internally. This is necessary as 220 | // an iterator with `filter_map` or `map` is optimised 221 | // out by the compiler when calling `count` at the end. 222 | let mut count = 0; 223 | for v in iter.skip(s).take(l) { 224 | black_box(v.unwrap().1); 225 | count += 1; 226 | } 227 | Ok(count) 228 | } 229 | Projection::Count => { 230 | Ok(txn 231 | .keys(&self.partition) 232 | .skip(s) // Skip the first `offset` entries 233 | .take(l) // Take the next `limit` entries 234 | .count()) 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/keydb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "keydb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::{bail, Result}; 9 | use futures::StreamExt; 10 | use redis::aio::MultiplexedConnection; 11 | use redis::{AsyncCommands, Client, ScanOptions}; 12 | use serde_json::Value; 13 | use std::hint::black_box; 14 | use tokio::sync::Mutex; 15 | 16 | pub const DEFAULT: &str = "redis://:root@127.0.0.1:6379/"; 17 | 18 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 19 | DockerParams { 20 | image: "eqalpha/keydb", 21 | pre_args: "-p 127.0.0.1:6379:6379", 22 | post_args: "keydb-server --requirepass root", 23 | } 24 | } 25 | 26 | pub(crate) struct KeydbClientProvider { 27 | url: String, 28 | } 29 | 30 | impl BenchmarkEngine for KeydbClientProvider { 31 | /// Initiates a new datastore benchmarking engine 32 | async fn setup(_kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 33 | Ok(KeydbClientProvider { 34 | url: options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(), 35 | }) 36 | } 37 | /// Creates a new client for this benchmarking engine 38 | async fn create_client(&self) -> Result { 39 | let client = Client::open(self.url.as_str())?; 40 | let conn_record = Mutex::new(client.get_multiplexed_async_connection().await?); 41 | let conn_iter = Mutex::new(client.get_multiplexed_async_connection().await?); 42 | Ok(KeydbClient { 43 | conn_record, 44 | conn_iter, 45 | }) 46 | } 47 | } 48 | 49 | pub(crate) struct KeydbClient { 50 | conn_record: Mutex, 51 | conn_iter: Mutex, 52 | } 53 | 54 | impl BenchmarkClient for KeydbClient { 55 | #[allow(dependency_on_unit_never_type_fallback)] 56 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 57 | let val = bincode::serialize(&val)?; 58 | self.conn_record.lock().await.set(key, val).await?; 59 | Ok(()) 60 | } 61 | 62 | #[allow(dependency_on_unit_never_type_fallback)] 63 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 64 | let val = bincode::serialize(&val)?; 65 | self.conn_record.lock().await.set(key, val).await?; 66 | Ok(()) 67 | } 68 | 69 | #[allow(dependency_on_unit_never_type_fallback)] 70 | async fn read_u32(&self, key: u32) -> Result<()> { 71 | let val: Vec = self.conn_record.lock().await.get(key).await?; 72 | assert!(!val.is_empty()); 73 | black_box(val); 74 | Ok(()) 75 | } 76 | 77 | #[allow(dependency_on_unit_never_type_fallback)] 78 | async fn read_string(&self, key: String) -> Result<()> { 79 | let val: Vec = self.conn_record.lock().await.get(key).await?; 80 | assert!(!val.is_empty()); 81 | black_box(val); 82 | Ok(()) 83 | } 84 | 85 | #[allow(dependency_on_unit_never_type_fallback)] 86 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 87 | let val = bincode::serialize(&val)?; 88 | self.conn_record.lock().await.set(key, val).await?; 89 | Ok(()) 90 | } 91 | 92 | #[allow(dependency_on_unit_never_type_fallback)] 93 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 94 | let val = bincode::serialize(&val)?; 95 | self.conn_record.lock().await.set(key, val).await?; 96 | Ok(()) 97 | } 98 | 99 | #[allow(dependency_on_unit_never_type_fallback)] 100 | async fn delete_u32(&self, key: u32) -> Result<()> { 101 | self.conn_record.lock().await.del(key).await?; 102 | Ok(()) 103 | } 104 | 105 | #[allow(dependency_on_unit_never_type_fallback)] 106 | async fn delete_string(&self, key: String) -> Result<()> { 107 | self.conn_record.lock().await.del(key).await?; 108 | Ok(()) 109 | } 110 | 111 | async fn scan_u32(&self, scan: &Scan) -> Result { 112 | self.scan_bytes(scan).await 113 | } 114 | 115 | async fn scan_string(&self, scan: &Scan) -> Result { 116 | self.scan_bytes(scan).await 117 | } 118 | } 119 | 120 | impl KeydbClient { 121 | async fn scan_bytes(&self, scan: &Scan) -> Result { 122 | // Conditional scans are not supported 123 | if scan.condition.is_some() { 124 | bail!(NOT_SUPPORTED_ERROR); 125 | } 126 | // Extract parameters 127 | let s = scan.start.unwrap_or(0); 128 | let l = scan.limit.unwrap_or(usize::MAX); 129 | let p = scan.projection()?; 130 | // Get the two connection types 131 | let mut conn_iter = self.conn_iter.lock().await; 132 | let mut conn_record = self.conn_record.lock().await; 133 | // Configure the scan options for improve iteration 134 | let opts = ScanOptions::default().with_count(5000); 135 | // Create an iterator starting at the beginning 136 | let mut iter = conn_iter.scan_options::(opts).await?.skip(s); 137 | // Perform the relevant projection scan type 138 | match p { 139 | Projection::Id => { 140 | // We use a for loop to iterate over the results, while 141 | // calling black_box internally. This is necessary as 142 | // an iterator with `filter_map` or `map` is optimised 143 | // out by the compiler when calling `count` at the end. 144 | let mut count = 0; 145 | for _ in 0..l { 146 | if let Some(k) = iter.next().await { 147 | black_box(k); 148 | count += 1; 149 | } else { 150 | break; 151 | } 152 | } 153 | Ok(count) 154 | } 155 | Projection::Full => { 156 | // We use a for loop to iterate over the results, while 157 | // calling black_box internally. This is necessary as 158 | // an iterator with `filter_map` or `map` is optimised 159 | // out by the compiler when calling `count` at the end. 160 | let mut count = 0; 161 | while let Some(k) = iter.next().await { 162 | let v: Vec = conn_record.get(k).await?; 163 | black_box(v); 164 | count += 1; 165 | if count >= l { 166 | break; 167 | } 168 | } 169 | Ok(count) 170 | } 171 | Projection::Count => match scan.limit { 172 | // Full count queries are too slow 173 | None => bail!(NOT_SUPPORTED_ERROR), 174 | Some(l) => Ok(iter.take(l).count().await), 175 | }, 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/keyprovider.rs: -------------------------------------------------------------------------------- 1 | use crate::KeyType; 2 | use twox_hash::XxHash64; 3 | 4 | #[derive(Clone, Copy)] 5 | pub(crate) enum KeyProvider { 6 | OrderedInteger(OrderedInteger), 7 | UnorderedInteger(UnorderedInteger), 8 | OrderedString(OrderedString), 9 | UnorderedString(UnorderedString), 10 | } 11 | 12 | impl KeyProvider { 13 | pub(crate) fn new(key_type: KeyType, random: bool) -> Self { 14 | match key_type { 15 | KeyType::Integer => { 16 | if random { 17 | Self::UnorderedInteger(UnorderedInteger::default()) 18 | } else { 19 | Self::OrderedInteger(OrderedInteger::default()) 20 | } 21 | } 22 | KeyType::String26 => { 23 | if random { 24 | Self::UnorderedString(UnorderedString::new(1)) 25 | } else { 26 | Self::OrderedString(OrderedString::new(1)) 27 | } 28 | } 29 | KeyType::String90 => { 30 | if random { 31 | Self::UnorderedString(UnorderedString::new(5)) 32 | } else { 33 | Self::OrderedString(OrderedString::new(5)) 34 | } 35 | } 36 | KeyType::String250 => { 37 | if random { 38 | Self::UnorderedString(UnorderedString::new(15)) 39 | } else { 40 | Self::OrderedString(OrderedString::new(15)) 41 | } 42 | } 43 | KeyType::String506 => { 44 | if random { 45 | Self::UnorderedString(UnorderedString::new(31)) 46 | } else { 47 | Self::OrderedString(OrderedString::new(31)) 48 | } 49 | } 50 | KeyType::Uuid => { 51 | todo!() 52 | } 53 | } 54 | } 55 | } 56 | 57 | pub(crate) trait IntegerKeyProvider { 58 | fn key(&mut self, n: u32) -> u32; 59 | } 60 | 61 | pub(crate) trait StringKeyProvider { 62 | fn key(&mut self, n: u32) -> String; 63 | } 64 | 65 | #[derive(Default, Clone, Copy)] 66 | pub(crate) struct OrderedInteger(); 67 | 68 | impl IntegerKeyProvider for OrderedInteger { 69 | fn key(&mut self, n: u32) -> u32 { 70 | // We need to increment by 1 71 | // because MySQL PRIMARY IDs 72 | // can not be 0, resulting in 73 | // duplicate ID errors. 74 | n + 1 75 | } 76 | } 77 | #[derive(Default, Clone, Copy)] 78 | pub(crate) struct UnorderedInteger(); 79 | 80 | impl IntegerKeyProvider for UnorderedInteger { 81 | fn key(&mut self, n: u32) -> u32 { 82 | Self::feistel_transform(n) 83 | } 84 | } 85 | 86 | impl UnorderedInteger { 87 | // A very simple round function: XOR the input with the key and shift 88 | fn feistel_round_function(value: u32, key: u32) -> u32 { 89 | (value ^ key).rotate_left(5).wrapping_add(key) 90 | } 91 | 92 | // Perform one round of the Feistel network 93 | fn feistel_round(left: u16, right: u16, round_key: u32) -> (u16, u16) { 94 | let new_left = right; 95 | let new_right = left ^ (Self::feistel_round_function(right as u32, round_key) as u16); 96 | (new_left, new_right) 97 | } 98 | 99 | fn feistel_transform(input: u32) -> u32 { 100 | let mut left = (input >> 16) as u16; 101 | let mut right = (input & 0xFFFF) as u16; 102 | 103 | // Hard-coded keys for simplicity 104 | let keys = [0xA5A5A5A5, 0x5A5A5A5A, 0x3C3C3C3C]; 105 | 106 | for &key in &keys { 107 | let (new_left, new_right) = Self::feistel_round(left, right, key); 108 | left = new_left; 109 | right = new_right; 110 | } 111 | 112 | // Combine left and right halves back into a single u32 113 | ((left as u32) << 16) | (right as u32) 114 | } 115 | } 116 | 117 | fn hash_string(n: u32, repeat: usize) -> String { 118 | let mut hex_string = String::with_capacity(repeat * 16 + 10); 119 | for s in 0..repeat as u64 { 120 | let hash_result = XxHash64::oneshot(s, &n.to_be_bytes()); 121 | hex_string.push_str(&format!("{:x}", hash_result)); 122 | } 123 | hex_string 124 | } 125 | 126 | #[derive(Clone, Copy)] 127 | pub(crate) struct OrderedString(usize); 128 | 129 | impl OrderedString { 130 | fn new(repeat: usize) -> Self { 131 | Self(repeat) 132 | } 133 | } 134 | 135 | impl StringKeyProvider for OrderedString { 136 | fn key(&mut self, n: u32) -> String { 137 | let hex_string = hash_string(n, self.0); 138 | format!("{:010}{hex_string}", n) 139 | } 140 | } 141 | 142 | #[derive(Default, Clone, Copy)] 143 | pub(crate) struct UnorderedString(usize); 144 | 145 | impl UnorderedString { 146 | fn new(repeat: usize) -> Self { 147 | Self(repeat) 148 | } 149 | } 150 | 151 | impl StringKeyProvider for UnorderedString { 152 | fn key(&mut self, n: u32) -> String { 153 | let hex_string = hash_string(n, self.0); 154 | format!("{hex_string}{:010}", n) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod test { 160 | use crate::keyprovider::{OrderedString, StringKeyProvider, UnorderedString}; 161 | 162 | #[test] 163 | fn ordered_string_26() { 164 | let mut o = OrderedString::new(1); 165 | let s = o.key(12345678); 166 | assert_eq!(s.len(), 26); 167 | assert_eq!(s, "0012345678d79235c904e704c6"); 168 | } 169 | 170 | #[test] 171 | fn unordered_string_26() { 172 | let mut o = UnorderedString::new(1); 173 | let s = o.key(12345678); 174 | assert_eq!(s.len(), 26); 175 | assert_eq!(s, "d79235c904e704c60012345678"); 176 | } 177 | 178 | #[test] 179 | fn ordered_string_90() { 180 | let mut o = OrderedString::new(5); 181 | let s = o.key(12345678); 182 | assert_eq!(s.len(), 90); 183 | assert_eq!(s, "0012345678d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d3"); 184 | } 185 | 186 | #[test] 187 | fn unordered_string_90() { 188 | let mut o = UnorderedString::new(5); 189 | let s = o.key(12345678); 190 | assert_eq!(s.len(), 90); 191 | assert_eq!(s, "d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d30012345678"); 192 | } 193 | 194 | #[test] 195 | fn ordered_string_250() { 196 | let mut o = OrderedString::new(15); 197 | let s = o.key(12345678); 198 | assert_eq!(s.len(), 250); 199 | assert_eq!(s, "0012345678d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d31bc0feb2815d5c908f5a4633b8a9d5d6ec1c074d5d64ab296c6495f784f8294ac42b828a9c4ef45d3decc0a8dff00062adfb547fea6132f38afda36acf629cc15413acfe35a50fecbec285e9ee42b136"); 200 | } 201 | 202 | #[test] 203 | fn unordered_string_250() { 204 | let mut o = UnorderedString::new(15); 205 | let s = o.key(12345678); 206 | assert_eq!(s.len(), 250); 207 | assert_eq!(s, "d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d31bc0feb2815d5c908f5a4633b8a9d5d6ec1c074d5d64ab296c6495f784f8294ac42b828a9c4ef45d3decc0a8dff00062adfb547fea6132f38afda36acf629cc15413acfe35a50fecbec285e9ee42b1360012345678"); 208 | } 209 | 210 | #[test] 211 | fn ordered_string_506() { 212 | let mut o = OrderedString::new(31); 213 | let s = o.key(12345678); 214 | assert_eq!(s.len(), 506); 215 | assert_eq!(s, "0012345678d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d31bc0feb2815d5c908f5a4633b8a9d5d6ec1c074d5d64ab296c6495f784f8294ac42b828a9c4ef45d3decc0a8dff00062adfb547fea6132f38afda36acf629cc15413acfe35a50fecbec285e9ee42b13691088df6c3740c87c3d003e3addf1888a582ac5cb408feec138fe9a43c9fda574006e770bb0b5e84edcbeecc6f723960ed7d02591a7b2487bb317f83bfd95e44a69d957deb6b10e22d895a375acfa54143137feeb53921625bc9d582166477e562454fecc90f130662338c070bd709c27d8478abaa825dc69bc3aa89dc7ce076"); 216 | } 217 | 218 | #[test] 219 | fn unordered_string_506() { 220 | let mut o = UnorderedString::new(31); 221 | let s = o.key(12345678); 222 | assert_eq!(s.len(), 506); 223 | assert_eq!(s, "d79235c904e704c6c379c25fea98cd11b4d0f71900f91df2ecc87c25d7fff4b03be1bd13590485d31bc0feb2815d5c908f5a4633b8a9d5d6ec1c074d5d64ab296c6495f784f8294ac42b828a9c4ef45d3decc0a8dff00062adfb547fea6132f38afda36acf629cc15413acfe35a50fecbec285e9ee42b13691088df6c3740c87c3d003e3addf1888a582ac5cb408feec138fe9a43c9fda574006e770bb0b5e84edcbeecc6f723960ed7d02591a7b2487bb317f83bfd95e44a69d957deb6b10e22d895a375acfa54143137feeb53921625bc9d582166477e562454fecc90f130662338c070bd709c27d8478abaa825dc69bc3aa89dc7ce0760012345678"); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/lmdb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "lmdb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use heed::types::Bytes; 9 | use heed::{Database, EnvOpenOptions}; 10 | use heed::{Env, EnvFlags}; 11 | use serde_json::Value; 12 | use std::hint::black_box; 13 | use std::sync::Arc; 14 | use std::sync::LazyLock; 15 | use std::time::Duration; 16 | 17 | const DATABASE_DIR: &str = "lmdb"; 18 | 19 | const DEFAULT_SIZE: usize = 1_073_741_824; 20 | 21 | static DATABASE_SIZE: LazyLock = LazyLock::new(|| { 22 | std::env::var("CRUD_BENCH_LMDB_DATABASE_SIZE") 23 | .map(|s| s.parse::().unwrap_or(DEFAULT_SIZE)) 24 | .unwrap_or(DEFAULT_SIZE) 25 | }); 26 | 27 | pub(crate) struct LmDBClientProvider(Arc<(Env, Database)>); 28 | 29 | impl BenchmarkEngine for LmDBClientProvider { 30 | /// The number of seconds to wait before connecting 31 | fn wait_timeout(&self) -> Option { 32 | None 33 | } 34 | /// Initiates a new datastore benchmarking engine 35 | async fn setup(_kt: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 36 | // Cleanup the data directory 37 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 38 | // Recreate the database directory 39 | std::fs::create_dir(DATABASE_DIR)?; 40 | // Create a new environment 41 | let env = unsafe { 42 | EnvOpenOptions::new() 43 | .flags( 44 | EnvFlags::NO_TLS 45 | | EnvFlags::MAP_ASYNC 46 | | EnvFlags::NO_SYNC 47 | | EnvFlags::NO_META_SYNC, 48 | ) 49 | .map_size(*DATABASE_SIZE) 50 | .open(DATABASE_DIR) 51 | }?; 52 | // Creaye the database 53 | let db = { 54 | // Open a new transaction 55 | let mut txn = env.write_txn()?; 56 | // Initiate the database 57 | env.create_database::(&mut txn, None)? 58 | }; 59 | // Create the store 60 | Ok(Self(Arc::new((env, db)))) 61 | } 62 | /// Creates a new client for this benchmarking engine 63 | async fn create_client(&self) -> Result { 64 | Ok(LmDBClient { 65 | db: self.0.clone(), 66 | }) 67 | } 68 | } 69 | 70 | pub(crate) struct LmDBClient { 71 | db: Arc<(Env, Database)>, 72 | } 73 | 74 | impl BenchmarkClient for LmDBClient { 75 | async fn shutdown(&self) -> Result<()> { 76 | // Cleanup the data directory 77 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 78 | // Ok 79 | Ok(()) 80 | } 81 | 82 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 83 | self.create_bytes(&key.to_ne_bytes(), val).await 84 | } 85 | 86 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 87 | self.create_bytes(&key.into_bytes(), val).await 88 | } 89 | 90 | async fn read_u32(&self, key: u32) -> Result<()> { 91 | self.read_bytes(&key.to_ne_bytes()).await 92 | } 93 | 94 | async fn read_string(&self, key: String) -> Result<()> { 95 | self.read_bytes(&key.into_bytes()).await 96 | } 97 | 98 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 99 | self.update_bytes(&key.to_ne_bytes(), val).await 100 | } 101 | 102 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 103 | self.update_bytes(&key.into_bytes(), val).await 104 | } 105 | 106 | async fn delete_u32(&self, key: u32) -> Result<()> { 107 | self.delete_bytes(&key.to_ne_bytes()).await 108 | } 109 | 110 | async fn delete_string(&self, key: String) -> Result<()> { 111 | self.delete_bytes(&key.into_bytes()).await 112 | } 113 | 114 | async fn scan_u32(&self, scan: &Scan) -> Result { 115 | self.scan_bytes(scan).await 116 | } 117 | 118 | async fn scan_string(&self, scan: &Scan) -> Result { 119 | self.scan_bytes(scan).await 120 | } 121 | } 122 | 123 | impl LmDBClient { 124 | async fn create_bytes(&self, key: &[u8], val: Value) -> Result<()> { 125 | // Serialise the value 126 | let val = bincode::serialize(&val)?; 127 | // Create a new transaction 128 | let mut txn = self.db.0.write_txn()?; 129 | // Process the data 130 | self.db.1.put(&mut txn, key, val.as_ref())?; 131 | txn.commit()?; 132 | Ok(()) 133 | } 134 | 135 | async fn read_bytes(&self, key: &[u8]) -> Result<()> { 136 | // Create a new transaction 137 | let txn = self.db.0.read_txn()?; 138 | // Process the data 139 | let res: Option<_> = self.db.1.get(&txn, key)?; 140 | // Check the value exists 141 | assert!(res.is_some()); 142 | // Deserialise the value 143 | black_box(res.unwrap()); 144 | // All ok 145 | Ok(()) 146 | } 147 | 148 | async fn update_bytes(&self, key: &[u8], val: Value) -> Result<()> { 149 | // Serialise the value 150 | let val = bincode::serialize(&val)?; 151 | // Create a new transaction 152 | let mut txn = self.db.0.write_txn()?; 153 | // Process the data 154 | self.db.1.put(&mut txn, key, &val)?; 155 | txn.commit()?; 156 | Ok(()) 157 | } 158 | 159 | async fn delete_bytes(&self, key: &[u8]) -> Result<()> { 160 | // Create a new transaction 161 | let mut txn = self.db.0.write_txn()?; 162 | // Process the data 163 | self.db.1.delete(&mut txn, key)?; 164 | txn.commit()?; 165 | Ok(()) 166 | } 167 | 168 | async fn scan_bytes(&self, scan: &Scan) -> Result { 169 | // Contional scans are not supported 170 | if scan.condition.is_some() { 171 | bail!(NOT_SUPPORTED_ERROR); 172 | } 173 | // Extract parameters 174 | let s = scan.start.unwrap_or(0); 175 | let l = scan.limit.unwrap_or(usize::MAX); 176 | let p = scan.projection()?; 177 | // Create a new transaction 178 | let txn = self.db.0.read_txn()?; 179 | // Create an iterator starting at the beginning 180 | let iter = self.db.1.iter(&txn)?; 181 | // Perform the relevant projection scan type 182 | match p { 183 | Projection::Id => { 184 | // We use a for loop to iterate over the results, while 185 | // calling black_box internally. This is necessary as 186 | // an iterator with `filter_map` or `map` is optimised 187 | // out by the compiler when calling `count` at the end. 188 | let mut count = 0; 189 | for v in iter.skip(s).take(l) { 190 | black_box(v.unwrap().0); 191 | count += 1; 192 | } 193 | Ok(count) 194 | } 195 | Projection::Full => { 196 | // We use a for loop to iterate over the results, while 197 | // calling black_box internally. This is necessary as 198 | // an iterator with `filter_map` or `map` is optimised 199 | // out by the compiler when calling `count` at the end. 200 | let mut count = 0; 201 | for v in iter.skip(s).take(l) { 202 | black_box(v.unwrap().1); 203 | count += 1; 204 | } 205 | Ok(count) 206 | } 207 | Projection::Count => { 208 | Ok(iter 209 | .skip(s) // Skip the first `offset` entries 210 | .take(l) // Take the next `limit` entries 211 | .count()) 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/map.rs: -------------------------------------------------------------------------------- 1 | use crate::benchmark::NOT_SUPPORTED_ERROR; 2 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 3 | use crate::valueprovider::Columns; 4 | use crate::{Benchmark, KeyType, Projection, Scan}; 5 | use anyhow::{bail, Result}; 6 | use dashmap::DashMap; 7 | use serde_json::Value; 8 | use std::hash::Hash; 9 | use std::hint::black_box; 10 | use std::sync::Arc; 11 | use std::time::Duration; 12 | 13 | #[derive(Clone)] 14 | pub(crate) enum MapDatabase { 15 | Integer(Arc>), 16 | String(Arc>), 17 | } 18 | 19 | impl From for MapDatabase { 20 | fn from(t: KeyType) -> Self { 21 | match t { 22 | KeyType::Integer => Self::Integer(DashMap::new().into()), 23 | KeyType::String26 | KeyType::String90 | KeyType::String250 | KeyType::String506 => { 24 | Self::String(DashMap::new().into()) 25 | } 26 | KeyType::Uuid => todo!(), 27 | } 28 | } 29 | } 30 | 31 | pub(crate) struct MapClientProvider(MapDatabase); 32 | 33 | impl BenchmarkEngine for MapClientProvider { 34 | /// The number of seconds to wait before connecting 35 | fn wait_timeout(&self) -> Option { 36 | None 37 | } 38 | /// Initiates a new datastore benchmarking engine 39 | async fn setup(kt: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 40 | Ok(Self(kt.into())) 41 | } 42 | /// Creates a new client for this benchmarking engine 43 | async fn create_client(&self) -> Result { 44 | Ok(MapClient(self.0.clone())) 45 | } 46 | } 47 | 48 | pub(crate) struct MapClient(MapDatabase); 49 | 50 | impl BenchmarkClient for MapClient { 51 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 52 | if let MapDatabase::Integer(m) = &self.0 { 53 | assert!(m.insert(key, val).is_none()); 54 | } else { 55 | bail!("Invalid MapDatabase variant"); 56 | } 57 | Ok(()) 58 | } 59 | 60 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 61 | if let MapDatabase::String(m) = &self.0 { 62 | assert!(m.insert(key, val).is_none()); 63 | } else { 64 | bail!("Invalid MapDatabase variant"); 65 | } 66 | Ok(()) 67 | } 68 | 69 | async fn read_u32(&self, key: u32) -> Result<()> { 70 | if let MapDatabase::Integer(m) = &self.0 { 71 | assert!(m.get(&key).is_some()); 72 | } else { 73 | bail!("Invalid MapDatabase variant"); 74 | } 75 | Ok(()) 76 | } 77 | 78 | async fn read_string(&self, key: String) -> Result<()> { 79 | if let MapDatabase::String(m) = &self.0 { 80 | assert!(m.get(&key).is_some()); 81 | } else { 82 | bail!("Invalid MapDatabase variant"); 83 | } 84 | Ok(()) 85 | } 86 | 87 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 88 | if let MapDatabase::Integer(m) = &self.0 { 89 | assert!(m.insert(key, val).is_some()); 90 | } else { 91 | bail!("Invalid MapDatabase variant"); 92 | } 93 | Ok(()) 94 | } 95 | 96 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 97 | if let MapDatabase::String(m) = &self.0 { 98 | assert!(m.insert(key, val).is_some()); 99 | } else { 100 | bail!("Invalid MapDatabase variant"); 101 | } 102 | Ok(()) 103 | } 104 | 105 | async fn delete_u32(&self, key: u32) -> Result<()> { 106 | if let MapDatabase::Integer(m) = &self.0 { 107 | assert!(m.remove(&key).is_some()); 108 | } else { 109 | bail!("Invalid MapDatabase variant"); 110 | } 111 | Ok(()) 112 | } 113 | 114 | async fn delete_string(&self, key: String) -> Result<()> { 115 | if let MapDatabase::String(m) = &self.0 { 116 | assert!(m.remove(&key).is_some()); 117 | } else { 118 | bail!("Invalid MapDatabase variant"); 119 | } 120 | Ok(()) 121 | } 122 | 123 | async fn scan_u32(&self, scan: &Scan) -> Result { 124 | if let MapDatabase::Integer(m) = &self.0 { 125 | Self::scan(m, scan).await 126 | } else { 127 | bail!("Invalid MapDatabase variant"); 128 | } 129 | } 130 | 131 | async fn scan_string(&self, scan: &Scan) -> Result { 132 | if let MapDatabase::String(m) = &self.0 { 133 | Self::scan(m, scan).await 134 | } else { 135 | bail!("Invalid MapDatabase variant"); 136 | } 137 | } 138 | } 139 | 140 | impl MapClient { 141 | async fn scan(m: &DashMap, scan: &Scan) -> Result 142 | where 143 | T: Eq + Hash, 144 | { 145 | // Contional scans are not supported 146 | if scan.condition.is_some() { 147 | bail!(NOT_SUPPORTED_ERROR); 148 | } 149 | // Extract parameters 150 | let s = scan.start.unwrap_or(0); 151 | let l = scan.limit.unwrap_or(usize::MAX); 152 | let p = scan.projection()?; 153 | // Perform the relevant projection scan type 154 | match p { 155 | Projection::Id => { 156 | // We use a for loop to iterate over the results, while 157 | // calling black_box internally. This is necessary as 158 | // an iterator with `filter_map` or `map` is optimised 159 | // out by the compiler when calling `count` at the end. 160 | let mut count = 0; 161 | for v in m.iter().skip(s).take(l) { 162 | black_box(v); 163 | count += 1; 164 | } 165 | Ok(count) 166 | } 167 | Projection::Full => { 168 | // We use a for loop to iterate over the results, while 169 | // calling black_box internally. This is necessary as 170 | // an iterator with `filter_map` or `map` is optimised 171 | // out by the compiler when calling `count` at the end. 172 | let mut count = 0; 173 | for v in m.iter().skip(s).take(l) { 174 | black_box(v); 175 | count += 1; 176 | } 177 | Ok(count) 178 | } 179 | Projection::Count => Ok(m 180 | .iter() 181 | .skip(s) // Skip the first `offset` entries 182 | .take(l) // Take the next `limit` entries 183 | .count()), 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/memodb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "memodb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use memodb::Database; 9 | use serde_json::Value; 10 | use std::hint::black_box; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | 14 | type Key = Vec; 15 | type Val = Vec; 16 | 17 | pub(crate) struct MemoDBClientProvider(Arc>); 18 | 19 | impl BenchmarkEngine for MemoDBClientProvider { 20 | /// The number of seconds to wait before connecting 21 | fn wait_timeout(&self) -> Option { 22 | None 23 | } 24 | /// Initiates a new datastore benchmarking engine 25 | async fn setup(_: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 26 | // Create the store 27 | Ok(Self(Arc::new(Database::new()))) 28 | } 29 | /// Creates a new client for this benchmarking engine 30 | async fn create_client(&self) -> Result { 31 | Ok(MemoDBClient { 32 | db: self.0.clone(), 33 | }) 34 | } 35 | } 36 | 37 | pub(crate) struct MemoDBClient { 38 | db: Arc>, 39 | } 40 | 41 | impl BenchmarkClient for MemoDBClient { 42 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 43 | self.create_bytes(&key.to_ne_bytes(), val).await 44 | } 45 | 46 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 47 | self.create_bytes(&key.into_bytes(), val).await 48 | } 49 | 50 | async fn read_u32(&self, key: u32) -> Result<()> { 51 | self.read_bytes(&key.to_ne_bytes()).await 52 | } 53 | 54 | async fn read_string(&self, key: String) -> Result<()> { 55 | self.read_bytes(&key.into_bytes()).await 56 | } 57 | 58 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 59 | self.update_bytes(&key.to_ne_bytes(), val).await 60 | } 61 | 62 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 63 | self.update_bytes(&key.into_bytes(), val).await 64 | } 65 | 66 | async fn delete_u32(&self, key: u32) -> Result<()> { 67 | self.delete_bytes(&key.to_ne_bytes()).await 68 | } 69 | 70 | async fn delete_string(&self, key: String) -> Result<()> { 71 | self.delete_bytes(&key.into_bytes()).await 72 | } 73 | 74 | async fn scan_u32(&self, scan: &Scan) -> Result { 75 | self.scan_bytes(scan).await 76 | } 77 | 78 | async fn scan_string(&self, scan: &Scan) -> Result { 79 | self.scan_bytes(scan).await 80 | } 81 | } 82 | 83 | impl MemoDBClient { 84 | async fn create_bytes(&self, key: &[u8], val: Value) -> Result<()> { 85 | // Serialise the value 86 | let val = bincode::serialize(&val)?; 87 | // Create a new transaction 88 | let mut txn = self.db.transaction(true); 89 | // Process the data 90 | txn.set(key, val)?; 91 | txn.commit()?; 92 | Ok(()) 93 | } 94 | 95 | async fn read_bytes(&self, key: &[u8]) -> Result<()> { 96 | // Create a new transaction 97 | let mut txn = self.db.transaction(false); 98 | // Process the data 99 | let res = txn.get(key.to_vec())?; 100 | // Check the value exists 101 | assert!(res.is_some()); 102 | // Deserialise the value 103 | black_box(res.unwrap()); 104 | // All ok 105 | Ok(()) 106 | } 107 | 108 | async fn update_bytes(&self, key: &[u8], val: Value) -> Result<()> { 109 | // Serialise the value 110 | let val = bincode::serialize(&val)?; 111 | // Create a new transaction 112 | let mut txn = self.db.transaction(true); 113 | // Process the data 114 | txn.set(key, val)?; 115 | txn.commit()?; 116 | Ok(()) 117 | } 118 | 119 | async fn delete_bytes(&self, key: &[u8]) -> Result<()> { 120 | // Create a new transaction 121 | let mut txn = self.db.transaction(true); 122 | // Process the data 123 | txn.del(key)?; 124 | txn.commit()?; 125 | Ok(()) 126 | } 127 | 128 | async fn scan_bytes(&self, scan: &Scan) -> Result { 129 | // Contional scans are not supported 130 | if scan.condition.is_some() { 131 | bail!(NOT_SUPPORTED_ERROR); 132 | } 133 | // Extract parameters 134 | let p = scan.projection()?; 135 | // Create a new transaction 136 | let mut txn = self.db.transaction(false); 137 | let beg = [0u8].to_vec(); 138 | let end = [255u8].to_vec(); 139 | // Perform the relevant projection scan type 140 | match p { 141 | Projection::Id => { 142 | // Scan the desired range of keys 143 | let iter = txn.keys(beg..end, scan.start, scan.limit)?; 144 | // Create an iterator starting at the beginning 145 | let iter = iter.into_iter(); 146 | // We use a for loop to iterate over the results, while 147 | // calling black_box internally. This is necessary as 148 | // an iterator with `filter_map` or `map` is optimised 149 | // out by the compiler when calling `count` at the end. 150 | let mut count = 0; 151 | for v in iter { 152 | black_box(v); 153 | count += 1; 154 | } 155 | Ok(count) 156 | } 157 | Projection::Full => { 158 | // Scan the desired range of keys 159 | let iter = txn.scan(beg..end, scan.start, scan.limit)?; 160 | // Create an iterator starting at the beginning 161 | let iter = iter.into_iter(); 162 | // We use a for loop to iterate over the results, while 163 | // calling black_box internally. This is necessary as 164 | // an iterator with `filter_map` or `map` is optimised 165 | // out by the compiler when calling `count` at the end. 166 | let mut count = 0; 167 | for v in iter { 168 | black_box(v.1); 169 | count += 1; 170 | } 171 | Ok(count) 172 | } 173 | Projection::Count => Ok(txn.total(beg..end, scan.start, scan.limit)?), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/mongodb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "mongodb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::{bail, Result}; 9 | use futures::{StreamExt, TryStreamExt}; 10 | use mongodb::bson::{doc, Bson, Document}; 11 | use mongodb::options::ClientOptions; 12 | use mongodb::options::DatabaseOptions; 13 | use mongodb::options::ReadConcern; 14 | use mongodb::options::{Acknowledgment, WriteConcern}; 15 | use mongodb::{bson, Client, Collection, Cursor, Database}; 16 | use serde_json::Value; 17 | use std::hint::black_box; 18 | 19 | pub const DEFAULT: &str = "mongodb://root:root@127.0.0.1:27017"; 20 | 21 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 22 | DockerParams { 23 | image: "mongo", 24 | pre_args: "--ulimit nofile=65536:65536 -p 127.0.0.1:27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=root", 25 | post_args: "", 26 | } 27 | } 28 | 29 | pub(crate) struct MongoDBClientProvider { 30 | sync: bool, 31 | client: Client, 32 | } 33 | 34 | impl BenchmarkEngine for MongoDBClientProvider { 35 | /// Initiates a new datastore benchmarking engine 36 | async fn setup(_kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 37 | // Get the custom endpoint if specified 38 | let url = options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(); 39 | // Create a new client with a connection pool. 40 | // The MongoDB client does not correctly limit 41 | // the number of connections in the connection 42 | // pool. Therefore we create a single connection 43 | // pool and share it with all of the crud-bench 44 | // clients. This follows the recommended advice 45 | // for using the MongoDB driver. Note that this 46 | // still creates 2 more connections than has 47 | // been specified in the `max_pool_size` option. 48 | let mut opts = ClientOptions::parse(url).await?; 49 | opts.max_pool_size = Some(options.clients); 50 | opts.min_pool_size = None; 51 | // Create the client provider 52 | Ok(Self { 53 | sync: options.sync, 54 | client: Client::with_options(opts)?, 55 | }) 56 | } 57 | /// Creates a new client for this benchmarking engine 58 | async fn create_client(&self) -> Result { 59 | let db = self.client.database_with_options( 60 | "crud-bench", 61 | DatabaseOptions::builder() 62 | // Configure the write concern options 63 | .write_concern( 64 | // Configure the write options 65 | WriteConcern::builder() 66 | // Ensure that all writes are written, 67 | // replicated, and acknowledged by the 68 | // majority of nodes in the cluster. 69 | .w(Acknowledgment::Majority) 70 | // Ensure that all writes are written 71 | // to the journal before being being 72 | // acknowledged back to the client. 73 | // MongoDB does not actually write the 74 | // journal file to disk synchronously 75 | // but instead the operation is only 76 | // acknowledged once the operation 77 | // is written to the journal in memory. 78 | .journal(true) 79 | // Finalise the write options 80 | .build(), 81 | ) 82 | // Configure the read concern options 83 | .read_concern(ReadConcern::majority()) 84 | // Finalise the database configuration 85 | .build(), 86 | ); 87 | Ok(MongoDBClient { 88 | sync: self.sync, 89 | db, 90 | }) 91 | } 92 | } 93 | 94 | pub(crate) struct MongoDBClient { 95 | sync: bool, 96 | db: Database, 97 | } 98 | 99 | impl BenchmarkClient for MongoDBClient { 100 | async fn compact(&self) -> Result<()> { 101 | // For a database compaction 102 | self.db 103 | .run_command(doc! { 104 | "compact": "record", 105 | "dryRun": false, 106 | "force": true, 107 | }) 108 | .await?; 109 | // Ok 110 | Ok(()) 111 | } 112 | 113 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 114 | self.create(key, val).await 115 | } 116 | 117 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 118 | self.create(key, val).await 119 | } 120 | 121 | async fn read_u32(&self, key: u32) -> Result<()> { 122 | let doc = self.read(key).await?; 123 | assert_eq!(doc.unwrap().get("_id").unwrap().as_i64().unwrap() as u32, key); 124 | Ok(()) 125 | } 126 | 127 | async fn read_string(&self, key: String) -> Result<()> { 128 | let doc = self.read(&key).await?; 129 | assert_eq!(doc.unwrap().get_str("_id")?, key); 130 | Ok(()) 131 | } 132 | 133 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 134 | self.update(key, val).await 135 | } 136 | 137 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 138 | self.update(key, val).await 139 | } 140 | 141 | async fn delete_u32(&self, key: u32) -> Result<()> { 142 | self.delete(key).await 143 | } 144 | 145 | async fn delete_string(&self, key: String) -> Result<()> { 146 | self.delete(key).await 147 | } 148 | 149 | async fn scan_u32(&self, scan: &Scan) -> Result { 150 | self.scan(scan).await 151 | } 152 | 153 | async fn scan_string(&self, scan: &Scan) -> Result { 154 | self.scan(scan).await 155 | } 156 | } 157 | 158 | impl MongoDBClient { 159 | fn collection(&self) -> Collection { 160 | self.db.collection("record") 161 | } 162 | 163 | fn to_doc(key: K, mut val: Value) -> Result 164 | where 165 | K: Into + Into, 166 | { 167 | let obj = val.as_object_mut().unwrap(); 168 | obj.insert("_id".to_string(), key.into()); 169 | Ok(bson::to_bson(&val)?) 170 | } 171 | 172 | async fn sync(&self) -> Result<()> { 173 | if self.sync { 174 | // If we need to ensure that writes are 175 | // synced to disk before being acknowledged 176 | // then we need to specifically call `fsync` 177 | // once we have written to the database. 178 | self.db.run_command(doc! { "fsync": 1 }).await?; 179 | } 180 | Ok(()) 181 | } 182 | 183 | async fn create(&self, key: K, val: Value) -> Result<()> 184 | where 185 | K: Into + Into, 186 | { 187 | let bson = Self::to_doc(key, val)?; 188 | let doc = bson.as_document().unwrap(); 189 | let res = self.collection().insert_one(doc).await?; 190 | assert_ne!(res.inserted_id, Bson::Null); 191 | self.sync().await?; 192 | Ok(()) 193 | } 194 | 195 | async fn read(&self, key: K) -> Result> 196 | where 197 | K: Into, 198 | { 199 | let filter = doc! { "_id": key }; 200 | let doc = self.collection().find_one(filter).await?; 201 | assert!(doc.is_some()); 202 | Ok(doc) 203 | } 204 | 205 | async fn update(&self, key: K, val: Value) -> Result<()> 206 | where 207 | K: Into + Into + Clone, 208 | { 209 | let bson = Self::to_doc(key.clone(), val)?; 210 | let doc = bson.as_document().unwrap(); 211 | let filter = doc! { "_id": key }; 212 | let res = self.collection().replace_one(filter, doc).await?; 213 | assert_eq!(res.modified_count, 1); 214 | self.sync().await?; 215 | Ok(()) 216 | } 217 | 218 | async fn delete(&self, key: K) -> Result<()> 219 | where 220 | K: Into, 221 | { 222 | let filter = doc! { "_id": key }; 223 | let res = self.collection().delete_one(filter).await?; 224 | assert_eq!(res.deleted_count, 1); 225 | self.sync().await?; 226 | Ok(()) 227 | } 228 | 229 | async fn scan(&self, scan: &Scan) -> Result { 230 | // Contional scans are not supported 231 | if scan.condition.is_some() { 232 | bail!(NOT_SUPPORTED_ERROR); 233 | } 234 | // Extract parameters 235 | let s = scan.start.unwrap_or(0); 236 | let l = scan.limit.unwrap_or(i64::MAX as usize); 237 | let p = scan.projection()?; 238 | // Consume documents function 239 | let consume = |mut cursor: Cursor| async move { 240 | let mut count = 0; 241 | while let Some(doc) = cursor.try_next().await? { 242 | black_box(doc); 243 | count += 1; 244 | } 245 | Ok(count) 246 | }; 247 | // Perform the relevant projection scan type 248 | match p { 249 | Projection::Id => { 250 | let cursor = self 251 | .collection() 252 | .find(doc! {}) 253 | .skip(s as u64) 254 | .limit(l as i64) 255 | .projection(doc! { "_id": 1 }) 256 | .await?; 257 | consume(cursor).await 258 | } 259 | Projection::Full => { 260 | let cursor = self.collection().find(doc! {}).skip(s as u64).limit(l as i64).await?; 261 | consume(cursor).await 262 | } 263 | Projection::Count => { 264 | let pipeline = vec![ 265 | doc! { "$skip": s as i64 }, 266 | doc! { "$limit": l as i64 }, 267 | doc! { "$count": "count" }, 268 | ]; 269 | let mut cursor = self.collection().aggregate(pipeline).await?; 270 | if let Some(result) = cursor.next().await { 271 | let doc: Document = result?; 272 | let count = doc.get_i32("count").unwrap_or(0); 273 | Ok(count as usize) 274 | } else { 275 | bail!("No row returned"); 276 | } 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/mysql.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "mysql")] 2 | 3 | use crate::dialect::{Dialect, MySqlDialect}; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::{ColumnType, Columns}; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::Result; 9 | use mysql_async::consts; 10 | use mysql_async::prelude::Queryable; 11 | use mysql_async::prelude::ToValue; 12 | use mysql_async::{Conn, Opts, Row}; 13 | use serde_json::{Map, Value}; 14 | use std::hint::black_box; 15 | use std::sync::Arc; 16 | use tokio::sync::Mutex; 17 | 18 | pub const DEFAULT: &str = "mysql://root:mysql@127.0.0.1:3306/bench"; 19 | 20 | pub(crate) const fn docker(options: &Benchmark) -> DockerParams { 21 | DockerParams { 22 | image: "mysql", 23 | pre_args: "--ulimit nofile=65536:65536 -p 127.0.0.1:3306:3306 -e MYSQL_ROOT_HOST=% -e MYSQL_ROOT_PASSWORD=mysql -e MYSQL_DATABASE=bench", 24 | post_args: match options.sync { 25 | true => "--max-connections=1024 --innodb-buffer-pool-size=16G --innodb-buffer-pool-instances=32 --sync_binlog=1 --innodb-flush-log-at-trx-commit=1", 26 | false => "--max-connections=1024 --innodb-buffer-pool-size=16G --innodb-buffer-pool-instances=32 --sync_binlog=0 --innodb-flush-log-at-trx-commit=0", 27 | } 28 | } 29 | } 30 | 31 | pub(crate) struct MysqlClientProvider(KeyType, Columns, String); 32 | 33 | impl BenchmarkEngine for MysqlClientProvider { 34 | /// Initiates a new datastore benchmarking engine 35 | async fn setup(kt: KeyType, columns: Columns, options: &Benchmark) -> Result { 36 | // Get the custom endpoint if specified 37 | let url = options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(); 38 | // Create the client provider 39 | Ok(Self(kt, columns, url)) 40 | } 41 | /// Creates a new client for this benchmarking engine 42 | async fn create_client(&self) -> Result { 43 | let conn = Conn::new(Opts::from_url(&self.2)?).await?; 44 | Ok(MysqlClient { 45 | conn: Arc::new(Mutex::new(conn)), 46 | kt: self.0, 47 | columns: self.1.clone(), 48 | }) 49 | } 50 | } 51 | 52 | pub(crate) struct MysqlClient { 53 | conn: Arc>, 54 | kt: KeyType, 55 | columns: Columns, 56 | } 57 | 58 | impl BenchmarkClient for MysqlClient { 59 | async fn startup(&self) -> Result<()> { 60 | let id_type = match self.kt { 61 | KeyType::Integer => "SERIAL", 62 | KeyType::String26 => "VARCHAR(26)", 63 | KeyType::String90 => "VARCHAR(90)", 64 | KeyType::String250 => "VARCHAR(250)", 65 | KeyType::String506 => "VARCHAR(506)", 66 | KeyType::Uuid => { 67 | todo!() 68 | } 69 | }; 70 | let fields = self 71 | .columns 72 | .0 73 | .iter() 74 | .map(|(n, t)| { 75 | let n = MySqlDialect::escape_field(n.clone()); 76 | match t { 77 | ColumnType::String => format!("{n} TEXT NOT NULL"), 78 | ColumnType::Integer => format!("{n} INTEGER NOT NULL"), 79 | ColumnType::Object => format!("{n} JSON NOT NULL"), 80 | ColumnType::Float => format!("{n} REAL NOT NULL"), 81 | ColumnType::DateTime => format!("{n} TIMESTAMP NOT NULL"), 82 | ColumnType::Uuid => format!("{n} UUID NOT NULL"), 83 | ColumnType::Bool => format!("{n} BOOL NOT NULL"), 84 | } 85 | }) 86 | .collect::>() 87 | .join(", "); 88 | let stm = 89 | format!("DROP TABLE IF EXISTS record; CREATE TABLE record ( id {id_type} PRIMARY KEY, {fields}) ENGINE=InnoDB;"); 90 | self.conn.lock().await.query_drop(&stm).await?; 91 | Ok(()) 92 | } 93 | 94 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 95 | self.create(key as u64, val).await 96 | } 97 | 98 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 99 | self.create(key, val).await 100 | } 101 | 102 | async fn read_u32(&self, key: u32) -> Result<()> { 103 | self.read(key as u64).await 104 | } 105 | 106 | async fn read_string(&self, key: String) -> Result<()> { 107 | self.read(key).await 108 | } 109 | 110 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 111 | self.update(key as u64, val).await 112 | } 113 | 114 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 115 | self.update(key, val).await 116 | } 117 | 118 | async fn delete_u32(&self, key: u32) -> Result<()> { 119 | self.delete(key as u64).await 120 | } 121 | 122 | async fn delete_string(&self, key: String) -> Result<()> { 123 | self.delete(key).await 124 | } 125 | 126 | async fn scan_u32(&self, scan: &Scan) -> Result { 127 | self.scan(scan).await 128 | } 129 | 130 | async fn scan_string(&self, scan: &Scan) -> Result { 131 | self.scan(scan).await 132 | } 133 | } 134 | 135 | impl MysqlClient { 136 | fn consume(&self, mut row: Row) -> Result { 137 | let mut val: Map = Map::new(); 138 | // 139 | for (i, c) in row.columns().iter().enumerate() { 140 | val.insert( 141 | c.name_str().to_string(), 142 | match c.column_type() { 143 | consts::ColumnType::MYSQL_TYPE_TINY => { 144 | let v: Option = row.take(i); 145 | Value::from(v) 146 | } 147 | consts::ColumnType::MYSQL_TYPE_SHORT => { 148 | let v: Option = row.take(i); 149 | Value::from(v) 150 | } 151 | consts::ColumnType::MYSQL_TYPE_VARCHAR => { 152 | let v: Option = row.take(i); 153 | Value::from(v) 154 | } 155 | consts::ColumnType::MYSQL_TYPE_VAR_STRING => { 156 | let v: Option = row.take(i); 157 | Value::from(v) 158 | } 159 | consts::ColumnType::MYSQL_TYPE_STRING => { 160 | let v: Option = row.take(i); 161 | Value::from(v) 162 | } 163 | consts::ColumnType::MYSQL_TYPE_LONG => { 164 | let v: Option = row.take(i); 165 | Value::from(v) 166 | } 167 | consts::ColumnType::MYSQL_TYPE_LONGLONG => { 168 | let v: Option = row.take(i); 169 | Value::from(v) 170 | } 171 | consts::ColumnType::MYSQL_TYPE_FLOAT => { 172 | let v: Option = row.take(i); 173 | Value::from(v) 174 | } 175 | consts::ColumnType::MYSQL_TYPE_DOUBLE => { 176 | let v: Option = row.take(i); 177 | Value::from(v) 178 | } 179 | consts::ColumnType::MYSQL_TYPE_BLOB => { 180 | let v: Option = row.take(i); 181 | Value::from(v) 182 | } 183 | consts::ColumnType::MYSQL_TYPE_JSON => { 184 | let v: Option = row.take(i); 185 | Value::from(v) 186 | } 187 | c => { 188 | todo!("Not yet implemented {c:?}") 189 | } 190 | }, 191 | ); 192 | } 193 | Ok(val.into()) 194 | } 195 | 196 | async fn create(&self, key: T, val: Value) -> Result<()> 197 | where 198 | T: ToValue + Sync, 199 | { 200 | let (fields, values) = MySqlDialect::create_clause(&self.columns, val); 201 | let stm = format!("INSERT INTO record (id, {fields}) VALUES (?, {values})"); 202 | let _: Vec = self.conn.lock().await.exec(stm, (key.to_value(),)).await?; 203 | Ok(()) 204 | } 205 | 206 | async fn read(&self, key: T) -> Result<()> 207 | where 208 | T: ToValue + Sync, 209 | { 210 | let stm = "SELECT * FROM record WHERE id=?"; 211 | let res: Vec = self.conn.lock().await.exec(stm, (key.to_value(),)).await?; 212 | assert_eq!(res.len(), 1); 213 | black_box(self.consume(res.into_iter().next().unwrap())?); 214 | Ok(()) 215 | } 216 | 217 | async fn update(&self, key: T, val: Value) -> Result<()> 218 | where 219 | T: ToValue + Sync, 220 | { 221 | let fields = MySqlDialect::update_clause(&self.columns, val); 222 | let stm = format!("UPDATE record SET {fields} WHERE id=?"); 223 | let _: Vec = self.conn.lock().await.exec(stm, (key.to_value(),)).await?; 224 | Ok(()) 225 | } 226 | 227 | async fn delete(&self, key: T) -> Result<()> 228 | where 229 | T: ToValue + Sync, 230 | { 231 | let stm = "DELETE FROM record WHERE id=?"; 232 | let _: Vec = self.conn.lock().await.exec(stm, (key.to_value(),)).await?; 233 | Ok(()) 234 | } 235 | 236 | async fn scan(&self, scan: &Scan) -> Result { 237 | // Extract parameters 238 | let s = scan.start.map(|s| format!("OFFSET {}", s)).unwrap_or_default(); 239 | let l = scan.limit.map(|s| format!("LIMIT {}", s)).unwrap_or_default(); 240 | let c = scan.condition.as_ref().map(|s| format!("WHERE {}", s)).unwrap_or_default(); 241 | let p = scan.projection()?; 242 | // Perform the relevant projection scan type 243 | match p { 244 | Projection::Id => { 245 | let stm = format!("SELECT id FROM record {c} {l} {s}"); 246 | let res: Vec = self.conn.lock().await.query(stm).await?; 247 | // We use a for loop to iterate over the results, while 248 | // calling black_box internally. This is necessary as 249 | // an iterator with `filter_map` or `map` is optimised 250 | // out by the compiler when calling `count` at the end. 251 | let mut count = 0; 252 | for v in res { 253 | black_box(self.consume(v).unwrap()); 254 | count += 1; 255 | } 256 | Ok(count) 257 | } 258 | Projection::Full => { 259 | let stm = format!("SELECT * FROM record {c} {l} {s}"); 260 | let res: Vec = self.conn.lock().await.query(stm).await?; 261 | // We use a for loop to iterate over the results, while 262 | // calling black_box internally. This is necessary as 263 | // an iterator with `filter_map` or `map` is optimised 264 | // out by the compiler when calling `count` at the end. 265 | let mut count = 0; 266 | for v in res { 267 | black_box(self.consume(v).unwrap()); 268 | count += 1; 269 | } 270 | Ok(count) 271 | } 272 | Projection::Count => { 273 | let stm = format!("SELECT COUNT(*) FROM (SELECT id FROM record {c} {l} {s}) AS T"); 274 | let res: Vec = self.conn.lock().await.query(stm).await?; 275 | let count: i64 = res.first().unwrap().get(0).unwrap(); 276 | Ok(count as usize) 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/neo4j.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "neo4j")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::dialect::Neo4jDialect; 5 | use crate::docker::DockerParams; 6 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 7 | use crate::valueprovider::Columns; 8 | use crate::{Benchmark, KeyType, Projection, Scan}; 9 | use anyhow::{bail, Result}; 10 | use neo4rs::query; 11 | use neo4rs::BoltType; 12 | use neo4rs::ConfigBuilder; 13 | use neo4rs::Graph; 14 | use serde_json::Value; 15 | use std::hint::black_box; 16 | 17 | pub const DEFAULT: &str = "127.0.0.1:7687"; 18 | 19 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 20 | DockerParams { 21 | image: "neo4j", 22 | pre_args: "--ulimit nofile=65536:65536 -p 127.0.0.1:7474:7474 -p 127.0.0.1:7687:7687 -e NEO4J_AUTH=none", 23 | post_args: "", 24 | } 25 | } 26 | 27 | pub(crate) struct Neo4jClientProvider { 28 | graph: Graph, 29 | } 30 | 31 | impl BenchmarkEngine for Neo4jClientProvider { 32 | /// Initiates a new datastore benchmarking engine 33 | async fn setup(_kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 34 | // Get the custom endpoint if specified 35 | let url = options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(); 36 | // Create a new client with a connection pool. 37 | // The Neo4j client supports connection pooling 38 | // and the recommended advice is to use a single 39 | // graph connection and share that with all async 40 | // tasks. Therefore we create a single connection 41 | // pool and share it with all of the crud-bench 42 | // clients. The Neo4j driver correctly limits the 43 | // number of connections to the number specified 44 | // in the `max_connections` option. 45 | let config = ConfigBuilder::default() 46 | .uri(url) 47 | .db("neo4j") 48 | .user("neo4j") 49 | .password("neo4j") 50 | .fetch_size(500) 51 | .max_connections(options.clients as usize) 52 | .build()?; 53 | // Create the client 54 | Ok(Self { 55 | graph: Graph::connect(config).await?, 56 | }) 57 | } 58 | /// Creates a new client for this benchmarking engine 59 | async fn create_client(&self) -> Result { 60 | Ok(Neo4jClient { 61 | graph: self.graph.clone(), 62 | }) 63 | } 64 | } 65 | 66 | pub(crate) struct Neo4jClient { 67 | graph: Graph, 68 | } 69 | 70 | impl BenchmarkClient for Neo4jClient { 71 | async fn startup(&self) -> Result<()> { 72 | let stm = "CREATE INDEX FOR (r:Record) ON (r.id);"; 73 | self.graph.execute(query(stm)).await?.next().await.ok(); 74 | Ok(()) 75 | } 76 | 77 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 78 | self.create(key, val).await 79 | } 80 | 81 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 82 | self.create(key, val).await 83 | } 84 | 85 | async fn read_u32(&self, key: u32) -> Result<()> { 86 | self.read(key).await 87 | } 88 | 89 | async fn read_string(&self, key: String) -> Result<()> { 90 | self.read(key).await 91 | } 92 | 93 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 94 | self.update(key, val).await 95 | } 96 | 97 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 98 | self.update(key, val).await 99 | } 100 | 101 | async fn delete_u32(&self, key: u32) -> Result<()> { 102 | self.delete(key).await 103 | } 104 | 105 | async fn delete_string(&self, key: String) -> Result<()> { 106 | self.delete(key).await 107 | } 108 | 109 | async fn scan_u32(&self, scan: &Scan) -> Result { 110 | self.scan(scan).await 111 | } 112 | 113 | async fn scan_string(&self, scan: &Scan) -> Result { 114 | self.scan(scan).await 115 | } 116 | } 117 | 118 | impl Neo4jClient { 119 | async fn create(&self, key: T, val: Value) -> Result<()> 120 | where 121 | T: Into + Sync, 122 | { 123 | let fields = Neo4jDialect::create_clause(val)?; 124 | let stm = format!("CREATE (r:Record {{ id: $id, {fields} }}) RETURN r.id"); 125 | let stm = query(&stm).param("id", key); 126 | let mut res = self.graph.execute(stm).await.unwrap(); 127 | assert!(matches!(res.next().await, Ok(Some(_)))); 128 | assert!(matches!(res.next().await, Ok(None))); 129 | Ok(()) 130 | } 131 | 132 | async fn read(&self, key: T) -> Result<()> 133 | where 134 | T: Into + Sync, 135 | { 136 | let stm = "MATCH (r:Record { id: $id }) RETURN r"; 137 | let stm = query(stm).param("id", key); 138 | let mut res = self.graph.execute(stm).await.unwrap(); 139 | assert!(matches!(black_box(res.next().await), Ok(Some(_)))); 140 | assert!(matches!(res.next().await, Ok(None))); 141 | Ok(()) 142 | } 143 | 144 | async fn update(&self, key: T, val: Value) -> Result<()> 145 | where 146 | T: Into + Sync, 147 | { 148 | let fields = Neo4jDialect::update_clause(val)?; 149 | let stm = format!("MATCH (r:Record {{ id: $id }}) SET {fields} RETURN r.id"); 150 | let stm = query(&stm).param("id", key); 151 | let mut res = self.graph.execute(stm).await.unwrap(); 152 | assert!(matches!(res.next().await, Ok(Some(_)))); 153 | assert!(matches!(res.next().await, Ok(None))); 154 | Ok(()) 155 | } 156 | 157 | async fn delete(&self, key: T) -> Result<()> 158 | where 159 | T: Into + Sync, 160 | { 161 | let stm = "MATCH (r:Record { id: $id }) WITH r, r.id AS id DETACH DELETE r RETURN id"; 162 | let stm = query(stm).param("id", key); 163 | let mut res = self.graph.execute(stm).await.unwrap(); 164 | assert!(matches!(res.next().await, Ok(Some(_)))); 165 | assert!(matches!(res.next().await, Ok(None))); 166 | Ok(()) 167 | } 168 | 169 | async fn scan(&self, scan: &Scan) -> Result { 170 | // Contional scans are not supported 171 | if scan.condition.is_some() { 172 | bail!(NOT_SUPPORTED_ERROR); 173 | } 174 | // Extract parameters 175 | let s = scan.start.unwrap_or(0); 176 | let l = scan.limit.unwrap_or(i64::MAX as usize); 177 | let p = scan.projection()?; 178 | // Perform the relevant projection scan type 179 | match p { 180 | Projection::Id => { 181 | let stm = format!("MATCH (r) SKIP {s} LIMIT {l} RETURN r.id"); 182 | let mut res = self.graph.execute(query(&stm)).await.unwrap(); 183 | let mut count = 0; 184 | while let Ok(Some(v)) = res.next().await { 185 | black_box(v); 186 | count += 1; 187 | } 188 | Ok(count) 189 | } 190 | Projection::Full => { 191 | let stm = format!("MATCH (r) SKIP {s} LIMIT {l} RETURN r"); 192 | let mut res = self.graph.execute(query(&stm)).await.unwrap(); 193 | let mut count = 0; 194 | while let Ok(Some(v)) = res.next().await { 195 | black_box(v); 196 | count += 1; 197 | } 198 | Ok(count) 199 | } 200 | Projection::Count => { 201 | let stm = format!("MATCH (r) SKIP {s} LIMIT {l} RETURN count(r) as count"); 202 | let mut res = self.graph.execute(query(&stm)).await.unwrap(); 203 | let count = res.next().await.unwrap().unwrap().get("count").unwrap(); 204 | Ok(count) 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/postgres.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "postgres")] 2 | 3 | use crate::dialect::{AnsiSqlDialect, Dialect}; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::{ColumnType, Columns}; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::Result; 9 | use serde_json::{Map, Value}; 10 | use std::hint::black_box; 11 | use tokio_postgres::types::{Json, ToSql}; 12 | use tokio_postgres::{Client, NoTls, Row}; 13 | 14 | pub const DEFAULT: &str = "host=127.0.0.1 user=postgres password=postgres"; 15 | 16 | pub(crate) const fn docker(options: &Benchmark) -> DockerParams { 17 | DockerParams { 18 | image: "postgres", 19 | pre_args: 20 | "--ulimit nofile=65536:65536 -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=postgres", 21 | post_args: match options.sync { 22 | true => "postgres -N 1024 -c fsync=on -c synchronous_commit=on", 23 | false => "postgres -N 1024 -c fsync=on -c synchronous_commit=off", 24 | }, 25 | } 26 | } 27 | 28 | pub(crate) struct PostgresClientProvider(KeyType, Columns, String); 29 | 30 | impl BenchmarkEngine for PostgresClientProvider { 31 | /// Initiates a new datastore benchmarking engine 32 | async fn setup(kt: KeyType, columns: Columns, options: &Benchmark) -> Result { 33 | // Get the custom endpoint if specified 34 | let url = options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(); 35 | // Create the client provider 36 | Ok(Self(kt, columns, url)) 37 | } 38 | /// Creates a new client for this benchmarking engine 39 | async fn create_client(&self) -> Result { 40 | // Connect to the database with TLS disabled 41 | let (client, connection) = tokio_postgres::connect(&self.2, NoTls).await?; 42 | // Log any errors when the connection is closed 43 | tokio::spawn(async move { 44 | if let Err(e) = connection.await { 45 | eprintln!("connection error: {}", e); 46 | } 47 | }); 48 | // Create the client 49 | Ok(PostgresClient { 50 | client, 51 | kt: self.0, 52 | columns: self.1.clone(), 53 | }) 54 | } 55 | } 56 | 57 | pub(crate) struct PostgresClient { 58 | client: Client, 59 | kt: KeyType, 60 | columns: Columns, 61 | } 62 | 63 | impl BenchmarkClient for PostgresClient { 64 | async fn startup(&self) -> Result<()> { 65 | let id_type = match self.kt { 66 | KeyType::Integer => "SERIAL", 67 | KeyType::String26 => "VARCHAR(26)", 68 | KeyType::String90 => "VARCHAR(90)", 69 | KeyType::String250 => "VARCHAR(250)", 70 | KeyType::String506 => "VARCHAR(506)", 71 | KeyType::Uuid => { 72 | todo!() 73 | } 74 | }; 75 | let fields = self 76 | .columns 77 | .0 78 | .iter() 79 | .map(|(n, t)| { 80 | let n = AnsiSqlDialect::escape_field(n.clone()); 81 | match t { 82 | ColumnType::String => format!("{n} TEXT NOT NULL"), 83 | ColumnType::Integer => format!("{n} INTEGER NOT NULL"), 84 | ColumnType::Object => format!("{n} JSONB NOT NULL"), 85 | ColumnType::Float => format!("{n} REAL NOT NULL"), 86 | ColumnType::DateTime => format!("{n} TIMESTAMP NOT NULL"), 87 | ColumnType::Uuid => format!("{n} UUID NOT NULL"), 88 | ColumnType::Bool => format!("{n} BOOL NOT NULL"), 89 | } 90 | }) 91 | .collect::>() 92 | .join(", "); 93 | let stm = format!("DROP TABLE IF EXISTS record; CREATE TABLE record ( id {id_type} PRIMARY KEY, {fields});"); 94 | self.client.batch_execute(&stm).await?; 95 | Ok(()) 96 | } 97 | 98 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 99 | self.create(key as i32, val).await 100 | } 101 | 102 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 103 | self.create(key, val).await 104 | } 105 | 106 | async fn read_u32(&self, key: u32) -> Result<()> { 107 | self.read(key as i32).await 108 | } 109 | 110 | async fn read_string(&self, key: String) -> Result<()> { 111 | self.read(key).await 112 | } 113 | 114 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 115 | self.update(key as i32, val).await 116 | } 117 | 118 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 119 | self.update(key, val).await 120 | } 121 | 122 | async fn delete_u32(&self, key: u32) -> Result<()> { 123 | self.delete(key as i32).await 124 | } 125 | 126 | async fn delete_string(&self, key: String) -> Result<()> { 127 | self.delete(key).await 128 | } 129 | 130 | async fn scan_u32(&self, scan: &Scan) -> Result { 131 | self.scan(scan).await 132 | } 133 | 134 | async fn scan_string(&self, scan: &Scan) -> Result { 135 | self.scan(scan).await 136 | } 137 | } 138 | 139 | impl PostgresClient { 140 | fn consume(&self, row: Row, columns: bool) -> Result { 141 | let mut val: Map = Map::new(); 142 | match self.kt { 143 | KeyType::Integer => { 144 | let v: i32 = row.try_get("id")?; 145 | val.insert("id".into(), Value::from(v)); 146 | } 147 | KeyType::String26 | KeyType::String90 | KeyType::String250 | KeyType::String506 => { 148 | let v: String = row.try_get("id")?; 149 | val.insert("id".into(), Value::from(v)); 150 | } 151 | KeyType::Uuid => { 152 | let v: uuid::Uuid = row.try_get("id")?; 153 | val.insert("id".into(), Value::from(v.to_string())); 154 | } 155 | } 156 | if columns { 157 | for (n, t) in self.columns.0.iter() { 158 | val.insert( 159 | n.clone(), 160 | match t { 161 | ColumnType::Bool => { 162 | let v: bool = row.try_get(n.as_str())?; 163 | Value::from(v) 164 | } 165 | ColumnType::Float => { 166 | let v: f64 = row.try_get(n.as_str())?; 167 | Value::from(v) 168 | } 169 | ColumnType::Integer => { 170 | let v: i32 = row.try_get(n.as_str())?; 171 | Value::from(v) 172 | } 173 | ColumnType::String => { 174 | let v: String = row.try_get(n.as_str())?; 175 | Value::from(v) 176 | } 177 | ColumnType::DateTime => { 178 | let v: String = row.try_get(n.as_str())?; 179 | Value::from(v) 180 | } 181 | ColumnType::Uuid => { 182 | let v: uuid::Uuid = row.try_get(n.as_str())?; 183 | Value::from(v.to_string()) 184 | } 185 | ColumnType::Object => { 186 | let v: Json = row.try_get(n.as_str())?; 187 | v.0 188 | } 189 | }, 190 | ); 191 | } 192 | } 193 | Ok(val.into()) 194 | } 195 | 196 | async fn create(&self, key: T, val: Value) -> Result<()> 197 | where 198 | T: ToSql + Sync, 199 | { 200 | let (fields, values) = AnsiSqlDialect::create_clause(&self.columns, val); 201 | let stm = format!("INSERT INTO record (id, {fields}) VALUES ($1, {values})"); 202 | let res = self.client.execute(&stm, &[&key]).await?; 203 | assert_eq!(res, 1); 204 | Ok(()) 205 | } 206 | 207 | async fn read(&self, key: T) -> Result<()> 208 | where 209 | T: ToSql + Sync, 210 | { 211 | let stm = "SELECT * FROM record WHERE id=$1"; 212 | let res = self.client.query(stm, &[&key]).await?; 213 | assert_eq!(res.len(), 1); 214 | black_box(self.consume(res.into_iter().next().unwrap(), true)?); 215 | Ok(()) 216 | } 217 | 218 | async fn update(&self, key: T, val: Value) -> Result<()> 219 | where 220 | T: ToSql + Sync, 221 | { 222 | let fields = AnsiSqlDialect::update_clause(&self.columns, val); 223 | let stm = format!("UPDATE record SET {fields} WHERE id=$1"); 224 | let res = self.client.execute(&stm, &[&key]).await?; 225 | assert_eq!(res, 1); 226 | Ok(()) 227 | } 228 | 229 | async fn delete(&self, key: T) -> Result<()> 230 | where 231 | T: ToSql + Sync, 232 | { 233 | let stm = "DELETE FROM record WHERE id=$1"; 234 | let res = self.client.execute(stm, &[&key]).await?; 235 | assert_eq!(res, 1); 236 | Ok(()) 237 | } 238 | 239 | async fn scan(&self, scan: &Scan) -> Result { 240 | // Extract parameters 241 | let s = scan.start.map(|s| format!("OFFSET {}", s)).unwrap_or_default(); 242 | let l = scan.limit.map(|s| format!("LIMIT {}", s)).unwrap_or_default(); 243 | let c = scan.condition.as_ref().map(|s| format!("WHERE {}", s)).unwrap_or_default(); 244 | let p = scan.projection()?; 245 | // Perform the relevant projection scan type 246 | match p { 247 | Projection::Id => { 248 | let stm = format!("SELECT id FROM record {c} {l} {s}"); 249 | let res = self.client.query(&stm, &[]).await?; 250 | // We use a for loop to iterate over the results, while 251 | // calling black_box internally. This is necessary as 252 | // an iterator with `filter_map` or `map` is optimised 253 | // out by the compiler when calling `count` at the end. 254 | let mut count = 0; 255 | for v in res { 256 | black_box(self.consume(v, false).unwrap()); 257 | count += 1; 258 | } 259 | Ok(count) 260 | } 261 | Projection::Full => { 262 | let stm = format!("SELECT * FROM record {c} {l} {s}"); 263 | let res = self.client.query(&stm, &[]).await?; 264 | // We use a for loop to iterate over the results, while 265 | // calling black_box internally. This is necessary as 266 | // an iterator with `filter_map` or `map` is optimised 267 | // out by the compiler when calling `count` at the end. 268 | let mut count = 0; 269 | for v in res { 270 | black_box(self.consume(v, true).unwrap()); 271 | count += 1; 272 | } 273 | Ok(count) 274 | } 275 | Projection::Count => { 276 | let stm = format!("SELECT COUNT(*) FROM (SELECT id FROM record {c} {l} {s})"); 277 | let res = self.client.query(&stm, &[]).await?; 278 | let count: i64 = res.first().unwrap().get(0); 279 | Ok(count as usize) 280 | } 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/profiling.rs: -------------------------------------------------------------------------------- 1 | use pprof::protos::Message; 2 | use pprof::ProfilerGuard; 3 | use pprof::ProfilerGuardBuilder; 4 | use std::io::Write; 5 | use std::sync::OnceLock; 6 | 7 | static PROFILER: OnceLock> = OnceLock::new(); 8 | 9 | pub(crate) fn initialise() { 10 | PROFILER.get_or_init(|| { 11 | ProfilerGuardBuilder::default() 12 | .frequency(1000) 13 | .blocklist(&["libc", "libgcc", "pthread", "vdso"]) 14 | .build() 15 | .unwrap() 16 | }); 17 | } 18 | 19 | pub(crate) fn process() { 20 | if let Some(guard) = PROFILER.get() { 21 | if let Ok(report) = guard.report().build() { 22 | // Output a flamegraph 23 | let file = std::fs::File::create("flamegraph.svg").unwrap(); 24 | report.flamegraph(file).unwrap(); 25 | // Output a pprof 26 | let mut file = std::fs::File::create("profile.pb").unwrap(); 27 | let profile = report.pprof().unwrap(); 28 | let mut content = Vec::new(); 29 | profile.encode(&mut content).unwrap(); 30 | file.write_all(&content).unwrap(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/redb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "redb")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use redb::{Database, Durability, ReadableTable, TableDefinition}; 9 | use serde_json::Value; 10 | use std::cmp::max; 11 | use std::hint::black_box; 12 | use std::sync::Arc; 13 | use std::time::Duration; 14 | use sysinfo::System; 15 | 16 | const DATABASE_DIR: &str = "redb"; 17 | 18 | const MIN_CACHE_SIZE: u64 = 512 * 1024 * 1024; 19 | 20 | const TABLE: TableDefinition<&[u8], Vec> = TableDefinition::new("test"); 21 | 22 | pub(crate) struct ReDBClientProvider(Arc); 23 | 24 | impl BenchmarkEngine for ReDBClientProvider { 25 | /// The number of seconds to wait before connecting 26 | fn wait_timeout(&self) -> Option { 27 | None 28 | } 29 | /// Initiates a new datastore benchmarking engine 30 | async fn setup(_kt: KeyType, _columns: Columns, _options: &Benchmark) -> Result { 31 | // Cleanup the data directory 32 | std::fs::remove_file(DATABASE_DIR).ok(); 33 | // Load the system attributes 34 | let system = System::new_all(); 35 | // Get the total system memory 36 | let memory = system.total_memory(); 37 | // Calculate a good cache memory size 38 | let memory = max(memory / 2, MIN_CACHE_SIZE); 39 | // Configure and create the database 40 | let db = Database::builder() 41 | // Set the cache size to 512 MiB 42 | .set_cache_size(memory as usize) 43 | // Create the database directory 44 | .create(DATABASE_DIR)?; 45 | // Create the store 46 | Ok(Self(Arc::new(db))) 47 | } 48 | /// Creates a new client for this benchmarking engine 49 | async fn create_client(&self) -> Result { 50 | Ok(ReDBClient { 51 | db: self.0.clone(), 52 | }) 53 | } 54 | } 55 | 56 | pub(crate) struct ReDBClient { 57 | db: Arc, 58 | } 59 | 60 | impl BenchmarkClient for ReDBClient { 61 | async fn shutdown(&self) -> Result<()> { 62 | // Cleanup the data directory 63 | std::fs::remove_file(DATABASE_DIR).ok(); 64 | // Ok 65 | Ok(()) 66 | } 67 | 68 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 69 | self.create_bytes(&key.to_ne_bytes(), val).await 70 | } 71 | 72 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 73 | self.create_bytes(&key.into_bytes(), val).await 74 | } 75 | 76 | async fn read_u32(&self, key: u32) -> Result<()> { 77 | self.read_bytes(&key.to_ne_bytes()).await 78 | } 79 | 80 | async fn read_string(&self, key: String) -> Result<()> { 81 | self.read_bytes(&key.into_bytes()).await 82 | } 83 | 84 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 85 | self.update_bytes(&key.to_ne_bytes(), val).await 86 | } 87 | 88 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 89 | self.update_bytes(&key.into_bytes(), val).await 90 | } 91 | 92 | async fn delete_u32(&self, key: u32) -> Result<()> { 93 | self.delete_bytes(&key.to_ne_bytes()).await 94 | } 95 | 96 | async fn delete_string(&self, key: String) -> Result<()> { 97 | self.delete_bytes(&key.into_bytes()).await 98 | } 99 | 100 | async fn scan_u32(&self, scan: &Scan) -> Result { 101 | self.scan_bytes(scan).await 102 | } 103 | 104 | async fn scan_string(&self, scan: &Scan) -> Result { 105 | self.scan_bytes(scan).await 106 | } 107 | } 108 | 109 | impl ReDBClient { 110 | async fn create_bytes(&self, key: &[u8], val: Value) -> Result<()> { 111 | // Clone the datastore 112 | let db = self.db.clone(); 113 | // Execute on the blocking threadpool 114 | affinitypool::spawn_local(|| -> Result<_> { 115 | // Serialise the value 116 | let val = bincode::serialize(&val)?; 117 | // Create a new transaction 118 | let mut txn = db.begin_write()?; 119 | // Let the OS handle syncing to disk 120 | txn.set_durability(Durability::Eventual); 121 | // Open the database table 122 | let mut tab = txn.open_table(TABLE)?; 123 | // Process the data 124 | tab.insert(key, val)?; 125 | drop(tab); 126 | txn.commit()?; 127 | Ok(()) 128 | }) 129 | .await 130 | } 131 | 132 | async fn read_bytes(&self, key: &[u8]) -> Result<()> { 133 | // Clone the datastore 134 | let db = self.db.clone(); 135 | // Execute on the blocking threadpool 136 | affinitypool::spawn_local(|| -> Result<_> { 137 | // Create a new transaction 138 | let txn = db.begin_read()?; 139 | // Open the database table 140 | let tab = txn.open_table(TABLE)?; 141 | // Process the data 142 | let res: Option<_> = tab.get(key)?; 143 | // Check the value exists 144 | assert!(res.is_some()); 145 | // Deserialise the value 146 | black_box(res.unwrap().value()); 147 | // All ok 148 | Ok(()) 149 | }) 150 | .await 151 | } 152 | 153 | async fn update_bytes(&self, key: &[u8], val: Value) -> Result<()> { 154 | // Clone the datastore 155 | let db = self.db.clone(); 156 | // Execute on the blocking threadpool 157 | affinitypool::spawn_local(|| -> Result<_> { 158 | // Serialise the value 159 | let val = bincode::serialize(&val)?; 160 | // Create a new transaction 161 | let mut txn = db.begin_write()?; 162 | // Let the OS handle syncing to disk 163 | txn.set_durability(Durability::Eventual); 164 | // Open the database table 165 | let mut tab = txn.open_table(TABLE)?; 166 | // Process the data 167 | tab.insert(key, val)?; 168 | drop(tab); 169 | txn.commit()?; 170 | Ok(()) 171 | }) 172 | .await 173 | } 174 | 175 | async fn delete_bytes(&self, key: &[u8]) -> Result<()> { 176 | // Clone the datastore 177 | let db = self.db.clone(); 178 | // Execute on the blocking threadpool 179 | affinitypool::spawn_local(|| -> Result<_> { 180 | // Create a new transaction 181 | let mut txn = db.begin_write()?; 182 | // Let the OS handle syncing to disk 183 | txn.set_durability(Durability::Eventual); 184 | // Open the database table 185 | let mut tab = txn.open_table(TABLE)?; 186 | // Process the data 187 | tab.remove(key)?; 188 | drop(tab); 189 | txn.commit()?; 190 | Ok(()) 191 | }) 192 | .await 193 | } 194 | 195 | async fn scan_bytes(&self, scan: &Scan) -> Result { 196 | // Contional scans are not supported 197 | if scan.condition.is_some() { 198 | bail!(NOT_SUPPORTED_ERROR); 199 | } 200 | // Extract parameters 201 | let s = scan.start.unwrap_or(0); 202 | let l = scan.limit.unwrap_or(usize::MAX); 203 | let p = scan.projection()?; 204 | // Clone the datastore 205 | let db = self.db.clone(); 206 | // Execute on the blocking threadpool 207 | affinitypool::spawn_local(|| -> Result<_> { 208 | // Create a new transaction 209 | let txn = db.begin_read()?; 210 | // Open the database table 211 | let tab = txn.open_table(TABLE)?; 212 | // Create an iterator starting at the beginning 213 | let iter = tab.iter()?; 214 | // Perform the relevant projection scan type 215 | match p { 216 | Projection::Id => { 217 | // We use a for loop to iterate over the results, while 218 | // calling black_box internally. This is necessary as 219 | // an iterator with `filter_map` or `map` is optimised 220 | // out by the compiler when calling `count` at the end. 221 | let mut count = 0; 222 | for v in iter.skip(s).take(l) { 223 | black_box(v.unwrap().1.value()); 224 | count += 1; 225 | } 226 | Ok(count) 227 | } 228 | Projection::Full => { 229 | // We use a for loop to iterate over the results, while 230 | // calling black_box internally. This is necessary as 231 | // an iterator with `filter_map` or `map` is optimised 232 | // out by the compiler when calling `count` at the end. 233 | let mut count = 0; 234 | for v in iter.skip(s).take(l) { 235 | black_box(v.unwrap().1.value()); 236 | count += 1; 237 | } 238 | Ok(count) 239 | } 240 | Projection::Count => { 241 | Ok(iter 242 | .skip(s) // Skip the first `offset` entries 243 | .take(l) // Take the next `limit` entries 244 | .count()) 245 | } 246 | } 247 | }) 248 | .await 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/redis.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "redis")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::{bail, Result}; 9 | use futures::StreamExt; 10 | use redis::aio::MultiplexedConnection; 11 | use redis::{AsyncCommands, Client, ScanOptions}; 12 | use serde_json::Value; 13 | use std::hint::black_box; 14 | use tokio::sync::Mutex; 15 | 16 | pub const DEFAULT: &str = "redis://:root@127.0.0.1:6379/"; 17 | 18 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 19 | DockerParams { 20 | image: "redis", 21 | pre_args: "-p 127.0.0.1:6379:6379", 22 | post_args: "redis-server --requirepass root", 23 | } 24 | } 25 | 26 | pub(crate) struct RedisClientProvider { 27 | url: String, 28 | } 29 | 30 | impl BenchmarkEngine for RedisClientProvider { 31 | /// Initiates a new datastore benchmarking engine 32 | async fn setup(_kt: KeyType, _columns: Columns, options: &Benchmark) -> Result { 33 | Ok(Self { 34 | url: options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(), 35 | }) 36 | } 37 | /// Creates a new client for this benchmarking engine 38 | async fn create_client(&self) -> Result { 39 | let client = Client::open(self.url.as_str())?; 40 | Ok(RedisClient { 41 | conn_iter: Mutex::new(client.get_multiplexed_async_connection().await?), 42 | conn_record: Mutex::new(client.get_multiplexed_async_connection().await?), 43 | }) 44 | } 45 | } 46 | 47 | pub(crate) struct RedisClient { 48 | conn_iter: Mutex, 49 | conn_record: Mutex, 50 | } 51 | 52 | impl BenchmarkClient for RedisClient { 53 | #[allow(dependency_on_unit_never_type_fallback)] 54 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 55 | let val = bincode::serialize(&val)?; 56 | self.conn_record.lock().await.set(key, val).await?; 57 | Ok(()) 58 | } 59 | 60 | #[allow(dependency_on_unit_never_type_fallback)] 61 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 62 | let val = bincode::serialize(&val)?; 63 | self.conn_record.lock().await.set(key, val).await?; 64 | Ok(()) 65 | } 66 | 67 | async fn read_u32(&self, key: u32) -> Result<()> { 68 | let val: Vec = self.conn_record.lock().await.get(key).await?; 69 | assert!(!val.is_empty()); 70 | black_box(val); 71 | Ok(()) 72 | } 73 | 74 | #[allow(dependency_on_unit_never_type_fallback)] 75 | async fn read_string(&self, key: String) -> Result<()> { 76 | let val: Vec = self.conn_record.lock().await.get(key).await?; 77 | assert!(!val.is_empty()); 78 | black_box(val); 79 | Ok(()) 80 | } 81 | 82 | #[allow(dependency_on_unit_never_type_fallback)] 83 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 84 | let val = bincode::serialize(&val)?; 85 | self.conn_record.lock().await.set(key, val).await?; 86 | Ok(()) 87 | } 88 | 89 | #[allow(dependency_on_unit_never_type_fallback)] 90 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 91 | let val = bincode::serialize(&val)?; 92 | self.conn_record.lock().await.set(key, val).await?; 93 | Ok(()) 94 | } 95 | 96 | #[allow(dependency_on_unit_never_type_fallback)] 97 | async fn delete_u32(&self, key: u32) -> Result<()> { 98 | self.conn_record.lock().await.del(key).await?; 99 | Ok(()) 100 | } 101 | 102 | #[allow(dependency_on_unit_never_type_fallback)] 103 | async fn delete_string(&self, key: String) -> Result<()> { 104 | self.conn_record.lock().await.del(key).await?; 105 | Ok(()) 106 | } 107 | 108 | async fn scan_u32(&self, scan: &Scan) -> Result { 109 | self.scan_bytes(scan).await 110 | } 111 | 112 | async fn scan_string(&self, scan: &Scan) -> Result { 113 | self.scan_bytes(scan).await 114 | } 115 | } 116 | 117 | impl RedisClient { 118 | async fn scan_bytes(&self, scan: &Scan) -> Result { 119 | // Conditional scans are not supported 120 | if scan.condition.is_some() { 121 | bail!(NOT_SUPPORTED_ERROR); 122 | } 123 | // Extract parameters 124 | let s = scan.start.unwrap_or(0); 125 | let l = scan.limit.unwrap_or(usize::MAX); 126 | let p = scan.projection()?; 127 | // Get the two connection types 128 | let mut conn_iter = self.conn_iter.lock().await; 129 | let mut conn_record = self.conn_record.lock().await; 130 | // Configure the scan options for improve iteration 131 | let opts = ScanOptions::default().with_count(5000); 132 | // Create an iterator starting at the beginning 133 | let mut iter = conn_iter.scan_options::(opts).await?.skip(s); 134 | // Perform the relevant projection scan type 135 | match p { 136 | Projection::Id => { 137 | // We use a for loop to iterate over the results, while 138 | // calling black_box internally. This is necessary as 139 | // an iterator with `filter_map` or `map` is optimised 140 | // out by the compiler when calling `count` at the end. 141 | let mut count = 0; 142 | for _ in 0..l { 143 | if let Some(k) = iter.next().await { 144 | black_box(k); 145 | count += 1; 146 | } else { 147 | break; 148 | } 149 | } 150 | Ok(count) 151 | } 152 | Projection::Full => { 153 | // We use a for loop to iterate over the results, while 154 | // calling black_box internally. This is necessary as 155 | // an iterator with `filter_map` or `map` is optimised 156 | // out by the compiler when calling `count` at the end. 157 | let mut count = 0; 158 | while let Some(k) = iter.next().await { 159 | let v: Vec = conn_record.get(k).await?; 160 | black_box(v); 161 | count += 1; 162 | if count >= l { 163 | break; 164 | } 165 | } 166 | Ok(count) 167 | } 168 | Projection::Count => match scan.limit { 169 | // Full count queries are too slow 170 | None => bail!(NOT_SUPPORTED_ERROR), 171 | Some(l) => Ok(iter.take(l).count().await), 172 | }, 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/result.rs: -------------------------------------------------------------------------------- 1 | use bytesize::ByteSize; 2 | use comfy_table::modifiers::UTF8_ROUND_CORNERS; 3 | use comfy_table::presets::UTF8_FULL; 4 | use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table}; 5 | use csv::Writer; 6 | use hdrhistogram::Histogram; 7 | use serde::Serialize; 8 | use serde_json::Value; 9 | use std::fmt::{Display, Formatter}; 10 | use std::process; 11 | use std::time::{Duration, Instant}; 12 | use sysinfo::{ 13 | DiskUsage, LoadAvg, Pid, Process, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, 14 | }; 15 | 16 | #[derive(Serialize)] 17 | pub(crate) struct BenchmarkResult { 18 | pub(crate) creates: Option, 19 | pub(crate) reads: Option, 20 | pub(crate) updates: Option, 21 | pub(crate) scans: Vec<(String, u32, Option)>, 22 | pub(crate) deletes: Option, 23 | pub(crate) sample: Value, 24 | } 25 | 26 | const HEADERS: [&str; 18] = [ 27 | "Test", 28 | "Total time", 29 | "Mean", 30 | "Max", 31 | "99th", 32 | "95th", 33 | "75th", 34 | "50th", 35 | "25th", 36 | "1st", 37 | "Min", 38 | "IQR", 39 | "OPS", 40 | "CPU", 41 | "Memory", 42 | "Reads", 43 | "Writes", 44 | "System load", 45 | ]; 46 | 47 | const SKIP: [&str; 17] = ["-"; 17]; 48 | 49 | impl Display for BenchmarkResult { 50 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 51 | let mut table = Table::new(); 52 | table 53 | .load_preset(UTF8_FULL) 54 | .apply_modifier(UTF8_ROUND_CORNERS) 55 | .set_content_arrangement(ContentArrangement::Dynamic); 56 | // Set the benchmark table header row 57 | let headers = HEADERS.map(|h| Cell::new(h).add_attribute(Attribute::Bold).fg(Color::Blue)); 58 | table.set_header(headers); 59 | // Add the [C]reate results to the output 60 | if let Some(res) = &self.creates { 61 | table.add_row(res.output("[C]reate")); 62 | } 63 | // Add the [R]eads results to the output 64 | if let Some(res) = &self.reads { 65 | table.add_row(res.output("[R]ead")); 66 | } 67 | // Add the [U]pdates results to the output 68 | if let Some(res) = &self.updates { 69 | table.add_row(res.output("[U]pdate")); 70 | } 71 | for (name, samples, result) in &self.scans { 72 | let name = format!("[S]can::{name} ({samples})"); 73 | if let Some(res) = &result { 74 | table.add_row(res.output(name)); 75 | } else { 76 | let mut cells = vec![name]; 77 | cells.extend(SKIP.iter().map(|s| s.to_string())); 78 | table.add_row(cells); 79 | } 80 | } 81 | // Add the [D]eletes results to the output 82 | if let Some(res) = &self.deletes { 83 | table.add_row(res.output("[D]elete")); 84 | } 85 | // Right align the `CPU` column 86 | let column = table.column_mut(13).expect("The table needs at least 14 columns"); 87 | column.set_cell_alignment(CellAlignment::Right); 88 | // Right align the `Memory` column 89 | let column = table.column_mut(14).expect("The table needs at least 15 columns"); 90 | column.set_cell_alignment(CellAlignment::Right); 91 | // Right align the `Reads` column 92 | let column = table.column_mut(15).expect("The table needs at least 16 columns"); 93 | column.set_cell_alignment(CellAlignment::Right); 94 | // Right align the `Writes` column 95 | let column = table.column_mut(16).expect("The table needs at least 17 columns"); 96 | column.set_cell_alignment(CellAlignment::Right); 97 | // Output the formatted table 98 | write!(f, "{table}") 99 | } 100 | } 101 | 102 | impl BenchmarkResult { 103 | pub(crate) fn to_csv(&self, path: &str) -> Result<(), csv::Error> { 104 | let mut w = Writer::from_path(path)?; 105 | 106 | // Write headers 107 | w.write_record(HEADERS)?; 108 | 109 | // Write rows 110 | // Add the [C]reate results to the output 111 | if let Some(res) = &self.creates { 112 | w.write_record(res.output("[C]reate"))?; 113 | } 114 | // Add the [R]eads results to the output 115 | if let Some(res) = &self.reads { 116 | w.write_record(res.output("[R]ead"))?; 117 | } 118 | // Add the [U]pdates results to the output 119 | if let Some(res) = &self.updates { 120 | w.write_record(res.output("[U]pdate"))?; 121 | } 122 | // Add the [S]cans results to the output 123 | for (name, samples, result) in &self.scans { 124 | let name = format!("[S]can::{name} ({samples})"); 125 | if let Some(res) = &result { 126 | w.write_record(res.output(name))?; 127 | } else { 128 | let mut cells = vec![name]; 129 | cells.extend(SKIP.iter().map(|s| s.to_string())); 130 | w.write_record(cells)?; 131 | } 132 | } 133 | // Add the [D]eletes results to the output 134 | if let Some(res) = &self.deletes { 135 | w.write_record(res.output("[D]elete"))?; 136 | } 137 | // Ensure all data is flushed to the file 138 | w.flush()?; 139 | Ok(()) 140 | } 141 | } 142 | 143 | pub(super) struct OperationMetric { 144 | system: System, 145 | pid: Pid, 146 | samples: u32, 147 | start_time: Instant, 148 | initial_disk_usage: DiskUsage, 149 | refresh_kind: ProcessRefreshKind, 150 | } 151 | 152 | impl OperationMetric { 153 | pub(super) fn new(pid: Option, samples: u32) -> Self { 154 | // We collect the PID 155 | let pid = Pid::from(pid.unwrap_or_else(process::id) as usize); 156 | let refresh_kind = ProcessRefreshKind::nothing().with_memory().with_cpu().with_disk_usage(); 157 | let system = 158 | System::new_with_specifics(RefreshKind::nothing().with_processes(refresh_kind)); 159 | let mut metric = Self { 160 | pid, 161 | samples, 162 | system, 163 | start_time: Instant::now(), 164 | initial_disk_usage: DiskUsage::default(), 165 | refresh_kind, 166 | }; 167 | // We collect the disk usage before the test, so we can't subtract it from the count after test 168 | if let Some(process) = metric.collect_process() { 169 | metric.initial_disk_usage = process.disk_usage(); 170 | } 171 | metric.start_time = Instant::now(); 172 | metric 173 | } 174 | 175 | fn collect_process(&mut self) -> Option<&Process> { 176 | self.system.refresh_processes_specifics( 177 | ProcessesToUpdate::Some(&[self.pid]), 178 | true, 179 | self.refresh_kind, 180 | ); 181 | self.system.process(self.pid) 182 | } 183 | } 184 | 185 | #[derive(Serialize)] 186 | pub(super) struct OperationResult { 187 | mean: f64, 188 | min: u64, 189 | max: u64, 190 | q99: u64, 191 | q95: u64, 192 | q75: u64, 193 | q50: u64, 194 | q25: u64, 195 | q01: u64, 196 | iqr: u64, 197 | ops: f64, 198 | elapsed: Duration, 199 | samples: u32, 200 | cpu_usage: f32, 201 | used_memory: u64, 202 | disk_usage: DiskUsage, 203 | load_avg: LoadAvg, 204 | } 205 | 206 | impl OperationResult { 207 | /// Create a new operataion result 208 | pub(crate) fn new(mut metric: OperationMetric, histogram: Histogram) -> Self { 209 | let elapsed = metric.start_time.elapsed(); 210 | let (mut cpu_usage, used_memory, mut disk_usage) = 211 | if let Some(process) = metric.collect_process() { 212 | (process.cpu_usage(), process.memory(), process.disk_usage()) 213 | } else { 214 | (0.0, 0, DiskUsage::default()) 215 | }; 216 | // Subtract the initial disk usage 217 | disk_usage.total_written_bytes -= metric.initial_disk_usage.total_written_bytes; 218 | disk_usage.total_read_bytes -= metric.initial_disk_usage.total_read_bytes; 219 | // Divide the cpu usage by the number of cpus to get a normalized valued 220 | cpu_usage /= num_cpus::get() as f32; 221 | // Metrics 222 | let q75 = histogram.value_at_quantile(0.75); 223 | let q25 = histogram.value_at_quantile(0.25); 224 | let ops = metric.samples as f64 / (elapsed.as_nanos() as f64 / 1_000_000_000.0); 225 | Self { 226 | samples: metric.samples, 227 | mean: histogram.mean(), 228 | min: histogram.min(), 229 | max: histogram.max(), 230 | q99: histogram.value_at_quantile(0.99), 231 | q95: histogram.value_at_quantile(0.95), 232 | q75, 233 | q50: histogram.value_at_quantile(0.50), 234 | q25, 235 | q01: histogram.value_at_quantile(0.01), 236 | iqr: q75 - q25, 237 | ops, 238 | elapsed, 239 | cpu_usage, 240 | used_memory, 241 | disk_usage, 242 | load_avg: System::load_average(), 243 | } 244 | } 245 | /// Output the total time for this operation 246 | pub(crate) fn total_time(&self) -> String { 247 | format_duration(self.elapsed) 248 | } 249 | /// Output this operation as a table row 250 | pub(crate) fn output(&self, name: S) -> Vec 251 | where 252 | S: ToString, 253 | { 254 | vec![ 255 | name.to_string(), 256 | format_duration(self.elapsed), 257 | format!("{:.2} ms", self.mean / 1000.0), 258 | format!("{:.2} ms", self.max as f64 / 1000.0), 259 | format!("{:.2} ms", self.q99 as f64 / 1000.0), 260 | format!("{:.2} ms", self.q95 as f64 / 1000.0), 261 | format!("{:.2} ms", self.q75 as f64 / 1000.0), 262 | format!("{:.2} ms", self.q50 as f64 / 1000.0), 263 | format!("{:.2} ms", self.q25 as f64 / 1000.0), 264 | format!("{:.2} ms", self.q01 as f64 / 1000.0), 265 | format!("{:.2} ms", self.min as f64 / 1000.0), 266 | format!("{:.2} ms", self.iqr as f64 / 1000.0), 267 | format!("{:.2}", self.ops), 268 | format!("{:.2}%", self.cpu_usage), 269 | format!("{}", ByteSize(self.used_memory)), 270 | format!("{}", ByteSize(self.disk_usage.total_written_bytes)), 271 | format!("{}", ByteSize(self.disk_usage.total_read_bytes)), 272 | format!( 273 | "{:.2}/{:.2}/{:.2}", 274 | self.load_avg.one, self.load_avg.five, self.load_avg.fifteen 275 | ), 276 | ] 277 | } 278 | } 279 | 280 | fn format_duration(duration: Duration) -> String { 281 | let secs = duration.as_secs(); 282 | if secs >= 86400 { 283 | let days = secs / 86400; 284 | let hours = (secs % 86400) / 3600; 285 | format!("{}d {}h", days, hours) 286 | } else if secs >= 3600 { 287 | let hours = secs / 3600; 288 | let minutes = (secs % 3600) / 60; 289 | format!("{}h {}m", hours, minutes) 290 | } else if secs >= 60 { 291 | let minutes = secs / 60; 292 | let seconds = secs % 60; 293 | format!("{}m {}s", minutes, seconds) 294 | } else if secs > 0 { 295 | let seconds = secs; 296 | let millis = duration.subsec_millis(); 297 | format!("{}s {}ms", seconds, millis) 298 | } else if duration.subsec_millis() > 0 { 299 | let millis = duration.subsec_millis(); 300 | let micros = duration.subsec_micros() % 1000; 301 | format!("{}ms {}µs", millis, micros) 302 | } else if duration.subsec_micros() > 0 { 303 | let micros = duration.subsec_micros(); 304 | let nanos = duration.subsec_nanos() % 1000; 305 | format!("{}µs {}ns", micros, nanos) 306 | } else { 307 | format!("{}ns", duration.subsec_nanos()) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/scylladb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "scylladb")] 2 | 3 | use crate::dialect::AnsiSqlDialect; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::{ColumnType, Columns}; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::Result; 9 | use futures::StreamExt; 10 | use scylla::_macro_internal::SerializeValue; 11 | use scylla::transport::session::PoolSize; 12 | use scylla::{Session, SessionBuilder}; 13 | use serde_json::Value; 14 | use std::hint::black_box; 15 | use std::num::NonZeroUsize; 16 | 17 | pub const DEFAULT: &str = "127.0.0.1:9042"; 18 | 19 | pub(crate) const fn docker(_options: &Benchmark) -> DockerParams { 20 | DockerParams { 21 | image: "scylladb/scylla", 22 | pre_args: "-p 9042:9042", 23 | post_args: "", 24 | } 25 | } 26 | 27 | pub(crate) struct ScyllaDBClientProvider(KeyType, Columns, String); 28 | 29 | impl BenchmarkEngine for ScyllaDBClientProvider { 30 | /// Initiates a new datastore benchmarking engine 31 | async fn setup(kt: KeyType, columns: Columns, options: &Benchmark) -> Result { 32 | let url = options.endpoint.as_deref().unwrap_or(DEFAULT).to_owned(); 33 | Ok(ScyllaDBClientProvider(kt, columns, url)) 34 | } 35 | /// Creates a new client for this benchmarking engine 36 | async fn create_client(&self) -> Result { 37 | let session = SessionBuilder::new() 38 | .pool_size(PoolSize::PerHost(NonZeroUsize::new(1).unwrap())) 39 | .known_node(&self.2) 40 | .tcp_nodelay(true) 41 | .build() 42 | .await?; 43 | Ok(ScylladbClient { 44 | session, 45 | kt: self.0, 46 | columns: self.1.clone(), 47 | }) 48 | } 49 | } 50 | 51 | pub(crate) struct ScylladbClient { 52 | session: Session, 53 | kt: KeyType, 54 | columns: Columns, 55 | } 56 | 57 | impl BenchmarkClient for ScylladbClient { 58 | async fn startup(&self) -> Result<()> { 59 | self.session 60 | .query_unpaged( 61 | " 62 | CREATE KEYSPACE bench 63 | WITH replication = { 'class': 'SimpleStrategy', 'replication_factor' : 1 } 64 | AND durable_writes = true 65 | ", 66 | (), 67 | ) 68 | .await?; 69 | let id_type = match self.kt { 70 | KeyType::Integer => "INT", 71 | KeyType::String26 | KeyType::String90 | KeyType::String250 | KeyType::String506 => { 72 | "TEXT" 73 | } 74 | KeyType::Uuid => { 75 | todo!() 76 | } 77 | }; 78 | let fields: Vec = self 79 | .columns 80 | .0 81 | .iter() 82 | .map(|(n, t)| match t { 83 | ColumnType::String => format!("{n} TEXT"), 84 | ColumnType::Integer => format!("{n} INT"), 85 | ColumnType::Object => format!("{n} TEXT"), 86 | ColumnType::Float => format!("{n} FLOAT"), 87 | ColumnType::DateTime => format!("{n} TIMESTAMP"), 88 | ColumnType::Uuid => format!("{n} UUID"), 89 | ColumnType::Bool => format!("{n} BOOLEAN"), 90 | }) 91 | .collect(); 92 | let fields = fields.join(","); 93 | self.session 94 | .query_unpaged( 95 | format!("CREATE TABLE bench.record ( id {id_type} PRIMARY KEY, {fields})"), 96 | (), 97 | ) 98 | .await?; 99 | Ok(()) 100 | } 101 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 102 | self.create(key as i32, val).await 103 | } 104 | 105 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 106 | self.create(key, val).await 107 | } 108 | 109 | async fn read_u32(&self, key: u32) -> Result<()> { 110 | self.read(key as i32).await 111 | } 112 | 113 | async fn read_string(&self, key: String) -> Result<()> { 114 | self.read(key).await 115 | } 116 | 117 | async fn scan_u32(&self, scan: &Scan) -> Result { 118 | self.scan(scan).await 119 | } 120 | 121 | async fn scan_string(&self, scan: &Scan) -> Result { 122 | self.scan(scan).await 123 | } 124 | 125 | #[allow(dependency_on_unit_never_type_fallback)] 126 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 127 | self.update(key as i32, val).await 128 | } 129 | 130 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 131 | self.update(key, val).await 132 | } 133 | 134 | #[allow(dependency_on_unit_never_type_fallback)] 135 | async fn delete_u32(&self, key: u32) -> Result<()> { 136 | self.delete(key as i32).await 137 | } 138 | 139 | async fn delete_string(&self, key: String) -> Result<()> { 140 | self.delete(key).await 141 | } 142 | } 143 | 144 | impl ScylladbClient { 145 | async fn create(&self, key: T, val: Value) -> Result<()> 146 | where 147 | T: SerializeValue, 148 | { 149 | let (fields, values) = AnsiSqlDialect::create_clause(&self.columns, val); 150 | let stm = format!("INSERT INTO bench.record (id, {fields}) VALUES (?, {values})"); 151 | self.session.query_unpaged(stm, (&key,)).await?; 152 | Ok(()) 153 | } 154 | 155 | async fn read(&self, key: T) -> Result<()> 156 | where 157 | T: SerializeValue, 158 | { 159 | let stm = "SELECT * FROM bench.record WHERE id=?"; 160 | let res = self.session.query_unpaged(stm, (&key,)).await?; 161 | assert_eq!(res.into_rows_result()?.rows_num(), 1); 162 | Ok(()) 163 | } 164 | 165 | async fn update(&self, key: T, val: Value) -> Result<()> 166 | where 167 | T: SerializeValue, 168 | { 169 | let fields = AnsiSqlDialect::update_clause(&self.columns, val); 170 | let stm = format!("UPDATE bench.record SET {fields} WHERE id=?"); 171 | self.session.query_unpaged(stm, (&key,)).await?; 172 | Ok(()) 173 | } 174 | 175 | async fn delete(&self, key: T) -> Result<()> 176 | where 177 | T: SerializeValue, 178 | { 179 | let stm = "DELETE FROM bench.record WHERE id=?"; 180 | self.session.query_unpaged(stm, (&key,)).await?; 181 | Ok(()) 182 | } 183 | 184 | async fn scan(&self, scan: &Scan) -> Result { 185 | // Extract parameters 186 | let s = scan.start.unwrap_or_default(); 187 | let l = scan.limit.map(|l| format!("LIMIT {}", l + s)).unwrap_or_default(); 188 | let c = scan.condition.as_ref().map(|s| format!("WHERE {}", s)).unwrap_or_default(); 189 | let p = scan.projection()?; 190 | // Perform the relevant projection scan type 191 | match p { 192 | Projection::Id => { 193 | let stm = format!("SELECT id FROM bench.record {c} {l}"); 194 | let mut res = self.session.query_iter(stm, ()).await?.rows_stream()?.skip(s); 195 | let mut count = 0; 196 | while let Some(v) = res.next().await { 197 | let v: (String,) = v?; 198 | black_box(v); 199 | count += 1; 200 | } 201 | Ok(count) 202 | } 203 | Projection::Full => { 204 | let stm = format!("SELECT id FROM bench.record {c} {l}"); 205 | let mut res = self.session.query_iter(stm, ()).await?.rows_stream()?.skip(s); 206 | let mut count = 0; 207 | while let Some(v) = res.next().await { 208 | let v: (String,) = v?; 209 | black_box(v); 210 | count += 1; 211 | } 212 | Ok(count) 213 | } 214 | Projection::Count => { 215 | let stm = format!("SELECT count(*) FROM bench.record {c} {l}"); 216 | let mut res = self.session.query_iter(stm, ()).await?.rows_stream()?.skip(s); 217 | let count: (String,) = res.next().await.unwrap()?; 218 | let count: usize = count.0.parse()?; 219 | Ok(count) 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/sqlite.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "sqlite")] 2 | 3 | use crate::dialect::{AnsiSqlDialect, Dialect}; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::{ColumnType, Columns}; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::Result; 8 | use serde_json::{Map, Value as Json}; 9 | use std::borrow::Cow; 10 | use std::hint::black_box; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | use tokio_rusqlite::types::ToSqlOutput; 14 | use tokio_rusqlite::types::Value; 15 | use tokio_rusqlite::Connection; 16 | 17 | const DATABASE_DIR: &str = "sqlite"; 18 | 19 | // We can't just return `tokio_rusqlite::Row` because it's not Send/Sync 20 | type Row = Vec<(String, Value)>; 21 | 22 | pub(crate) struct SqliteClientProvider { 23 | conn: Arc, 24 | kt: KeyType, 25 | columns: Columns, 26 | } 27 | 28 | impl BenchmarkEngine for SqliteClientProvider { 29 | /// The number of seconds to wait before connecting 30 | fn wait_timeout(&self) -> Option { 31 | None 32 | } 33 | /// Initiates a new datastore benchmarking engine 34 | async fn setup(kt: KeyType, columns: Columns, _options: &Benchmark) -> Result { 35 | // Remove the database directory 36 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 37 | // Recreate the database directory 38 | std::fs::create_dir(DATABASE_DIR)?; 39 | // Switch to the new directory 40 | let path = format!("{DATABASE_DIR}/db"); 41 | // Create the connection 42 | let conn = Connection::open(&path).await?; 43 | // Create the store 44 | Ok(Self { 45 | conn: Arc::new(conn), 46 | kt, 47 | columns, 48 | }) 49 | } 50 | /// Creates a new client for this benchmarking engine 51 | async fn create_client(&self) -> Result { 52 | Ok(SqliteClient { 53 | conn: self.conn.clone(), 54 | kt: self.kt, 55 | columns: self.columns.clone(), 56 | }) 57 | } 58 | } 59 | 60 | pub(crate) struct SqliteClient { 61 | conn: Arc, 62 | kt: KeyType, 63 | columns: Columns, 64 | } 65 | 66 | impl BenchmarkClient for SqliteClient { 67 | async fn shutdown(&self) -> Result<()> { 68 | // Remove the database directory 69 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 70 | // Ok 71 | Ok(()) 72 | } 73 | 74 | async fn startup(&self) -> Result<()> { 75 | // Optimise SQLite 76 | let stmt = " 77 | PRAGMA synchronous = OFF; 78 | PRAGMA journal_mode = WAL; 79 | PRAGMA page_size = 16384; 80 | PRAGMA cache_size = 2500; 81 | PRAGMA locking_mode = EXCLUSIVE; 82 | "; 83 | self.execute_batch(Cow::Borrowed(stmt)).await?; 84 | let id_type = match self.kt { 85 | KeyType::Integer => "SERIAL", 86 | KeyType::String26 => "VARCHAR(26)", 87 | KeyType::String90 => "VARCHAR(90)", 88 | KeyType::String250 => "VARCHAR(250)", 89 | KeyType::String506 => "VARCHAR(506)", 90 | KeyType::Uuid => { 91 | todo!() 92 | } 93 | }; 94 | let fields = self 95 | .columns 96 | .0 97 | .iter() 98 | .map(|(n, t)| { 99 | let n = AnsiSqlDialect::escape_field(n.clone()); 100 | match t { 101 | ColumnType::String => format!("{n} TEXT NOT NULL"), 102 | ColumnType::Integer => format!("{n} INTEGER NOT NULL"), 103 | ColumnType::Object => format!("{n} JSON NOT NULL"), 104 | ColumnType::Float => format!("{n} REAL NOT NULL"), 105 | ColumnType::DateTime => format!("{n} TIMESTAMP NOT NULL"), 106 | ColumnType::Uuid => format!("{n} UUID NOT NULL"), 107 | ColumnType::Bool => format!("{n} BOOL NOT NULL"), 108 | } 109 | }) 110 | .collect::>() 111 | .join(","); 112 | let stmt = format!( 113 | " 114 | DROP TABLE IF EXISTS record; 115 | CREATE TABLE record ( id {id_type} PRIMARY KEY, {fields}); 116 | " 117 | ); 118 | self.execute_batch(Cow::Owned(stmt)).await?; 119 | Ok(()) 120 | } 121 | 122 | async fn create_u32(&self, key: u32, val: Json) -> Result<()> { 123 | self.create(key.into(), val).await 124 | } 125 | 126 | async fn create_string(&self, key: String, val: Json) -> Result<()> { 127 | self.create(key.into(), val).await 128 | } 129 | 130 | async fn read_u32(&self, key: u32) -> Result<()> { 131 | self.read(key.into()).await 132 | } 133 | 134 | async fn read_string(&self, key: String) -> Result<()> { 135 | self.read(key.into()).await 136 | } 137 | 138 | async fn update_u32(&self, key: u32, val: Json) -> Result<()> { 139 | self.update(key.into(), val).await 140 | } 141 | 142 | async fn update_string(&self, key: String, val: Json) -> Result<()> { 143 | self.update(key.into(), val).await 144 | } 145 | 146 | async fn delete_u32(&self, key: u32) -> Result<()> { 147 | self.delete(key.into()).await 148 | } 149 | 150 | async fn delete_string(&self, key: String) -> Result<()> { 151 | self.delete(key.into()).await 152 | } 153 | 154 | async fn scan_u32(&self, scan: &Scan) -> Result { 155 | self.scan(scan).await 156 | } 157 | 158 | async fn scan_string(&self, scan: &Scan) -> Result { 159 | self.scan(scan).await 160 | } 161 | } 162 | 163 | impl SqliteClient { 164 | async fn execute_batch(&self, query: Cow<'static, str>) -> Result<()> { 165 | self.conn.call(move |conn| conn.execute_batch(query.as_ref()).map_err(Into::into)).await?; 166 | Ok(()) 167 | } 168 | 169 | async fn execute( 170 | &self, 171 | query: Cow<'static, str>, 172 | params: ToSqlOutput<'static>, 173 | ) -> Result { 174 | self.conn 175 | .call(move |conn| conn.execute(query.as_ref(), [¶ms]).map_err(Into::into)) 176 | .await 177 | .map_err(Into::into) 178 | } 179 | 180 | async fn query( 181 | &self, 182 | stmt: Cow<'static, str>, 183 | params: Option>, 184 | ) -> Result> { 185 | self.conn 186 | .call(move |conn| { 187 | let mut stmt = conn.prepare(stmt.as_ref())?; 188 | let mut rows = match params { 189 | Some(params) => stmt.query([¶ms])?, 190 | None => stmt.query(())?, 191 | }; 192 | let mut vec = Vec::new(); 193 | while let Some(row) = rows.next()? { 194 | let names = row.as_ref().column_names(); 195 | let mut map = Vec::with_capacity(names.len()); 196 | for (i, name) in names.into_iter().enumerate() { 197 | map.push((name.to_owned(), row.get(i)?)); 198 | } 199 | vec.push(map); 200 | } 201 | Ok(vec) 202 | }) 203 | .await 204 | .map_err(Into::into) 205 | } 206 | 207 | async fn create(&self, key: ToSqlOutput<'static>, val: Json) -> Result<()> { 208 | let (fields, values) = AnsiSqlDialect::create_clause(&self.columns, val); 209 | let stmt = format!("INSERT INTO record (id, {fields}) VALUES ($1, {values})"); 210 | let res = self.execute(Cow::Owned(stmt), key).await?; 211 | assert_eq!(res, 1); 212 | Ok(()) 213 | } 214 | 215 | async fn read(&self, key: ToSqlOutput<'static>) -> Result<()> { 216 | let stm = "SELECT * FROM record WHERE id=$1"; 217 | let res = self.query(Cow::Borrowed(stm), Some(key)).await?; 218 | assert_eq!(res.len(), 1); 219 | Ok(()) 220 | } 221 | 222 | async fn update(&self, key: ToSqlOutput<'static>, val: Json) -> Result<()> { 223 | let fields = AnsiSqlDialect::update_clause(&self.columns, val); 224 | let stmt = format!("UPDATE record SET {fields} WHERE id=$1"); 225 | let res = self.execute(Cow::Owned(stmt), key).await?; 226 | assert_eq!(res, 1); 227 | Ok(()) 228 | } 229 | 230 | async fn delete(&self, key: ToSqlOutput<'static>) -> Result<()> { 231 | let stmt = "DELETE FROM record WHERE id=$1"; 232 | let res = self.execute(Cow::Borrowed(stmt), key).await?; 233 | assert_eq!(res, 1); 234 | Ok(()) 235 | } 236 | 237 | fn consume(&self, row: Row) -> Json { 238 | let mut val = Map::new(); 239 | for (key, value) in row { 240 | val.insert( 241 | key, 242 | match value { 243 | Value::Null => Json::Null, 244 | Value::Integer(int) => int.into(), 245 | Value::Real(float) => float.into(), 246 | Value::Text(text) => text.into(), 247 | Value::Blob(vec) => vec.into(), 248 | }, 249 | ); 250 | } 251 | val.into() 252 | } 253 | 254 | async fn scan(&self, scan: &Scan) -> Result { 255 | // Extract parameters 256 | let s = scan.start.map(|s| format!("OFFSET {s}")).unwrap_or_default(); 257 | let l = scan.limit.map(|s| format!("LIMIT {s}")).unwrap_or_default(); 258 | let c = scan.condition.as_ref().map(|s| format!("WHERE {s}")).unwrap_or_default(); 259 | let p = scan.projection()?; 260 | // Perform the relevant projection scan type 261 | match p { 262 | Projection::Id => { 263 | let stm = format!("SELECT id FROM record {c} {l} {s}"); 264 | let res = self.query(Cow::Owned(stm), None).await?; 265 | // We use a for loop to iterate over the results, while 266 | // calling black_box internally. This is necessary as 267 | // an iterator with `filter_map` or `map` is optimised 268 | // out by the compiler when calling `count` at the end. 269 | let mut count = 0; 270 | for v in res { 271 | black_box(self.consume(v)); 272 | count += 1; 273 | } 274 | Ok(count) 275 | } 276 | Projection::Full => { 277 | let stm = format!("SELECT * FROM record {c} {l} {s}"); 278 | let res = self.query(Cow::Owned(stm), None).await?; 279 | // We use a for loop to iterate over the results, while 280 | // calling black_box internally. This is necessary as 281 | // an iterator with `filter_map` or `map` is optimised 282 | // out by the compiler when calling `count` at the end. 283 | let mut count = 0; 284 | for v in res { 285 | black_box(self.consume(v)); 286 | count += 1; 287 | } 288 | Ok(count) 289 | } 290 | Projection::Count => { 291 | let stm = format!("SELECT COUNT(*) FROM (SELECT id FROM record {c} {l} {s})"); 292 | let res = self.query(Cow::Owned(stm), None).await?; 293 | let Value::Integer(count) = res.first().unwrap().first().unwrap().1 else { 294 | panic!("Unexpected response type `{res:?}`"); 295 | }; 296 | Ok(count as usize) 297 | } 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/surrealdb.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "surrealdb")] 2 | 3 | use crate::database::Database; 4 | use crate::docker::DockerParams; 5 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 6 | use crate::valueprovider::Columns; 7 | use crate::{Benchmark, KeyType, Projection, Scan}; 8 | use anyhow::Result; 9 | use serde::Deserialize; 10 | use serde_json::Value; 11 | use surrealdb::engine::any::{connect, Any}; 12 | use surrealdb::opt::auth::Root; 13 | use surrealdb::opt::{Config, Raw, Resource}; 14 | use surrealdb::RecordId; 15 | use surrealdb::Surreal; 16 | 17 | pub const DEFAULT: &str = "ws://127.0.0.1:8000"; 18 | 19 | pub(crate) const fn docker(options: &Benchmark) -> DockerParams { 20 | match options.database { 21 | Database::SurrealdbMemory => DockerParams { 22 | image: "surrealdb/surrealdb:nightly", 23 | pre_args: "--ulimit nofile=65536:65536 -p 8000:8000 --user root", 24 | post_args: "start --user root --pass root memory", 25 | }, 26 | Database::SurrealdbRocksdb => DockerParams { 27 | image: "surrealdb/surrealdb:nightly", 28 | pre_args: match options.sync { 29 | true => "--ulimit nofile=65536:65536 -p 8000:8000 -e SURREAL_SYNC_DATA=true --user root", 30 | false => "--ulimit nofile=65536:65536 -p 8000:8000 -e SURREAL_SYNC_DATA=false --user root", 31 | }, 32 | post_args: "start --user root --pass root rocksdb:/data/crud-bench.db", 33 | }, 34 | Database::SurrealdbSurrealkv => DockerParams { 35 | image: "surrealdb/surrealdb:nightly", 36 | pre_args: match options.sync { 37 | true => "--ulimit nofile=65536:65536 -p 8000:8000 -e SURREAL_SYNC_DATA=true --user root", 38 | false => "--ulimit nofile=65536:65536 -p 8000:8000 -e SURREAL_SYNC_DATA=false --user root", 39 | }, 40 | post_args: "start --user root --pass root surrealkv:/data/crud-bench.db", 41 | }, 42 | _ => unreachable!(), 43 | } 44 | } 45 | 46 | pub(crate) struct SurrealDBClientProvider { 47 | client: Option>, 48 | endpoint: String, 49 | root: Root<'static>, 50 | } 51 | 52 | async fn initialise_db(endpoint: &str, root: Root<'static>) -> Result> { 53 | // Set the root user 54 | let config = Config::new().user(root).ast_payload(); 55 | // Connect to the database 56 | let db = connect((endpoint, config)).await?; 57 | // Signin as a namespace, database, or root user 58 | db.signin(root).await?; 59 | // Select a specific namespace / database 60 | db.use_ns("test").use_db("test").await?; 61 | // Return the client 62 | Ok(db) 63 | } 64 | 65 | impl BenchmarkEngine for SurrealDBClientProvider { 66 | /// Initiates a new datastore benchmarking engine 67 | async fn setup(_: KeyType, _columns: Columns, options: &Benchmark) -> Result { 68 | // Get the custom endpoint if specified 69 | let endpoint = options.endpoint.as_deref().unwrap_or(DEFAULT).replace("memory", "mem://"); 70 | // Define root user details 71 | let root = Root { 72 | username: "root", 73 | password: "root", 74 | }; 75 | // Setup the optional client 76 | let client = match endpoint.split_once(':').unwrap().0 { 77 | // We want to be able to create multiple connections 78 | // when testing against an external server 79 | "ws" | "wss" | "http" | "https" => None, 80 | // When the database is embedded, create only 81 | // one client to avoid sending queries to the 82 | // wrong database 83 | _ => Some(initialise_db(&endpoint, root).await?), 84 | }; 85 | Ok(Self { 86 | endpoint, 87 | root, 88 | client, 89 | }) 90 | } 91 | /// Creates a new client for this benchmarking engine 92 | async fn create_client(&self) -> Result { 93 | let client = match &self.client { 94 | Some(client) => client.clone(), 95 | None => initialise_db(&self.endpoint, self.root).await?, 96 | }; 97 | Ok(SurrealDBClient::new(client)) 98 | } 99 | } 100 | 101 | pub(crate) struct SurrealDBClient { 102 | db: Surreal, 103 | } 104 | 105 | impl SurrealDBClient { 106 | const fn new(db: Surreal) -> Self { 107 | Self { 108 | db, 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug, Deserialize)] 114 | struct SurrealRecord { 115 | #[allow(dead_code)] 116 | id: RecordId, 117 | } 118 | 119 | impl BenchmarkClient for SurrealDBClient { 120 | async fn startup(&self) -> Result<()> { 121 | // Ensure the table exists. This wouldn't 122 | // normally be an issue, as SurrealDB is 123 | // schemaless. However, because we are testing 124 | // benchmarking of concurrent, optimistic 125 | // transactions, each initial concurrent 126 | // insert/create into the table attempts 127 | // to set up the NS+DB+TB, and this causes 128 | // 'resource busy' key conflict failures. 129 | let surql = " 130 | REMOVE TABLE IF EXISTS record; 131 | DEFINE TABLE record; 132 | "; 133 | self.db.query(Raw::from(surql)).await?.check()?; 134 | Ok(()) 135 | } 136 | 137 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 138 | let res = self.db.create(Resource::from(("record", key as i64))).content(val).await?; 139 | assert!(res.into_inner().is_some()); 140 | Ok(()) 141 | } 142 | 143 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 144 | let res = self.db.create(Resource::from(("record", key))).content(val).await?; 145 | assert!(res.into_inner().is_some()); 146 | Ok(()) 147 | } 148 | 149 | async fn read_u32(&self, key: u32) -> Result<()> { 150 | let res = self.db.select(Resource::from(("record", key as i64))).await?; 151 | assert!(res.into_inner().is_some()); 152 | Ok(()) 153 | } 154 | 155 | async fn read_string(&self, key: String) -> Result<()> { 156 | let res = self.db.select(Resource::from(("record", key))).await?; 157 | assert!(res.into_inner().is_some()); 158 | Ok(()) 159 | } 160 | 161 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 162 | let res = self.db.update(Resource::from(("record", key as i64))).content(val).await?; 163 | assert!(res.into_inner().is_some()); 164 | Ok(()) 165 | } 166 | 167 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 168 | let res = self.db.update(Resource::from(("record", key))).content(val).await?; 169 | assert!(res.into_inner().is_some()); 170 | Ok(()) 171 | } 172 | 173 | async fn delete_u32(&self, key: u32) -> Result<()> { 174 | let res = self.db.delete(Resource::from(("record", key as i64))).await?; 175 | assert!(res.into_inner().is_some()); 176 | Ok(()) 177 | } 178 | 179 | async fn delete_string(&self, key: String) -> Result<()> { 180 | let res = self.db.delete(Resource::from(("record", key))).await?; 181 | assert!(res.into_inner().is_some()); 182 | Ok(()) 183 | } 184 | 185 | async fn scan_u32(&self, scan: &Scan) -> Result { 186 | self.scan(scan).await 187 | } 188 | 189 | async fn scan_string(&self, scan: &Scan) -> Result { 190 | self.scan(scan).await 191 | } 192 | } 193 | 194 | impl SurrealDBClient { 195 | async fn scan(&self, scan: &Scan) -> Result { 196 | // Extract parameters 197 | let s = scan.start.map(|s| format!("START {s}")).unwrap_or_default(); 198 | let l = scan.limit.map(|s| format!("LIMIT {s}")).unwrap_or_default(); 199 | let c = scan.condition.as_ref().map(|s| format!("WHERE {s}")).unwrap_or_default(); 200 | let p = scan.projection()?; 201 | // Perform the relevant projection scan type 202 | match p { 203 | Projection::Id => { 204 | let sql = format!("SELECT id FROM record {c} {s} {l}"); 205 | let res: surrealdb::Value = self.db.query(Raw::from(sql)).await?.take(0)?; 206 | let surrealdb::sql::Value::Array(arr) = res.into_inner() else { 207 | panic!("Unexpected response type"); 208 | }; 209 | Ok(arr.len()) 210 | } 211 | Projection::Full => { 212 | let sql = format!("SELECT * FROM record {c} {s} {l}"); 213 | let res: surrealdb::Value = self.db.query(Raw::from(sql)).await?.take(0)?; 214 | let surrealdb::sql::Value::Array(arr) = res.into_inner() else { 215 | panic!("Unexpected response type"); 216 | }; 217 | Ok(arr.len()) 218 | } 219 | Projection::Count => { 220 | let sql = if s.is_empty() && l.is_empty() { 221 | format!("SELECT count() FROM record {c} GROUP ALL") 222 | } else { 223 | format!("SELECT count() FROM (SELECT 1 FROM record {c} {s} {l}) GROUP ALL") 224 | }; 225 | let res: Option = self.db.query(Raw::from(sql)).await?.take("count")?; 226 | Ok(res.unwrap()) 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/surrealkv.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "surrealkv")] 2 | 3 | use crate::benchmark::NOT_SUPPORTED_ERROR; 4 | use crate::engine::{BenchmarkClient, BenchmarkEngine}; 5 | use crate::valueprovider::Columns; 6 | use crate::{Benchmark, KeyType, Projection, Scan}; 7 | use anyhow::{bail, Result}; 8 | use serde_json::Value; 9 | use std::cmp::max; 10 | use std::hint::black_box; 11 | use std::iter::Iterator; 12 | use std::path::PathBuf; 13 | use std::sync::Arc; 14 | use std::sync::OnceLock; 15 | use std::time::Duration; 16 | use surrealkv::Durability; 17 | use surrealkv::Mode::{ReadOnly, ReadWrite}; 18 | use surrealkv::Options; 19 | use surrealkv::Store; 20 | use sysinfo::System; 21 | 22 | const DATABASE_DIR: &str = "surrealkv"; 23 | 24 | const MIN_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB 25 | 26 | pub(crate) static SKV_THREADPOOL: OnceLock = OnceLock::new(); 27 | 28 | fn get_threadpool() -> &'static affinitypool::Threadpool { 29 | SKV_THREADPOOL.get_or_init(|| { 30 | affinitypool::Builder::new() 31 | .thread_name("surrealkv-threadpool") 32 | .thread_stack_size(5 * 1024 * 1024) 33 | .thread_per_core(false) 34 | .worker_threads(1) 35 | .build() 36 | }) 37 | } 38 | 39 | pub(crate) struct SurrealKVClientProvider(Arc); 40 | 41 | impl BenchmarkEngine for SurrealKVClientProvider { 42 | /// The number of seconds to wait before connecting 43 | fn wait_timeout(&self) -> Option { 44 | None 45 | } 46 | /// Initiates a new datastore benchmarking engine 47 | async fn setup(_: KeyType, _columns: Columns, options: &Benchmark) -> Result { 48 | // Cleanup the data directory 49 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 50 | // Load the system attributes 51 | let system = System::new_all(); 52 | // Get the total system memory 53 | let memory = system.total_memory(); 54 | // Divide the total memory into half 55 | let memory = memory.saturating_div(2); 56 | // Subtract 1 GiB from the memory size 57 | let memory = memory.saturating_sub(1024 * 1024 * 1024); 58 | // Divide the total memory by a 4KiB value size 59 | let cache = memory.saturating_div(4096); 60 | // Calculate a good cache memory size 61 | let cache = max(cache, MIN_CACHE_SIZE); 62 | // Configure custom options 63 | let mut opts = Options::new(); 64 | // Disable versioning 65 | opts.enable_versions = false; 66 | // Enable disk persistence 67 | opts.disk_persistence = options.disk_persistence; 68 | // Set the directory location 69 | opts.dir = PathBuf::from(DATABASE_DIR); 70 | // Set the cache to 250,000 entries 71 | opts.max_value_cache_size = cache; 72 | 73 | // Create the store 74 | Ok(Self(Arc::new(Store::new(opts)?))) 75 | } 76 | /// Creates a new client for this benchmarking engine 77 | async fn create_client(&self) -> Result { 78 | Ok(SurrealKVClient { 79 | db: self.0.clone(), 80 | }) 81 | } 82 | } 83 | 84 | pub(crate) struct SurrealKVClient { 85 | db: Arc, 86 | } 87 | 88 | impl BenchmarkClient for SurrealKVClient { 89 | async fn shutdown(&self) -> Result<()> { 90 | // Cleanup the data directory 91 | std::fs::remove_dir_all(DATABASE_DIR).ok(); 92 | // Ok 93 | Ok(()) 94 | } 95 | 96 | async fn create_u32(&self, key: u32, val: Value) -> Result<()> { 97 | self.create_bytes(&key.to_ne_bytes(), val).await 98 | } 99 | 100 | async fn create_string(&self, key: String, val: Value) -> Result<()> { 101 | self.create_bytes(&key.into_bytes(), val).await 102 | } 103 | 104 | async fn read_u32(&self, key: u32) -> Result<()> { 105 | self.read_bytes(&key.to_ne_bytes()).await 106 | } 107 | 108 | async fn read_string(&self, key: String) -> Result<()> { 109 | self.read_bytes(&key.into_bytes()).await 110 | } 111 | 112 | async fn update_u32(&self, key: u32, val: Value) -> Result<()> { 113 | self.update_bytes(&key.to_ne_bytes(), val).await 114 | } 115 | 116 | async fn update_string(&self, key: String, val: Value) -> Result<()> { 117 | self.update_bytes(&key.into_bytes(), val).await 118 | } 119 | 120 | async fn delete_u32(&self, key: u32) -> Result<()> { 121 | self.delete_bytes(&key.to_ne_bytes()).await 122 | } 123 | 124 | async fn delete_string(&self, key: String) -> Result<()> { 125 | self.delete_bytes(&key.into_bytes()).await 126 | } 127 | 128 | async fn scan_u32(&self, scan: &Scan) -> Result { 129 | self.scan_bytes(scan).await 130 | } 131 | 132 | async fn scan_string(&self, scan: &Scan) -> Result { 133 | self.scan_bytes(scan).await 134 | } 135 | } 136 | 137 | impl SurrealKVClient { 138 | async fn create_bytes(&self, key: &[u8], val: Value) -> Result<()> { 139 | // Serialise the value 140 | let val = bincode::serialize(&val)?; 141 | // Create a new transaction 142 | let mut txn = self.db.begin_with_mode(ReadWrite)?; 143 | // Let the OS handle syncing to disk 144 | txn.set_durability(Durability::Eventual); 145 | // Process the data 146 | txn.set(key, &val)?; 147 | get_threadpool().spawn(move || txn.commit()).await?; 148 | Ok(()) 149 | } 150 | 151 | async fn read_bytes(&self, key: &[u8]) -> Result<()> { 152 | // Create a new transaction 153 | let mut txn = self.db.begin_with_mode(ReadOnly)?; 154 | // Process the data 155 | let res = txn.get(key)?; 156 | // Check the value exists 157 | assert!(res.is_some()); 158 | // Deserialise the value 159 | black_box(res.unwrap()); 160 | // All ok 161 | Ok(()) 162 | } 163 | 164 | async fn update_bytes(&self, key: &[u8], val: Value) -> Result<()> { 165 | // Serialise the value 166 | let val = bincode::serialize(&val)?; 167 | // Create a new transaction 168 | let mut txn = self.db.begin_with_mode(ReadWrite)?; 169 | // Let the OS handle syncing to disk 170 | txn.set_durability(Durability::Eventual); 171 | // Process the data 172 | txn.set(key, &val)?; 173 | get_threadpool().spawn(move || txn.commit()).await?; 174 | Ok(()) 175 | } 176 | 177 | async fn delete_bytes(&self, key: &[u8]) -> Result<()> { 178 | // Create a new transaction 179 | let mut txn = self.db.begin_with_mode(ReadWrite)?; 180 | // Let the OS handle syncing to disk 181 | txn.set_durability(Durability::Eventual); 182 | // Process the data 183 | txn.delete(key)?; 184 | get_threadpool().spawn(move || txn.commit()).await?; 185 | Ok(()) 186 | } 187 | 188 | async fn scan_bytes(&self, scan: &Scan) -> Result { 189 | // Contional scans are not supported 190 | if scan.condition.is_some() { 191 | bail!(NOT_SUPPORTED_ERROR); 192 | } 193 | // Extract parameters 194 | let s = scan.start.unwrap_or(0); 195 | let l = scan.limit.unwrap_or(usize::MAX); 196 | let t = scan.limit.map(|l| s + l); 197 | let p = scan.projection()?; 198 | // Create a new transaction 199 | let mut txn = self.db.begin_with_mode(ReadOnly)?; 200 | let beg = [0u8].as_slice(); 201 | let end = [255u8].as_slice(); 202 | // Perform the relevant projection scan type 203 | match p { 204 | Projection::Id => { 205 | // Create an iterator starting at the beginning 206 | let iter = txn.keys(beg..end, t); 207 | // We use a for loop to iterate over the results, while 208 | // calling black_box internally. This is necessary as 209 | // an iterator with `filter_map` or `map` is optimised 210 | // out by the compiler when calling `count` at the end. 211 | let mut count = 0; 212 | for v in iter.skip(s).take(l) { 213 | black_box(v); 214 | count += 1; 215 | } 216 | Ok(count) 217 | } 218 | Projection::Full => { 219 | // Create an iterator starting at the beginning 220 | let iter = txn.scan(beg..end, t); 221 | // We use a for loop to iterate over the results, while 222 | // calling black_box internally. This is necessary as 223 | // an iterator with `filter_map` or `map` is optimised 224 | // out by the compiler when calling `count` at the end. 225 | let mut count = 0; 226 | for v in iter.skip(s).take(l) { 227 | assert!(v.is_ok()); 228 | black_box(v.unwrap().1); 229 | count += 1; 230 | } 231 | Ok(count) 232 | } 233 | Projection::Count => { 234 | Ok(txn 235 | .keys(beg..end, t) 236 | .skip(s) // Skip the first `offset` entries 237 | .take(l) // Take the next `limit` entries 238 | .count()) 239 | } 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/terminal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::fmt::Display; 3 | use std::io::Write; 4 | use std::io::{stdout, IsTerminal, Stdout}; 5 | 6 | pub(crate) struct Terminal(Option); 7 | 8 | impl Default for Terminal { 9 | fn default() -> Self { 10 | let stdout = stdout(); 11 | if stdout.is_terminal() { 12 | Self(Some(stdout)) 13 | } else { 14 | Self(None) 15 | } 16 | } 17 | } 18 | 19 | impl Clone for Terminal { 20 | fn clone(&self) -> Self { 21 | Self(self.0.as_ref().map(|_| stdout())) 22 | } 23 | } 24 | 25 | impl Terminal { 26 | /// Write a line to this Terminal via a callback 27 | pub(crate) fn write(&mut self, mut f: F) -> Result<()> 28 | where 29 | F: FnMut() -> Option, 30 | S: Display, 31 | { 32 | if let Some(ref mut o) = self.0 { 33 | if let Some(s) = f() { 34 | write!(o, "{s}")?; 35 | o.flush()?; 36 | } 37 | } 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/valueprovider.rs: -------------------------------------------------------------------------------- 1 | use crate::dialect::Dialect; 2 | use anyhow::{anyhow, bail, Result}; 3 | use log::debug; 4 | use rand::prelude::SmallRng; 5 | use rand::{Rng, SeedableRng}; 6 | use serde_json::{Map, Number, Value}; 7 | use std::collections::BTreeMap; 8 | use std::fmt::Display; 9 | use std::ops::Range; 10 | use std::str::FromStr; 11 | use uuid::Uuid; 12 | 13 | pub(crate) struct ValueProvider { 14 | generator: ValueGenerator, 15 | rng: SmallRng, 16 | columns: Columns, 17 | } 18 | 19 | impl ValueProvider { 20 | pub(crate) fn new(json: &str) -> Result { 21 | // Decode the JSON string 22 | let val = serde_json::from_str(json)?; 23 | debug!("Value template: {val:#}"); 24 | // Compile a value generator 25 | let generator = ValueGenerator::new(val)?; 26 | // Identifies the field in the top level (used for column oriented DB like Postgresql) 27 | let columns = Columns::new(&generator)?; 28 | Ok(Self { 29 | generator, 30 | columns, 31 | rng: SmallRng::from_entropy(), 32 | }) 33 | } 34 | 35 | pub(crate) fn columns(&self) -> Columns { 36 | self.columns.clone() 37 | } 38 | 39 | pub(crate) fn generate_value(&mut self) -> Value 40 | where 41 | D: Dialect, 42 | { 43 | self.generator.generate::(&mut self.rng) 44 | } 45 | } 46 | 47 | impl Clone for ValueProvider { 48 | fn clone(&self) -> Self { 49 | Self { 50 | generator: self.generator.clone(), 51 | rng: SmallRng::from_entropy(), 52 | columns: self.columns.clone(), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, Debug)] 58 | enum ValueGenerator { 59 | Bool, 60 | String(Length), 61 | Text(Length), 62 | Integer, 63 | Float, 64 | DateTime, 65 | Uuid, 66 | // We use i32 for better compatibility across DBs 67 | IntegerRange(Range), 68 | // We use f32 by default for better compatibility across DBs 69 | FloatRange(Range), 70 | StringEnum(Vec), 71 | IntegerEnum(Vec), 72 | FloatEnum(Vec), 73 | Array(Vec), 74 | Object(BTreeMap), 75 | } 76 | 77 | const CHARSET: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 78 | 79 | fn string(rng: &mut SmallRng, size: usize) -> String { 80 | (0..size) 81 | .map(|_| { 82 | let idx = rng.gen_range(0..CHARSET.len()); 83 | CHARSET[idx] as char 84 | }) 85 | .collect() 86 | } 87 | 88 | fn string_range(rng: &mut SmallRng, range: Range) -> String { 89 | let l = rng.gen_range(range); 90 | string(rng, l) 91 | } 92 | 93 | fn text(rng: &mut SmallRng, size: usize) -> String { 94 | let mut l = 0; 95 | let mut words = Vec::with_capacity(size / 5); 96 | let mut i = 0; 97 | while l < size { 98 | let w = string_range(rng, 2..10); 99 | l += w.len(); 100 | words.push(w); 101 | l += i; 102 | // We ignore the first whitespace, but not the following ones 103 | i = 1; 104 | } 105 | words.join(" ") 106 | } 107 | 108 | fn text_range(rng: &mut SmallRng, range: Range) -> String { 109 | let l = rng.gen_range(range); 110 | text(rng, l) 111 | } 112 | 113 | impl ValueGenerator { 114 | fn new(value: Value) -> Result { 115 | match value { 116 | Value::Null => bail!("Unsupported type: Null"), 117 | Value::Bool(_) => bail!("Unsupported type: Bool"), 118 | Value::Number(_) => bail!("Unsupported type: Number"), 119 | Value::String(s) => Self::new_string(s), 120 | Value::Array(a) => Self::new_array(a), 121 | Value::Object(o) => Self::new_object(o), 122 | } 123 | } 124 | 125 | fn new_string(s: String) -> Result { 126 | let s = s.to_lowercase(); 127 | let r = if let Some(i) = s.strip_prefix("string:") { 128 | Self::String(Length::new(i)?) 129 | } else if let Some(i) = s.strip_prefix("text:") { 130 | Self::Text(Length::new(i)?) 131 | } else if let Some(i) = s.strip_prefix("int:") { 132 | if let Length::Range(r) = Length::new(i)? { 133 | Self::IntegerRange(r) 134 | } else { 135 | bail!("Expected a range but got: {i}"); 136 | } 137 | } else if let Some(i) = s.strip_prefix("float:") { 138 | if let Length::Range(r) = Length::new(i)? { 139 | Self::FloatRange(r) 140 | } else { 141 | bail!("Expected a range but got: {i}"); 142 | } 143 | } else if let Some(s) = s.strip_prefix("string_enum:") { 144 | let labels = s.split(",").map(|s| s.to_string()).collect(); 145 | Self::StringEnum(labels) 146 | } else if let Some(s) = s.strip_prefix("int_enum:") { 147 | let split: Vec<&str> = s.split(",").collect(); 148 | let mut numbers = Vec::with_capacity(split.len()); 149 | for s in split { 150 | numbers.push(s.parse::()?.into()); 151 | } 152 | Self::IntegerEnum(numbers) 153 | } else if let Some(s) = s.strip_prefix("float_enum:") { 154 | let split: Vec<&str> = s.split(",").collect(); 155 | let mut numbers = Vec::with_capacity(split.len()); 156 | for s in split { 157 | numbers.push(Number::from_f64(s.parse::()? as f64).unwrap()); 158 | } 159 | Self::FloatEnum(numbers) 160 | } else if s.eq("bool") { 161 | Self::Bool 162 | } else if s.eq("int") { 163 | Self::Integer 164 | } else if s.eq("float") { 165 | Self::Float 166 | } else if s.eq("datetime") { 167 | Self::DateTime 168 | } else if s.eq("uuid") { 169 | Self::Uuid 170 | } else { 171 | bail!("Unsupported type: {s}"); 172 | }; 173 | Ok(r) 174 | } 175 | 176 | fn new_array(a: Vec) -> Result { 177 | let mut array = Vec::with_capacity(a.len()); 178 | for v in a { 179 | array.push(ValueGenerator::new(v)?); 180 | } 181 | Ok(Self::Array(array)) 182 | } 183 | 184 | fn new_object(o: Map) -> Result { 185 | let mut map = BTreeMap::new(); 186 | for (k, v) in o { 187 | map.insert(k, Self::new(v)?); 188 | } 189 | Ok(Self::Object(map)) 190 | } 191 | 192 | fn generate(&self, rng: &mut SmallRng) -> Value 193 | where 194 | D: Dialect, 195 | { 196 | match self { 197 | ValueGenerator::Bool => { 198 | let v = rng.gen::(); 199 | Value::Bool(v) 200 | } 201 | ValueGenerator::String(l) => { 202 | let val = match l { 203 | Length::Range(r) => string_range(rng, r.clone()), 204 | Length::Fixed(l) => string(rng, *l), 205 | }; 206 | Value::String(val) 207 | } 208 | ValueGenerator::Text(l) => { 209 | let val = match l { 210 | Length::Range(r) => text_range(rng, r.clone()), 211 | Length::Fixed(l) => text(rng, *l), 212 | }; 213 | Value::String(val) 214 | } 215 | ValueGenerator::Integer => { 216 | let v = rng.gen::(); 217 | Value::Number(Number::from(v)) 218 | } 219 | ValueGenerator::Float => { 220 | let v = rng.gen::(); 221 | Value::Number(Number::from_f64(v as f64).unwrap()) 222 | } 223 | ValueGenerator::DateTime => { 224 | // Number of seconds from Epoch to 31/12/2030 225 | let s = rng.gen_range(0..1_924_991_999); 226 | D::date_time(s) 227 | } 228 | ValueGenerator::Uuid => { 229 | let uuid = Uuid::new_v4(); 230 | D::uuid(uuid) 231 | } 232 | ValueGenerator::IntegerRange(r) => { 233 | let v = rng.gen_range(r.start..r.end); 234 | Value::Number(v.into()) 235 | } 236 | ValueGenerator::FloatRange(r) => { 237 | let v = rng.gen_range(r.start..r.end); 238 | Value::Number(Number::from_f64(v as f64).unwrap()) 239 | } 240 | ValueGenerator::StringEnum(a) => { 241 | let i = rng.gen_range(0..a.len()); 242 | Value::String(a[i].to_string()) 243 | } 244 | ValueGenerator::IntegerEnum(a) => { 245 | let i = rng.gen_range(0..a.len()); 246 | Value::Number(a[i].clone()) 247 | } 248 | ValueGenerator::FloatEnum(a) => { 249 | let i = rng.gen_range(0..a.len()); 250 | Value::Number(a[i].clone()) 251 | } 252 | ValueGenerator::Array(a) => { 253 | // Generate any array structure values 254 | let mut vec = Vec::with_capacity(a.len()); 255 | for v in a { 256 | vec.push(v.generate::(rng)); 257 | } 258 | Value::Array(vec) 259 | } 260 | ValueGenerator::Object(o) => { 261 | // Generate any object structure values 262 | let mut map = Map::::new(); 263 | for (k, v) in o { 264 | map.insert(k.clone(), v.generate::(rng)); 265 | } 266 | Value::Object(map) 267 | } 268 | } 269 | } 270 | } 271 | 272 | #[derive(Clone, Debug)] 273 | enum Length 274 | where 275 | Idx: FromStr, 276 | { 277 | Range(Range), 278 | Fixed(Idx), 279 | } 280 | 281 | impl Length 282 | where 283 | Idx: FromStr, 284 | { 285 | fn new(s: &str) -> Result 286 | where 287 | ::Err: Display, 288 | { 289 | // Get the length config setting 290 | let gen: Vec<&str> = s.split("..").collect(); 291 | // Check the length parameter 292 | let r = match gen.len() { 293 | 2 => { 294 | let min = Idx::from_str(gen[0]).map_err(|e| anyhow!("{e}"))?; 295 | let max = Idx::from_str(gen[1]).map_err(|e| anyhow!("{e}"))?; 296 | Self::Range(min..max) 297 | } 298 | 1 => Self::Fixed(Idx::from_str(gen[0]).map_err(|e| anyhow!("{e}"))?), 299 | v => { 300 | bail!("Invalid length generation value: {v}"); 301 | } 302 | }; 303 | Ok(r) 304 | } 305 | } 306 | 307 | #[derive(Clone, Debug)] 308 | /// This structures defines the main columns use for create the schema 309 | /// and insert generated data into a column-oriented database (PostreSQL). 310 | pub(crate) struct Columns(pub(crate) Vec<(String, ColumnType)>); 311 | 312 | impl Columns { 313 | fn new(value: &ValueGenerator) -> Result { 314 | if let ValueGenerator::Object(o) = value { 315 | let mut columns = Vec::with_capacity(o.len()); 316 | for (f, g) in o { 317 | columns.push((f.to_string(), ColumnType::new(g)?)); 318 | } 319 | Ok(Columns(columns)) 320 | } else { 321 | bail!("An object was expected, but got: {value:?}"); 322 | } 323 | } 324 | } 325 | 326 | #[derive(Clone, Debug)] 327 | pub(crate) enum ColumnType { 328 | String, 329 | Integer, 330 | Float, 331 | DateTime, 332 | Uuid, 333 | Object, 334 | Bool, 335 | } 336 | 337 | impl ColumnType { 338 | fn new(v: &ValueGenerator) -> Result { 339 | let r = match v { 340 | ValueGenerator::Object(_) => ColumnType::Object, 341 | ValueGenerator::StringEnum(_) | ValueGenerator::String(_) | ValueGenerator::Text(_) => { 342 | ColumnType::String 343 | } 344 | ValueGenerator::Integer 345 | | ValueGenerator::IntegerRange(_) 346 | | ValueGenerator::IntegerEnum(_) => ColumnType::Integer, 347 | ValueGenerator::Float 348 | | ValueGenerator::FloatRange(_) 349 | | ValueGenerator::FloatEnum(_) => ColumnType::Float, 350 | ValueGenerator::DateTime => ColumnType::DateTime, 351 | ValueGenerator::Bool => ColumnType::Bool, 352 | ValueGenerator::Uuid => ColumnType::Uuid, 353 | t => { 354 | bail!("Invalid data type: {t:?}"); 355 | } 356 | }; 357 | Ok(r) 358 | } 359 | } 360 | 361 | #[cfg(test)] 362 | mod test { 363 | use super::*; 364 | use crate::dialect::AnsiSqlDialect; 365 | use tokio::task; 366 | 367 | #[tokio::test] 368 | async fn check_all_values_are_unique() { 369 | let vp = ValueProvider::new(r#"{ "int": "int", "int_range": "int:1..99"}"#).unwrap(); 370 | let mut v = vp.clone(); 371 | let f1 = task::spawn(async move { 372 | (v.generate_value::(), v.generate_value::()) 373 | }); 374 | let mut v = vp.clone(); 375 | let f2 = task::spawn(async move { 376 | (v.generate_value::(), v.generate_value::()) 377 | }); 378 | let (v1a, v1b) = f1.await.unwrap(); 379 | let (v2a, v2b) = f2.await.unwrap(); 380 | assert_ne!(v1a, v1b); 381 | assert_ne!(v2a, v2b); 382 | assert_ne!(v1a, v2a); 383 | assert_ne!(v1b, v2b); 384 | } 385 | } 386 | --------------------------------------------------------------------------------