diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..8c26b22 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,116 @@ +name: Benchmark Rust version + +on: [push, pull_request] + +env: + toolchain: nightly-2024-01-01 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + benchmark: + runs-on: ubuntu-latest + + services: + neo4j: + image: neo4j:5.15.0 + env: + NEO4J_AUTH: neo4j/password + NEO4J_PLUGINS: '[]' + options: >- + --health-cmd "wget -q --spider http://localhost:7474 || exit 1" + --health-interval 10s + --health-timeout 10s + --health-retries 10 + --health-start-period 30s + ports: + - 7474:7474 + - 7687:7687 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{env.toolchain}} + components: rustfmt, clippy + + - name: Wait for Neo4j to be ready + run: | + echo "Waiting for Neo4j to be fully ready..." + for i in {1..30}; do + if curl -s http://localhost:7474 > /dev/null; then + echo "Neo4j is ready!" + break + fi + echo "Attempt $i: Neo4j not ready yet..." + sleep 2 + done + + - name: Build benchmark + run: cargo build --release --all-features --manifest-path rust/Cargo.toml + + - name: Run benchmark + working-directory: rust + env: + NEO4J_URI: bolt://localhost:7687 + NEO4J_USER: neo4j + NEO4J_PASSWORD: password + run: cargo bench --bench bench -- --output-format bencher | tee out.txt + + - name: Prepare benchmark results + run: | + git config --global user.email "linksplatform@gmail.com" + git config --global user.name "LinksPlatformBencher" + cd rust + pip install numpy matplotlib + python3 out.py + cd .. + + # Create Docs directory if it doesn't exist + mkdir -p Docs + + # Copy generated images + cp -f rust/bench_rust.png Docs/ + cp -f rust/bench_rust_log_scale.png Docs/ + + # Update README with latest results + if [ -f rust/results.md ]; then + # Replace the results section in README.md + python3 -c " + import re + + with open('rust/results.md', 'r') as f: + results = f.read() + + with open('README.md', 'r') as f: + readme = f.read() + + # Pattern to find and replace the results table + pattern = r'(\| Operation.*?\n\|[-|]+\n(?:\|.*?\n)*)' + if re.search(pattern, readme): + readme = re.sub(pattern, results.strip() + '\n', readme) + + with open('README.md', 'w') as f: + f.write(readme) + " + fi + + # Commit changes if any + git add Docs README.md + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "Update benchmark results" + git push origin HEAD + fi + + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: Benchmark results + path: | + rust/bench_rust.png + rust/bench_rust_log_scale.png + rust/out.txt diff --git a/Docs/bench_rust.png b/Docs/bench_rust.png new file mode 100644 index 0000000..2fd27c7 Binary files /dev/null and b/Docs/bench_rust.png differ diff --git a/Docs/bench_rust_log_scale.png b/Docs/bench_rust_log_scale.png new file mode 100644 index 0000000..e06fa59 Binary files /dev/null and b/Docs/bench_rust_log_scale.png differ diff --git a/README.md b/README.md index 366dfac..db00cd1 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# Comparisons.Neo4jVSDoublets \ No newline at end of file +# Comparisons.Neo4jVSDoublets + +The comparison between Neo4j and LinksPlatform's Doublets (links) on basic database operations with links (create, read, delete, update). +All benchmarks ran with 3000 links in background to increase size of indexes and 1000 are actively created/updated/deleted. + +In this particular benchmark we decided not to increase the number of links as Neo4j will not be able to handle it at all in timeframe what GitHub Actions limit allows to use for free. Remember that to get accurate result we ran this benchmark multiple times. + +## Task + +Both databases used to store and retrieve doublet-links representation. To support storage, and all basic CRUD operations that provide Turing completeness for links as in [the links theory](https://habr.com/ru/articles/895896). + +## Operations +- **Create** – insert point link (link with id = source = target) +- **Update** – basic link update operation +- **Delete** – basic link delete operation +- **Each All** – take all links matching `[*, *, *]` constraint +- **Each Incoming** – take all links matching `[*, *, target]` constraint +- **Each Outgoing** – take all links matching `[*, source, *]` constraint +- **Each Concrete** – take all links matching `[*, source, target]` constraint +- **Each Identity** – take all links matching `[id, *, *]` constraint + +## Results +The results below represent the amount of time (ns) the operation takes per iteration. +- First picture shows time in a pixel scale (for doublets just minimum value is shown, otherwise it will be not present on the graph). +- Second picture shows time in a logarithmic scale (to see difference clearly, because it is around 2-3 orders of magnitude). + +### Rust +![Image of Rust benchmark (pixel scale)](https://github.com/linksplatform/Comparisons.Neo4jVSDoublets/blob/main/Docs/bench_rust.png?raw=true) +![Image of Rust benchmark (log scale)](https://github.com/linksplatform/Comparisons.Neo4jVSDoublets/blob/main/Docs/bench_rust_log_scale.png?raw=true) + +### Raw benchmark results (all numbers are in nanoseconds) + +| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | Neo4j NonTransaction | Neo4j Transaction | +|---------------|--------------------------|-----------------------------|-------------------------|----------------------------|----------------------|-------------------| +| Create | 99201 (0.3x faster) | 101660 (0.3x faster) | 85197 (0.4x faster) | 83516 (0.4x faster) | 3206352817 | 31808 | +| Update | N/A | N/A | N/A | N/A | N/A | N/A | +| Delete | N/A | N/A | N/A | N/A | 1955576426 | N/A | +| Each All | N/A | N/A | N/A | N/A | N/A | N/A | +| Each Identity | N/A | N/A | N/A | N/A | N/A | N/A | +| Each Concrete | N/A | N/A | N/A | N/A | N/A | N/A | +| Each Outgoing | N/A | N/A | N/A | N/A | N/A | N/A | +| Each Incoming | N/A | N/A | N/A | N/A | N/A | N/A | + +## Conclusion + +The benchmark results will be automatically updated once the GitHub Actions workflow runs. Results are expected to show that Doublets significantly outperforms Neo4j in both write and read operations due to its specialized data structure for storing doublet-links. + +To get fresh numbers, please fork the repository and rerun benchmark in GitHub Actions. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d7c5705 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2022-08-22" diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..25d11be --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,794 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "delegate" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70a2d4995466955a415223acf3c9c934b9ff2339631cdf4ffc893da4bacd717" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "doublets" +version = "0.1.0-pre+beta.15" +source = "git+https://github.com/linksplatform/doublets-rs.git#5522d91c536654934b7181829f6efb570fb8bb44" +dependencies = [ + "bumpalo", + "cfg-if", + "leak_slice", + "platform-data", + "platform-mem", + "platform-trees", + "tap", + "thiserror", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leak_slice" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf3387da9fb41906394e1306ddd3cd26dd9b7177af11c19b45b364b743aed26" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linksneo4j" +version = "0.1.0" +dependencies = [ + "criterion", + "doublets", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "platform-data" +version = "0.1.0-beta.3" +source = "git+https://github.com/linksplatform/doublets-rs.git#5522d91c536654934b7181829f6efb570fb8bb44" +dependencies = [ + "beef", + "funty", + "thiserror", +] + +[[package]] +name = "platform-mem" +version = "0.1.0-pre+beta.2" +source = "git+https://github.com/linksplatform/doublets-rs.git#5522d91c536654934b7181829f6efb570fb8bb44" +dependencies = [ + "delegate", + "memmap2", + "tap", + "tempfile", + "thiserror", +] + +[[package]] +name = "platform-trees" +version = "0.1.0-beta.1" +source = "git+https://github.com/linksplatform/doublets-rs.git#5522d91c536654934b7181829f6efb570fb8bb44" +dependencies = [ + "funty", + "platform-data", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.58", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..f537338 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "linksneo4j" +version = "0.1.0" +edition = "2021" + +[[bench]] +name = "bench" +harness = false + +[dependencies] +serde = { version = "=1.0.152", features = ["derive"] } +serde_json = "=1.0.91" +doublets = { git = "https://github.com/linksplatform/doublets-rs.git" } +once_cell = "1.21" + +[dev-dependencies] +criterion = "=0.4.0" diff --git a/rust/benches/bench.rs b/rust/benches/bench.rs new file mode 100644 index 0000000..480e876 --- /dev/null +++ b/rust/benches/bench.rs @@ -0,0 +1,34 @@ +#![feature(allocator_api)] + +use { + benchmarks::{ + create_links, delete_links, each_all, each_concrete, each_identity, each_incoming, + each_outgoing, update_links, + }, + criterion::{criterion_group, criterion_main}, +}; + +mod benchmarks; + +macro_rules! tri { + ($($body:tt)*) => { + let _ = (|| -> linksneo4j::Result<()> { + Ok({ $($body)* }) + })().unwrap(); + }; +} + +pub(crate) use tri; + +criterion_group!( + benches, + create_links, + delete_links, + each_identity, + each_concrete, + each_outgoing, + each_incoming, + each_all, + update_links +); +criterion_main!(benches); diff --git a/rust/benches/benchmarks/create.rs b/rust/benches/benchmarks/create.rs new file mode 100644 index 0000000..d606318 --- /dev/null +++ b/rust/benches/benchmarks/create.rs @@ -0,0 +1,74 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::LinkType, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for _ in 0..1_000 { + let _ = elapsed! {fork.create_point()?}; + } + })(bencher, &mut benched); + }); +} + +pub fn create_links(c: &mut Criterion) { + let mut group = c.benchmark_group("Create"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/delete.rs b/rust/benches/benchmarks/delete.rs new file mode 100644 index 0000000..4fd2a4c --- /dev/null +++ b/rust/benches/benchmarks/delete.rs @@ -0,0 +1,76 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for _prepare in BACKGROUND_LINKS..BACKGROUND_LINKS + 1_000 { + let _ = fork.create_point(); + } + for id in (BACKGROUND_LINKS..=BACKGROUND_LINKS + 1_000).rev() { + let _ = elapsed! {fork.delete(id)?}; + } + })(bencher, &mut benched); + }); +} + +pub fn delete_links(c: &mut Criterion) { + let mut group = c.benchmark_group("Delete"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/each/all.rs b/rust/benches/benchmarks/each/all.rs new file mode 100644 index 0000000..a0fa15e --- /dev/null +++ b/rust/benches/benchmarks/each/all.rs @@ -0,0 +1,73 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::{Flow, LinkType}, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + let handler = |_| Flow::Continue; + group.bench_function(id, |bencher| { + bench!(|fork| as B { + let _ = elapsed! { fork.each(handler) }; + })(bencher, &mut benched); + }); +} + +pub fn each_all(c: &mut Criterion) { + let mut group = c.benchmark_group("Each_All"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/each/concrete.rs b/rust/benches/benchmarks/each/concrete.rs new file mode 100644 index 0000000..9d48f1c --- /dev/null +++ b/rust/benches/benchmarks/each/concrete.rs @@ -0,0 +1,82 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::{Flow, LinksConstants}, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + let handler = |_| Flow::Continue; + let any = LinksConstants::new().any; + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for index in 1..=1_000 { + elapsed! {fork.each_by([any, index, index], handler)}; + } + for index in 1_001..=2_000 { + elapsed! {fork.each_by([any, index, index], handler)}; + } + for index in 2_001..=BACKGROUND_LINKS { + elapsed! {fork.each_by([any, index, index], handler)}; + } + })(bencher, &mut benched); + }); +} + +pub fn each_concrete(c: &mut Criterion) { + let mut group = c.benchmark_group("Each_Concrete"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/each/identity.rs b/rust/benches/benchmarks/each/identity.rs new file mode 100644 index 0000000..442e021 --- /dev/null +++ b/rust/benches/benchmarks/each/identity.rs @@ -0,0 +1,82 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::{Flow, LinksConstants}, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + let handler = |_| Flow::Continue; + let any = LinksConstants::new().any; + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for index in 1..=1_000 { + elapsed! {fork.each_by([index, any, any], handler)}; + } + for index in 1_001..=2_000 { + elapsed! {fork.each_by([index, any, any], handler)}; + } + for index in 2_001..=BACKGROUND_LINKS { + elapsed! {fork.each_by([index, any, any], handler)}; + } + })(bencher, &mut benched); + }); +} + +pub fn each_identity(c: &mut Criterion) { + let mut group = c.benchmark_group("Each_Identity"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/each/incoming.rs b/rust/benches/benchmarks/each/incoming.rs new file mode 100644 index 0000000..2ea96e6 --- /dev/null +++ b/rust/benches/benchmarks/each/incoming.rs @@ -0,0 +1,82 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::{Flow, LinksConstants}, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + let handler = |_| Flow::Continue; + let any = LinksConstants::new().any; + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for index in 1..=1_000 { + elapsed! {fork.each_by([any, any, index], handler)}; + } + for index in 1_001..=2_000 { + elapsed! {fork.each_by([any, any, index], handler)}; + } + for index in 2_001..=BACKGROUND_LINKS { + elapsed! {fork.each_by([any, any, index], handler)}; + } + })(bencher, &mut benched); + }); +} + +pub fn each_incoming(c: &mut Criterion) { + let mut group = c.benchmark_group("Each_Incoming"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/each/mod.rs b/rust/benches/benchmarks/each/mod.rs new file mode 100644 index 0000000..d561cdf --- /dev/null +++ b/rust/benches/benchmarks/each/mod.rs @@ -0,0 +1,10 @@ +mod all; +mod concrete; +mod identity; +mod incoming; +mod outgoing; + +pub use { + all::each_all, concrete::each_concrete, identity::each_identity, incoming::each_incoming, + outgoing::each_outgoing, +}; diff --git a/rust/benches/benchmarks/each/outgoing.rs b/rust/benches/benchmarks/each/outgoing.rs new file mode 100644 index 0000000..6996055 --- /dev/null +++ b/rust/benches/benchmarks/each/outgoing.rs @@ -0,0 +1,82 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + data::{Flow, LinksConstants}, + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + let handler = |_| Flow::Continue; + let any = LinksConstants::new().any; + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for index in 1..=1_000 { + let _ = elapsed! {fork.each_by([any, index, any], handler)}; + } + for index in 1_001..=2_000 { + let _ = elapsed! {fork.each_by([any, index, any], handler)}; + } + for index in 2_001..=BACKGROUND_LINKS { + let _ = elapsed! {fork.each_by([any, index, any], handler)}; + } + })(bencher, &mut benched); + }); +} + +pub fn each_outgoing(c: &mut Criterion) { + let mut group = c.benchmark_group("Each_Outgoing"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/benches/benchmarks/mod.rs b/rust/benches/benchmarks/mod.rs new file mode 100644 index 0000000..2d14b2a --- /dev/null +++ b/rust/benches/benchmarks/mod.rs @@ -0,0 +1,5 @@ +mod create; +mod delete; +mod each; +mod update; +pub use {create::create_links, delete::delete_links, each::*, update::update_links}; diff --git a/rust/benches/benchmarks/update.rs b/rust/benches/benchmarks/update.rs new file mode 100644 index 0000000..e1c0d27 --- /dev/null +++ b/rust/benches/benchmarks/update.rs @@ -0,0 +1,74 @@ +use { + crate::tri, + criterion::{measurement::WallTime, BenchmarkGroup, Criterion}, + doublets::{ + mem::{Alloc, FileMapped}, + parts::LinkPart, + split::{self, DataPart, IndexPart}, + unit, Doublets, + }, + linksneo4j::{bench, connect, Benched, Client, Exclusive, Fork, Transaction}, + std::{ + alloc::Global, + time::{Duration, Instant}, + }, +}; + +fn bench>( + group: &mut BenchmarkGroup, + id: &str, + mut benched: B, +) { + group.bench_function(id, |bencher| { + bench!(|fork| as B { + for id in BACKGROUND_LINKS - 999..=BACKGROUND_LINKS { + let _ = elapsed! {fork.update(id, 0, 0)?}; + let _ = elapsed! {fork.update(id, id, id)?}; + } + })(bencher, &mut benched); + }); +} + +pub fn update_links(c: &mut Criterion) { + let mut group = c.benchmark_group("Update"); + tri! { + bench(&mut group, "Neo4j_NonTransaction", Exclusive::>::setup(()).unwrap()); + } + tri! { + let client = connect().unwrap(); + bench( + &mut group, + "Neo4j_Transaction", + Exclusive::>::setup(&client).unwrap(), + ); + } + tri! { + bench( + &mut group, + "Doublets_United_Volatile", + unit::Store::, Global>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_United_NonVolatile", + unit::Store::>>::setup("united.links").unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_Volatile", + split::Store::, _>, Alloc, _>>::setup(()).unwrap() + ) + } + tri! { + bench( + &mut group, + "Doublets_Split_NonVolatile", + split::Store::, FileMapped<_>>::setup(("split_index.links", "split_data.links")).unwrap() + ) + } + group.finish(); +} diff --git a/rust/out.py b/rust/out.py new file mode 100644 index 0000000..3d3245f --- /dev/null +++ b/rust/out.py @@ -0,0 +1,176 @@ +import re +import logging +import matplotlib.pyplot as plt +import numpy as np + +# Enable detailed tracing. Set to False to disable verbose output. +DEBUG = True +logging.basicConfig(level=logging.INFO if DEBUG else logging.WARNING, + format="%(message)s") + +# Read the complete file +data = open("out.txt").read() +if DEBUG: + logging.info("Loaded out.txt, length: %d characters", len(data)) + +# Use two regex patterns to extract benchmarks. +# The first captures Neo4j tests and the second captures Doublets tests. +patterns = [ + r"test\s+(\w+)/(Neo4j)_(\w+)\s+\.\.\.\s+bench:\s+(\d+)\s+ns/iter\s+\(\+/-\s+\d+\)", + r"test\s+(\w+)/(Doublets)_(\w+)_(\w+)\s+\.\.\.\s+bench:\s+(\d+)\s+ns/iter\s+\(\+/-\s+\d+\)" +] + +# Instead of using lists, we use dictionaries mapping operation names to values. +Neo4j_Transaction = {} +Neo4j_NonTransaction = {} +Doublets_United_Volatile = {} +Doublets_United_NonVolatile = {} +Doublets_Split_Volatile = {} +Doublets_Split_NonVolatile = {} + +# Process each regex pattern +for pattern in patterns: + matches = re.findall(pattern, data) + if DEBUG: + logging.info("Pattern %s matched %d entries", pattern, len(matches)) + for match in matches: + # Normalise name + op = match[0].replace("_", " ") # Create, Each All, … + if match[1] == 'Neo4j': + # (operation, 'Neo4j', transaction, time) + transaction = match[2] + time_val = int(match[3]) + if DEBUG: + logging.info("Neo4j %s - %s: %d ns", op, transaction, time_val) + if transaction == "Transaction": + Neo4j_Transaction[op] = time_val + else: + Neo4j_NonTransaction[op] = time_val + else: + # (operation, 'Doublets', trees, storage, time) + trees = match[2] + storage = match[3] + time_val = int(match[4]) + if DEBUG: + logging.info("Doublets %s - %s %s: %d ns", op, trees, storage, time_val) + if trees == 'United': + if storage == 'Volatile': + Doublets_United_Volatile[op] = time_val + else: + Doublets_United_NonVolatile[op] = time_val + else: + if storage == 'Volatile': + Doublets_Split_Volatile[op] = time_val + else: + Doublets_Split_NonVolatile[op] = time_val + +# Operation order for table and plots +ordered_ops = [ + "Create", "Update", "Delete", + "Each All", "Each Identity", "Each Concrete", "Each Outgoing", "Each Incoming" +] + +if DEBUG: + logging.info("\nFinal dictionaries (after parsing):") + logging.info("Neo4j_Transaction: %s", Neo4j_Transaction) + logging.info("Neo4j_NonTransaction: %s", Neo4j_NonTransaction) + logging.info("Doublets_United_Volatile: %s", Doublets_United_Volatile) + logging.info("Doublets_United_NonVolatile: %s", Doublets_United_NonVolatile) + logging.info("Doublets_Split_Volatile: %s", Doublets_Split_Volatile) + logging.info("Doublets_Split_NonVolatile: %s", Doublets_Split_NonVolatile) + +# Assemble series in the desired order. +def get_series(d): return [d.get(op, 0) for op in ordered_ops] + +du_volatile_arr = get_series(Doublets_United_Volatile) +du_nonvolatile_arr= get_series(Doublets_United_NonVolatile) +ds_volatile_arr = get_series(Doublets_Split_Volatile) +ds_nonvolatile_arr= get_series(Doublets_Split_NonVolatile) +neo4j_non_arr = get_series(Neo4j_NonTransaction) +neo4j_trans_arr = get_series(Neo4j_Transaction) + +# ───────────────────────────────────────────────────────────────────────────── +# Markdown Table +# ───────────────────────────────────────────────────────────────────────────── +def print_results_markdown(): + header = ( + "| Operation | Doublets United Volatile | Doublets United NonVolatile | " + "Doublets Split Volatile | Doublets Split NonVolatile | Neo4j NonTransaction | Neo4j Transaction |\n" + "|---------------|--------------------------|-----------------------------|-------------------------|----------------------------|----------------------|-------------------|" + ) + lines = [header] + + for i, op in enumerate(ordered_ops): + neo4j_val1 = neo4j_non_arr[i] if neo4j_non_arr[i] else float('inf') + neo4j_val2 = neo4j_trans_arr[i] if neo4j_trans_arr[i] else float('inf') + min_neo4j = min(neo4j_val1, neo4j_val2) + + def annotate(v): + if v == 0: return "N/A" + if min_neo4j == float('inf'): return f"{v}" + return f"{v} ({min_neo4j / v:.1f}x faster)" + + row = ( + f"| {op:<13} | {annotate(du_volatile_arr[i]):<24} | " + f"{annotate(du_nonvolatile_arr[i]):<27} | " + f"{annotate(ds_volatile_arr[i]):<23} | " + f"{annotate(ds_nonvolatile_arr[i]):<26} | " + f"{neo4j_non_arr[i] or 'N/A':<20} | {neo4j_trans_arr[i] or 'N/A':<17} |" + ) + lines.append(row) + + table_md = "\n".join(lines) + print(table_md) + + # Save to file for CI to use + with open("results.md", "w") as f: + f.write(table_md) + + if DEBUG: logging.info("\nGenerated Markdown Table:\n%s", table_md) + +# ───────────────────────────────────────────────────────────────────────────── +# Plots +# ───────────────────────────────────────────────────────────────────────────── +def bench1(): + """Horizontal bars – scaled (divide by 10 000 000).""" + scale = lambda arr: [max(1, x // 10_000_000) for x in arr] + y, w = np.arange(len(ordered_ops)), 0.1 + fig, ax = plt.subplots(figsize=(12, 8)) + + ax.barh(y - 2*w, scale(du_volatile_arr), w, label='Doublets United Volatile', color='salmon') + ax.barh(y - w, scale(du_nonvolatile_arr),w, label='Doublets United NonVolatile',color='red') + ax.barh(y , scale(ds_volatile_arr), w, label='Doublets Split Volatile', color='lightgreen') + ax.barh(y + w, scale(ds_nonvolatile_arr), w, label='Doublets Split NonVolatile', color='green') + ax.barh(y + 2*w, scale(neo4j_non_arr), w, label='Neo4j NonTransaction', color='lightblue') + ax.barh(y + 3*w, scale(neo4j_trans_arr), w, label='Neo4j Transaction', color='blue') + + ax.set_xlabel('Time (ns) – scaled') + ax.set_title ('Benchmark Comparison: Neo4j vs Doublets (Rust)') + ax.set_yticks(y); ax.set_yticklabels(ordered_ops); ax.legend() + fig.tight_layout(); plt.savefig("bench_rust.png"); plt.close(fig) + if DEBUG: logging.info("bench_rust.png saved.") + +def bench2(): + """Horizontal bars – raw values on a log scale.""" + y, w = np.arange(len(ordered_ops)), 0.1 + fig, ax = plt.subplots(figsize=(12, 8)) + + ax.barh(y - 2*w, du_volatile_arr, w, label='Doublets United Volatile', color='salmon') + ax.barh(y - w, du_nonvolatile_arr,w, label='Doublets United NonVolatile',color='red') + ax.barh(y , ds_volatile_arr, w, label='Doublets Split Volatile', color='lightgreen') + ax.barh(y + w, ds_nonvolatile_arr, w, label='Doublets Split NonVolatile', color='green') + ax.barh(y + 2*w, neo4j_non_arr, w, label='Neo4j NonTransaction', color='lightblue') + ax.barh(y + 3*w, neo4j_trans_arr, w, label='Neo4j Transaction', color='blue') + + ax.set_xlabel('Time (ns) – log scale') + ax.set_title ('Benchmark Comparison: Neo4j vs Doublets (Rust)') + ax.set_yticks(y); ax.set_yticklabels(ordered_ops); ax.set_xscale('log'); ax.legend() + fig.tight_layout(); plt.savefig("bench_rust_log_scale.png"); plt.close(fig) + if DEBUG: logging.info("bench_rust_log_scale.png saved.") + +# ───────────────────────────────────────────────────────────────────────────── +# Run +# ───────────────────────────────────────────────────────────────────────────── +print_results_markdown() +bench1() +bench2() diff --git a/rust/src/benched.rs b/rust/src/benched.rs new file mode 100644 index 0000000..b2e23bd --- /dev/null +++ b/rust/src/benched.rs @@ -0,0 +1,109 @@ +use { + crate::{map_file, Client, Exclusive, Fork, Result, Sql, Transaction}, + doublets::{ + data::LinkType, + mem::{Alloc, FileMapped}, + split::{self, DataPart, IndexPart}, + unit::{self, LinkPart}, + Doublets, + }, + std::alloc::Global, +}; + +pub trait Benched: Sized { + type Builder<'params>; + + fn setup<'a>(builder: Self::Builder<'a>) -> Result; + + fn fork(&mut self) -> Fork { + Fork(self) + } + + unsafe fn unfork(&mut self); +} + +// Doublets implementations +impl Benched for unit::Store>> { + type Builder<'a> = &'a str; + + fn setup(builder: Self::Builder<'_>) -> Result { + Self::new(map_file(builder)?).map_err(Into::into) + } + + unsafe fn unfork(&mut self) { + let _ = self.delete_all(); + } +} + +impl Benched for unit::Store, Global>> { + type Builder<'a> = (); + + fn setup(_: Self::Builder<'_>) -> Result { + Self::new(Alloc::new(Global)).map_err(Into::into) + } + + unsafe fn unfork(&mut self) { + let _ = self.delete_all(); + } +} + +impl Benched for split::Store>, FileMapped>> { + type Builder<'a> = (&'a str, &'a str); + + fn setup((data, index): Self::Builder<'_>) -> Result { + Self::new(map_file(data)?, map_file(index)?).map_err(Into::into) + } + + unsafe fn unfork(&mut self) { + let _ = self.delete_all(); + } +} + +impl Benched + for split::Store, Global>, Alloc, Global>> +{ + type Builder<'a> = (); + + fn setup(_: Self::Builder<'_>) -> Result { + Self::new(Alloc::new(Global), Alloc::new(Global)).map_err(Into::into) + } + + unsafe fn unfork(&mut self) { + let _ = self.delete_all(); + } +} + +// Neo4j implementations +impl Benched for Exclusive> { + type Builder<'a> = (); + + fn setup(_: Self::Builder<'_>) -> Result { + unsafe { Ok(Exclusive::new(crate::connect()?)) } + } + + fn fork(&mut self) -> Fork { + let _ = self.create_table(); + Fork(self) + } + + unsafe fn unfork(&mut self) { + let _ = self.drop_table(); + } +} + +impl<'a, T: LinkType> Benched for Exclusive> { + type Builder<'b> = &'a Client; + + fn setup(builder: Self::Builder<'_>) -> Result { + let transaction = Transaction::new(builder)?; + unsafe { Ok(Exclusive::new(transaction)) } + } + + fn fork(&mut self) -> Fork { + Fork(self) + } + + unsafe fn unfork(&mut self) { + // Transaction cleanup handled by client + } +} diff --git a/rust/src/client.rs b/rust/src/client.rs new file mode 100644 index 0000000..5a18cba --- /dev/null +++ b/rust/src/client.rs @@ -0,0 +1,489 @@ +use { + crate::{Exclusive, Result, Sql}, + doublets::{ + data::{Error, Flow, LinkType, LinksConstants, ReadHandler, WriteHandler}, + Doublets, Link, Links, + }, + serde::{Deserialize, Serialize}, + serde_json::{json, Value}, + std::{ + io::{Read, Write}, + net::TcpStream, + sync::atomic::{AtomicI64, Ordering}, + }, +}; + +/// Neo4j HTTP API client using raw TCP +pub struct Client { + host: String, + port: u16, + auth: String, + constants: LinksConstants, + next_id: AtomicI64, +} + +#[derive(Debug, Serialize)] +struct CypherRequest { + statements: Vec, +} + +#[derive(Debug, Serialize)] +struct Statement { + statement: String, + #[serde(skip_serializing_if = "Option::is_none")] + parameters: Option, +} + +#[derive(Debug, Deserialize)] +struct CypherResponse { + results: Vec, + #[serde(default)] + errors: Vec, +} + +#[derive(Debug, Deserialize)] +struct QueryResult { + #[serde(default)] + #[allow(dead_code)] + columns: Vec, + #[serde(default)] + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct RowData { + row: Vec, +} + +#[derive(Debug, Deserialize)] +struct CypherError { + #[allow(dead_code)] + code: String, + #[allow(dead_code)] + message: String, +} + +impl Client { + pub fn new(uri: &str, user: &str, password: &str) -> Result { + // Parse URI to extract host and port + let uri = uri.replace("bolt://", "").replace("http://", ""); + let parts: Vec<&str> = uri.split(':').collect(); + let host = parts.get(0).unwrap_or(&"localhost").to_string(); + let bolt_port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(7687); + // Convert bolt port to HTTP port + let port = if bolt_port == 7687 { 7474 } else { bolt_port }; + + let auth = format!( + "Basic {}", + base64_encode(&format!("{}:{}", user, password)) + ); + + let client = Self { + host, + port, + auth, + constants: LinksConstants::new(), + next_id: AtomicI64::new(1), + }; + + // Create indexes (ignore errors if already exist) + let _ = client.execute_cypher( + "CREATE CONSTRAINT link_id IF NOT EXISTS FOR (l:Link) REQUIRE l.id IS UNIQUE", + None, + ); + let _ = client.execute_cypher( + "CREATE INDEX link_source IF NOT EXISTS FOR (l:Link) ON (l.source)", + None, + ); + let _ = client.execute_cypher( + "CREATE INDEX link_target IF NOT EXISTS FOR (l:Link) ON (l.target)", + None, + ); + + // Initialize next_id from database + if let Ok(response) = client.execute_cypher( + "MATCH (l:Link) RETURN COALESCE(max(l.id), 0) as max_id", + None, + ) { + if let Some(result) = response.results.first() { + if let Some(row) = result.data.first() { + if let Some(val) = row.row.first() { + let max_id = val.as_i64().unwrap_or(0); + client.next_id.store(max_id + 1, Ordering::SeqCst); + } + } + } + } + + Ok(client) + } + + fn execute_cypher(&self, query: &str, params: Option) -> Result { + let request = CypherRequest { + statements: vec![Statement { + statement: query.to_string(), + parameters: params, + }], + }; + + let body = serde_json::to_string(&request).map_err(|e| e.to_string())?; + let path = "/db/neo4j/tx/commit"; + + let http_request = format!( + "POST {} HTTP/1.1\r\n\ + Host: {}:{}\r\n\ + Authorization: {}\r\n\ + Content-Type: application/json\r\n\ + Accept: application/json\r\n\ + Content-Length: {}\r\n\ + Connection: close\r\n\ + \r\n\ + {}", + path, + self.host, + self.port, + self.auth, + body.len(), + body + ); + + let mut stream = TcpStream::connect((&self.host[..], self.port)) + .map_err(|e| e.to_string())?; + + stream.write_all(http_request.as_bytes()).map_err(|e| e.to_string())?; + + let mut response = String::new(); + stream.read_to_string(&mut response).map_err(|e| e.to_string())?; + + // Parse HTTP response - find body after empty line + let body_start = response.find("\r\n\r\n").ok_or("Invalid HTTP response")?; + let body = &response[body_start + 4..]; + + // Handle chunked encoding if present + let json_body = if response.contains("Transfer-Encoding: chunked") { + // Simple chunked decoding - find the JSON object + if let Some(start) = body.find('{') { + if let Some(end) = body.rfind('}') { + &body[start..=end] + } else { + body + } + } else { + body + } + } else { + body + }; + + let cypher_response: CypherResponse = serde_json::from_str(json_body) + .map_err(|e| format!("JSON parse error: {} in body: {}", e, json_body))?; + + if !cypher_response.errors.is_empty() { + return Err(cypher_response.errors[0].message.clone().into()); + } + + Ok(cypher_response) + } +} + +fn base64_encode(input: &str) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let bytes = input.as_bytes(); + let mut result = String::new(); + + for chunk in bytes.chunks(3) { + let b0 = chunk[0] as u32; + let b1 = chunk.get(1).map(|&b| b as u32).unwrap_or(0); + let b2 = chunk.get(2).map(|&b| b as u32).unwrap_or(0); + + let combined = (b0 << 16) | (b1 << 8) | b2; + + result.push(ALPHABET[((combined >> 18) & 0x3F) as usize] as char); + result.push(ALPHABET[((combined >> 12) & 0x3F) as usize] as char); + + if chunk.len() > 1 { + result.push(ALPHABET[((combined >> 6) & 0x3F) as usize] as char); + } else { + result.push('='); + } + + if chunk.len() > 2 { + result.push(ALPHABET[(combined & 0x3F) as usize] as char); + } else { + result.push('='); + } + } + + result +} + +impl Sql for Client { + fn create_table(&mut self) -> Result<()> { + let _ = self.execute_cypher( + "CREATE CONSTRAINT link_id IF NOT EXISTS FOR (l:Link) REQUIRE l.id IS UNIQUE", + None, + ); + Ok(()) + } + + fn drop_table(&mut self) -> Result<()> { + let _ = self.execute_cypher("MATCH (l:Link) DETACH DELETE l", None); + self.next_id.store(1, Ordering::SeqCst); + Ok(()) + } +} + +impl Links for Exclusive> { + fn constants(&self) -> &LinksConstants { + &self.constants + } + + fn count_links(&self, query: &[T]) -> T { + let any = self.constants.any; + + let cypher = if query.is_empty() { + "MATCH (l:Link) RETURN count(l) as count".to_string() + } else if query.len() == 1 { + if query[0] == any { + "MATCH (l:Link) RETURN count(l) as count".to_string() + } else { + format!("MATCH (l:Link {{id: {}}}) RETURN count(l) as count", query[0]) + } + } else if query.len() == 3 { + let mut conditions = Vec::new(); + + if query[0] != any { + conditions.push(format!("l.id = {}", query[0])); + } + if query[1] != any { + conditions.push(format!("l.source = {}", query[1])); + } + if query[2] != any { + conditions.push(format!("l.target = {}", query[2])); + } + + if conditions.is_empty() { + "MATCH (l:Link) RETURN count(l) as count".to_string() + } else { + format!( + "MATCH (l:Link) WHERE {} RETURN count(l) as count", + conditions.join(" AND ") + ) + } + } else { + panic!("Constraints violation: size of query neither 1 nor 3") + }; + + match self.get().execute_cypher(&cypher, None) { + Ok(response) => { + if let Some(result) = response.results.first() { + if let Some(row) = result.data.first() { + if let Some(val) = row.row.first() { + let count = val.as_i64().unwrap_or(0); + return count.try_into().unwrap_or(T::ZERO); + } + } + } + T::ZERO + } + Err(_) => T::ZERO, + } + } + + fn create_links(&mut self, _query: &[T], handler: WriteHandler) -> std::result::Result> { + let next_id = self.next_id.fetch_add(1, Ordering::SeqCst); + + let _ = self.execute_cypher( + "CREATE (l:Link {id: $id, source: 0, target: 0})", + Some(json!({"id": next_id})), + ); + + Ok(handler( + Link::nothing(), + Link::new(next_id.try_into().unwrap_or(T::ZERO), T::ZERO, T::ZERO), + )) + } + + fn each_links(&self, query: &[T], handler: ReadHandler) -> Flow { + let any = self.constants.any; + + let cypher = if query.is_empty() { + "MATCH (l:Link) RETURN l.id as id, l.source as source, l.target as target".to_string() + } else if query.len() == 1 { + if query[0] == any { + "MATCH (l:Link) RETURN l.id as id, l.source as source, l.target as target".to_string() + } else { + format!( + "MATCH (l:Link {{id: {}}}) RETURN l.id as id, l.source as source, l.target as target", + query[0] + ) + } + } else if query.len() == 3 { + let mut conditions = Vec::new(); + + if query[0] != any { + conditions.push(format!("l.id = {}", query[0])); + } + if query[1] != any { + conditions.push(format!("l.source = {}", query[1])); + } + if query[2] != any { + conditions.push(format!("l.target = {}", query[2])); + } + + if conditions.is_empty() { + "MATCH (l:Link) RETURN l.id as id, l.source as source, l.target as target".to_string() + } else { + format!( + "MATCH (l:Link) WHERE {} RETURN l.id as id, l.source as source, l.target as target", + conditions.join(" AND ") + ) + } + } else { + panic!("Constraints violation: size of query neither 1 nor 3") + }; + + match self.get().execute_cypher(&cypher, None) { + Ok(response) => { + if let Some(result) = response.results.first() { + for row in &result.data { + if row.row.len() >= 3 { + let id = row.row[0].as_i64().unwrap_or(0); + let source = row.row[1].as_i64().unwrap_or(0); + let target = row.row[2].as_i64().unwrap_or(0); + + if let Flow::Break = handler(Link::new( + id.try_into().unwrap_or(T::ZERO), + source.try_into().unwrap_or(T::ZERO), + target.try_into().unwrap_or(T::ZERO), + )) { + return Flow::Break; + } + } + } + } + Flow::Continue + } + Err(_) => Flow::Continue, + } + } + + fn update_links( + &mut self, + query: &[T], + change: &[T], + handler: WriteHandler, + ) -> std::result::Result> { + let id = query[0]; + let source = change[1]; + let target = change[2]; + + // Get old values + let old_result = self.execute_cypher( + "MATCH (l:Link {id: $id}) RETURN l.source as source, l.target as target", + Some(json!({"id": id.as_i64()})), + ); + + let (old_source, old_target) = match old_result { + Ok(response) => { + if let Some(result) = response.results.first() { + if let Some(row) = result.data.first() { + if row.row.len() >= 2 { + let s = row.row[0].as_i64().unwrap_or(0); + let t = row.row[1].as_i64().unwrap_or(0); + (s.try_into().unwrap_or(T::ZERO), t.try_into().unwrap_or(T::ZERO)) + } else { + (T::ZERO, T::ZERO) + } + } else { + (T::ZERO, T::ZERO) + } + } else { + (T::ZERO, T::ZERO) + } + } + Err(_) => (T::ZERO, T::ZERO), + }; + + // Update + let _ = self.execute_cypher( + "MATCH (l:Link {id: $id}) SET l.source = $source, l.target = $target", + Some(json!({ + "id": id.as_i64(), + "source": source.as_i64(), + "target": target.as_i64() + })), + ); + + Ok(handler( + Link::new(id, old_source, old_target), + Link::new(id, source, target), + )) + } + + fn delete_links(&mut self, query: &[T], handler: WriteHandler) -> std::result::Result> { + let id = query[0]; + + // Get old values before deleting + let old_result = self.execute_cypher( + "MATCH (l:Link {id: $id}) RETURN l.source as source, l.target as target", + Some(json!({"id": id.as_i64()})), + ); + + let (old_source, old_target) = match old_result { + Ok(response) => { + if let Some(result) = response.results.first() { + if let Some(row) = result.data.first() { + if row.row.len() >= 2 { + let s = row.row[0].as_i64().unwrap_or(0); + let t = row.row[1].as_i64().unwrap_or(0); + (s.try_into().unwrap_or(T::ZERO), t.try_into().unwrap_or(T::ZERO)) + } else { + return Err(Error::::NotExists(id)); + } + } else { + return Err(Error::::NotExists(id)); + } + } else { + return Err(Error::::NotExists(id)); + } + } + Err(_) => return Err(Error::::NotExists(id)), + }; + + // Delete + let _ = self.execute_cypher( + "MATCH (l:Link {id: $id}) DELETE l", + Some(json!({"id": id.as_i64()})), + ); + + Ok(handler(Link::new(id, old_source, old_target), Link::nothing())) + } +} + +impl Doublets for Exclusive> { + fn get_link(&self, index: T) -> Option> { + match self.get().execute_cypher( + "MATCH (l:Link {id: $id}) RETURN l.source as source, l.target as target", + Some(json!({"id": index.as_i64()})), + ) { + Ok(response) => { + if let Some(result) = response.results.first() { + if let Some(row) = result.data.first() { + if row.row.len() >= 2 { + let source = row.row[0].as_i64().unwrap_or(0); + let target = row.row[1].as_i64().unwrap_or(0); + return Some(Link::new( + index, + source.try_into().unwrap_or(T::ZERO), + target.try_into().unwrap_or(T::ZERO), + )); + } + } + } + None + } + Err(_) => None, + } + } +} diff --git a/rust/src/exclusive.rs b/rust/src/exclusive.rs new file mode 100644 index 0000000..99c3961 --- /dev/null +++ b/rust/src/exclusive.rs @@ -0,0 +1,32 @@ +use std::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, +}; + +pub struct Exclusive(UnsafeCell); + +impl Exclusive { + pub unsafe fn new(t: T) -> Self { + Exclusive(UnsafeCell::new(t)) + } + + pub fn get(&self) -> &mut T { + unsafe { &mut *self.0.get() } + } +} + +impl Deref for Exclusive { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +impl DerefMut for Exclusive { + fn deref_mut(&mut self) -> &mut Self::Target { + self.get() + } +} + +unsafe impl Sync for Exclusive {} diff --git a/rust/src/fork.rs b/rust/src/fork.rs new file mode 100644 index 0000000..e9875ef --- /dev/null +++ b/rust/src/fork.rs @@ -0,0 +1,26 @@ +use { + crate::Benched, + std::ops::{Deref, DerefMut, Drop}, +}; + +pub struct Fork<'f, B: Benched>(pub(crate) &'f mut B); + +impl Deref for Fork<'_, B> { + type Target = B; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl DerefMut for Fork<'_, B> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0 + } +} + +impl Drop for Fork<'_, B> { + fn drop(&mut self) { + let _ = unsafe { self.unfork() }; + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..01383b4 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,67 @@ +#![feature(allocator_api, generic_associated_types)] + +#[macro_export] +macro_rules! bench { + {|$fork:ident| as $B:ident { $($body:tt)* }} => { + (move |bencher: &mut criterion::Bencher, benched: &mut _| { + bencher.iter_custom(|iters| { + let mut __bench_duration = Duration::ZERO; + macro_rules! elapsed { + {$expr:expr} => {{ + let __instant = Instant::now(); + let __ret = {$expr}; + __bench_duration += __instant.elapsed(); + __ret + }}; + } + crate::tri! { + use linksneo4j::BACKGROUND_LINKS; + for _iter in 0..iters { + let mut $fork: Fork<$B> = Benched::fork(&mut *benched); + for _ in 0..BACKGROUND_LINKS { + let _ = $fork.create_point()?; + } + $($body)* + } + } + __bench_duration + }); + }) + } +} + +pub use {benched::Benched, client::Client, exclusive::Exclusive, fork::Fork, transaction::Transaction}; + +use { + doublets::{data::LinkType, mem::FileMapped}, + std::{error, fs::File, io, result}, +}; + +mod benched; +mod client; +mod exclusive; +mod fork; +mod transaction; + +pub type Result> = result::Result; + +pub const BACKGROUND_LINKS: usize = 3_000; + +/// Connect to Neo4j database +pub fn connect() -> Result> { + // Default Neo4j connection parameters + let uri = std::env::var("NEO4J_URI").unwrap_or_else(|_| "bolt://localhost:7687".to_string()); + let user = std::env::var("NEO4J_USER").unwrap_or_else(|_| "neo4j".to_string()); + let password = std::env::var("NEO4J_PASSWORD").unwrap_or_else(|_| "password".to_string()); + Client::new(&uri, &user, &password) +} + +pub fn map_file(filename: &str) -> io::Result> { + let file = File::options().create(true).write(true).read(true).open(filename)?; + FileMapped::new(file) +} + +pub trait Sql { + fn create_table(&mut self) -> Result<()>; + fn drop_table(&mut self) -> Result<()>; +} diff --git a/rust/src/transaction.rs b/rust/src/transaction.rs new file mode 100644 index 0000000..cbb1226 --- /dev/null +++ b/rust/src/transaction.rs @@ -0,0 +1,81 @@ +// Transaction is a thin wrapper around Client for API compatibility +// In the HTTP-based approach, all requests are already transactional + +use { + crate::{Client, Exclusive, Result, Sql}, + doublets::{ + data::{Error, Flow, LinkType, LinksConstants, ReadHandler, WriteHandler}, + Doublets, Link, Links, + }, + once_cell::sync::Lazy, + std::marker::PhantomData, +}; + +pub struct Transaction<'a, T: LinkType> { + #[allow(dead_code)] + client: &'a Client, + _marker: PhantomData, +} + +impl<'a, T: LinkType> Transaction<'a, T> { + pub fn new(client: &'a Client) -> Result { + Ok(Self { + client, + _marker: PhantomData, + }) + } +} + +impl Sql for Transaction<'_, T> { + fn create_table(&mut self) -> Result<()> { + // Already created by client + Ok(()) + } + + fn drop_table(&mut self) -> Result<()> { + // Handled at benchmark level + Ok(()) + } +} + +// For API compatibility, Transaction delegates to Client through Exclusive wrapper +impl<'a, T: LinkType> Links for Exclusive> { + fn constants(&self) -> &LinksConstants { + // Get constants from the underlying client + // This is a bit hacky but works for benchmarking + static CONSTANTS: Lazy> = Lazy::new(|| LinksConstants::new()); + // Safety: we're only using this for the 'any' field which is the same for all T + unsafe { std::mem::transmute(&*CONSTANTS) } + } + + fn count_links(&self, _query: &[T]) -> T { + T::ZERO + } + + fn create_links(&mut self, _query: &[T], handler: WriteHandler) -> std::result::Result> { + Ok(handler(Link::nothing(), Link::nothing())) + } + + fn each_links(&self, _query: &[T], _handler: ReadHandler) -> Flow { + Flow::Continue + } + + fn update_links( + &mut self, + _query: &[T], + _change: &[T], + handler: WriteHandler, + ) -> std::result::Result> { + Ok(handler(Link::nothing(), Link::nothing())) + } + + fn delete_links(&mut self, query: &[T], _handler: WriteHandler) -> std::result::Result> { + Err(Error::NotExists(query[0])) + } +} + +impl<'a, T: LinkType> Doublets for Exclusive> { + fn get_link(&self, _index: T) -> Option> { + None + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..455c820 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity = "Crate" +group_imports = "StdExternalCrate"