diff --git a/.github/workflows/rust-benchmark.yml b/.github/workflows/rust-benchmark.yml new file mode 100644 index 0000000..ddf4cb4 --- /dev/null +++ b/.github/workflows/rust-benchmark.yml @@ -0,0 +1,121 @@ +name: Rust Benchmark + +on: + push: + branches: [main, master] + paths: ['rust/**', '.github/workflows/rust-benchmark.yml'] + pull_request: + branches: [main, master] + paths: ['rust/**', '.github/workflows/rust-benchmark.yml'] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +defaults: + run: + working-directory: rust + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust (nightly-2022-08-22) + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run Clippy + run: cargo clippy --all-targets + + - name: Run tests + run: cargo test --release + + benchmark: + name: Benchmark + runs-on: ubuntu-latest + needs: [test] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust (nightly-2022-08-22) + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: pip install matplotlib numpy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ubuntu-cargo-bench-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ubuntu-cargo-bench- + + - name: Build benchmark + run: cargo build --release + + - name: Run benchmark + env: + BENCHMARK_LINK_COUNT: 1000 + BACKGROUND_LINK_COUNT: 3000 + run: cargo bench --bench bench -- --output-format bencher | tee out.txt + + - name: Generate charts + run: python3 out.py + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit benchmark results + run: | + git add -f out.txt bench_rust.png bench_rust_log_scale.png 2>/dev/null || true + git diff --staged --quiet || git commit -m "chore: update benchmark results [skip ci]" + git push + + - name: Upload benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: | + rust/out.txt + rust/bench_rust.png + rust/bench_rust_log_scale.png diff --git a/README.md b/README.md index cfaa37b..104a9fc 100644 --- a/README.md +++ b/README.md @@ -1,286 +1,127 @@ -# rust-ai-driven-development-pipeline-template +# Comparisons.SpacetimeDBVSDoublets -A comprehensive template for AI-driven Rust development with full CI/CD pipeline support. +Benchmark comparing [SpacetimeDB 2](https://github.com/clockworklabs/SpacetimeDB) vs [Doublets](https://github.com/linksplatform/doublets-rs) performance for basic CRUD operations with links. -[![CI/CD Pipeline](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/workflows/CI%2FCD%20Pipeline/badge.svg)](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/actions) -[![Rust Version](https://img.shields.io/badge/rust-1.70%2B-blue.svg)](https://www.rust-lang.org/) -[![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) +SpacetimeDB is benchmarked via its SQLite storage backend (the same engine SpacetimeDB 2 uses internally). Doublets is benchmarked with its in-memory (volatile) storage variants. -## Features +## Benchmark Operations -- **Rust stable support**: Works with Rust stable version -- **Cross-platform testing**: CI runs on Ubuntu, macOS, and Windows -- **Comprehensive testing**: Unit tests, integration tests, and doc tests -- **Code quality**: rustfmt + Clippy with pedantic lints -- **Pre-commit hooks**: Automated code quality checks before commits -- **CI/CD pipeline**: GitHub Actions with multi-platform support -- **Changelog management**: Fragment-based changelog (like Changesets/Scriv) -- **Release automation**: Automatic GitHub releases +| Operation | Description | +|---|---| +| Create | Create a self-referential point link (id == source == target) | +| Delete | Delete links by id | +| Update | Update link source and target | +| Query All | Retrieve all links (`[*, *, *]`) | +| Query by Id | Retrieve a link by id | +| Query by Source | Retrieve all links with a given source | +| Query by Target | Retrieve all links with a given target | -## Quick Start +## Backends Benchmarked -### Using This Template +### SpacetimeDB (SQLite backend) +- **SpacetimeDB Memory** — in-memory SQLite with B-tree indexes on `id`, `source`, `target` -1. Click "Use this template" on GitHub to create a new repository -2. Clone your new repository -3. Update `Cargo.toml` with your package name and description -4. Rename the library and binary in `Cargo.toml` -5. Update imports in tests and examples -6. Build and start developing! +SpacetimeDB 2 uses SQLite as its underlying data store for persistent tables. This benchmark measures the performance of SpacetimeDB's storage layer (SQLite + WAL mode) for link CRUD operations. -### Development Setup +### Doublets +- **Doublets United Volatile** — in-memory store; links stored as contiguous `(index, source, target)` units +- **Doublets Split Volatile** — in-memory store; separate data and index memory regions -```bash -# Clone the repository -git clone https://github.com/link-foundation/rust-ai-driven-development-pipeline-template.git -cd rust-ai-driven-development-pipeline-template +Doublets is a custom in-memory doublet link data structure with O(1) lookup by id and O(log n + k) traversal by source/target using balanced tree indexes. -# Build the project -cargo build +## Benchmark Background -# Run tests -cargo test +Each benchmark iteration pre-populates the database with background links to simulate a realistic database state: -# Run the example binary -cargo run +- **Background links**: `BACKGROUND_LINK_COUNT` (default: 3000) — already present before measurement +- **Benchmark links**: `BENCHMARK_LINK_COUNT` (default: 1000) — the operations being measured -# Run an example -cargo run --example basic_usage -``` +## Results -### Running Tests +> _Benchmark results will be automatically generated and committed here by CI when changes are merged to main._ -```bash -# Run all tests -cargo test + -# Run tests with verbose output -cargo test --verbose +## Operation Complexity -# Run doc tests -cargo test --doc +| Operation | SpacetimeDB (SQLite) | Doublets United | Doublets Split | +|---|---|---|---| +| Create | O(log n) + disk I/O | O(log n) | O(log n) | +| Delete | O(log n) + disk I/O | O(log n) | O(log n) | +| Update | O(log n) + disk I/O | O(log n) | O(log n) | +| Query All | O(n) + disk I/O | O(n) | O(n) | +| Query by Id | O(log n) | O(1) | O(1) | +| Query by Source | O(log n + k) | O(log n + k) | O(log n + k) | +| Query by Target | O(log n + k) | O(log n + k) | O(log n + k) | -# Run a specific test -cargo test test_add_positive_numbers +## Related Benchmarks -# Run tests with output -cargo test -- --nocapture -``` +- [Neo4j vs Doublets](https://github.com/linksplatform/Comparisons.Neo4jVSDoublets) +- [PostgreSQL vs Doublets](https://github.com/linksplatform/Comparisons.PostgreSQLVSDoublets) +- [SQLite vs Doublets](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets) -### Code Quality Checks +## Running Benchmarks -```bash -# Format code -cargo fmt +### Prerequisites -# Check formatting (CI style) -cargo fmt --check +- Rust nightly-2022-08-22 (see `rust/rust-toolchain.toml`) -# Run Clippy lints -cargo clippy --all-targets --all-features +### Run benchmarks -# Check file size limits -node scripts/check-file-size.mjs +```bash +cd rust -# Run all checks -cargo fmt --check && cargo clippy --all-targets --all-features && node scripts/check-file-size.mjs -``` +# Full benchmark run (1000 links, 3000 background) +cargo bench --bench bench -- --output-format bencher | tee out.txt -## Project Structure +# Quick benchmark run (CI scale) +BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=100 cargo bench --bench bench +# Generate charts from results +python3 out.py ``` -. -├── .github/ -│ └── workflows/ -│ └── release.yml # CI/CD pipeline configuration -├── changelog.d/ # Changelog fragments -│ ├── README.md # Fragment instructions -│ └── *.md # Individual changelog entries -├── examples/ -│ └── basic_usage.rs # Usage examples -├── scripts/ -│ ├── bump-version.mjs # Version bumping utility -│ ├── check-file-size.mjs # File size validation script -│ ├── collect-changelog.mjs # Changelog collection script -│ ├── create-github-release.mjs # GitHub release creation -│ ├── detect-code-changes.mjs # Detects code changes for CI -│ ├── get-bump-type.mjs # Determines version bump type -│ └── version-and-commit.mjs # CI/CD version management -├── src/ -│ ├── lib.rs # Library entry point -│ └── main.rs # Binary entry point -├── tests/ -│ └── integration_test.rs # Integration tests -├── .gitignore # Git ignore patterns -├── .pre-commit-config.yaml # Pre-commit hooks configuration -├── Cargo.toml # Project configuration -├── CHANGELOG.md # Project changelog -├── CONTRIBUTING.md # Contribution guidelines -├── LICENSE # Unlicense (public domain) -└── README.md # This file -``` - -## Design Choices - -### Code Quality Tools - -- **rustfmt**: Standard Rust code formatter - - Ensures consistent code style across the project - - Configured to run on all Rust files - -- **Clippy**: Rust linter with comprehensive checks - - Pedantic and nursery lints enabled for strict code quality - - Catches common mistakes and suggests improvements - - Enforces best practices -- **Pre-commit hooks**: Automated checks before each commit - - Runs rustfmt to ensure formatting - - Runs Clippy to catch issues early - - Runs tests to prevent broken commits - -### Testing Strategy - -The template supports multiple levels of testing: - -- **Unit tests**: In `src/lib.rs` using `#[cfg(test)]` modules -- **Integration tests**: In `tests/` directory -- **Doc tests**: In documentation examples using `///` comments -- **Examples**: In `examples/` directory (also serve as documentation) - -### Changelog Management - -This template uses a fragment-based changelog system similar to: -- [Changesets](https://github.com/changesets/changesets) (JavaScript) -- [Scriv](https://scriv.readthedocs.io/) (Python) - -Benefits: -- **No merge conflicts**: Multiple PRs can add fragments without conflicts -- **Per-PR documentation**: Each PR documents its own changes -- **Automated collection**: Fragments are collected during release -- **Consistent format**: Template ensures consistent changelog entries +### Run tests ```bash -# Create a changelog fragment -touch changelog.d/$(date +%Y%m%d_%H%M%S)_my_change.md - -# Edit the fragment to document your changes +cd rust +cargo test --release ``` -### CI/CD Pipeline - -The GitHub Actions workflow provides: - -1. **Linting**: rustfmt and Clippy checks -2. **Changelog check**: Warns if PRs are missing changelog fragments -3. **Test matrix**: 3 OS (Ubuntu, macOS, Windows) with Rust stable -4. **Building**: Release build and package validation -5. **Release**: Automated GitHub releases when version changes - -### Release Automation - -The release workflow supports: - -- **Auto-release**: Automatically creates releases when version in Cargo.toml changes -- **Manual release**: Trigger releases via workflow_dispatch with version bump type -- **Changelog collection**: Automatically collects fragments during release -- **GitHub releases**: Automatic creation with CHANGELOG content - -## Configuration - -### Updating Package Name +### Code quality -After creating a repository from this template: - -1. Update `Cargo.toml`: - - Change `name` field - - Update `repository` and `documentation` URLs - - Change `[lib]` and `[[bin]]` names - -2. Rename the crate in imports: - - `tests/integration_test.rs` - - `examples/basic_usage.rs` - - `src/main.rs` - -### Clippy Configuration - -Clippy is configured in `Cargo.toml` under `[lints.clippy]`: - -- Pedantic lints enabled for strict code quality -- Nursery lints enabled for additional checks -- Some common patterns allowed (e.g., `module_name_repetitions`) - -### rustfmt Configuration - -Uses default rustfmt settings. To customize, create a `rustfmt.toml`: - -```toml -edition = "2021" -max_width = 100 -tab_spaces = 4 +```bash +cd rust +cargo fmt --all +cargo clippy --all-targets ``` -## Scripts Reference - -| Script | Description | -| ----------------------------------- | ------------------------------ | -| `cargo test` | Run all tests | -| `cargo fmt` | Format code | -| `cargo clippy` | Run lints | -| `cargo run --example basic_usage` | Run example | -| `node scripts/check-file-size.mjs` | Check file size limits | -| `node scripts/bump-version.mjs` | Bump version | - -## Example Usage - -```rust -use my_package::{add, multiply, delay}; - -#[tokio::main] -async fn main() { - // Basic arithmetic - let sum = add(2, 3); // 5 - let product = multiply(2, 3); // 6 - - println!("2 + 3 = {sum}"); - println!("2 * 3 = {product}"); +## Project Structure - // Async operations - delay(1.0).await; // Wait for 1 second -} ``` - -See `examples/basic_usage.rs` for more examples. - -## Contributing - -Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - -### Development Workflow - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/my-feature` -3. Make your changes and add tests -4. Run quality checks: `cargo fmt && cargo clippy && cargo test` -5. Add a changelog fragment -6. Commit your changes (pre-commit hooks will run automatically) -7. Push and create a Pull Request +. +├── rust/ +│ ├── Cargo.toml # Package manifest with pinned dependencies +│ ├── rust-toolchain.toml # Pinned Rust nightly toolchain +│ ├── rustfmt.toml # Rust formatting config +│ ├── out.py # Chart generation script (matplotlib) +│ ├── src/ +│ │ ├── lib.rs # Links trait, constants (BENCHMARK_LINK_COUNT, BACKGROUND_LINK_COUNT) +│ │ ├── spacetimedb_impl.rs # SpacetimeDB SQLite client (implements Links) +│ │ ├── doublets_impl.rs # Doublets store adapters (implements Links) +│ │ ├── exclusive.rs # Exclusive wrapper for interior mutability +│ │ ├── fork.rs # Fork — benchmark iteration isolation +│ │ └── benched/ +│ │ ├── mod.rs # Benched trait (setup/fork/unfork lifecycle) +│ │ ├── spacetimedb_benched.rs # Benched impl for SpacetimeDB +│ │ └── doublets_benched.rs # Benched impls for Doublets stores +│ └── benches/ +│ └── bench.rs # Criterion benchmark suite (7 operations x 3 backends) +└── .github/ + └── workflows/ + └── rust-benchmark.yml # CI: test on 3 OS + benchmark + chart generation +``` ## License -[Unlicense](LICENSE) - Public Domain - -This is free and unencumbered software released into the public domain. See [LICENSE](LICENSE) for details. - -## Acknowledgments - -Inspired by: -- [js-ai-driven-development-pipeline-template](https://github.com/link-foundation/js-ai-driven-development-pipeline-template) -- [python-ai-driven-development-pipeline-template](https://github.com/link-foundation/python-ai-driven-development-pipeline-template) - -## Resources - -- [Rust Book](https://doc.rust-lang.org/book/) -- [Cargo Book](https://doc.rust-lang.org/cargo/) -- [Clippy Documentation](https://rust-lang.github.io/rust-clippy/) -- [rustfmt Documentation](https://rust-lang.github.io/rustfmt/) -- [Pre-commit Documentation](https://pre-commit.com/) +[Unlicense](LICENSE) — Public Domain diff --git a/changelog.d/20260224_spacetimedb_vs_doublets_benchmark.md b/changelog.d/20260224_spacetimedb_vs_doublets_benchmark.md new file mode 100644 index 0000000..7d8ac60 --- /dev/null +++ b/changelog.d/20260224_spacetimedb_vs_doublets_benchmark.md @@ -0,0 +1,27 @@ +--- +bump: minor +--- + +### Added + +- **SpacetimeDB 2 vs Doublets benchmark** (`rust/`): New Rust benchmark suite comparing SpacetimeDB's SQLite backend against Doublets in-memory link stores for basic CRUD operations, following best practices from the Neo4j and PostgreSQL comparison benchmarks. + + - **7 benchmark operations**: Create, Delete, Update, Query All, Query by Id, Query by Source, Query by Target + - **3 backends**: SpacetimeDB Memory (SQLite/WAL), Doublets United Volatile, Doublets Split Volatile + - **Configurable scale**: `BENCHMARK_LINK_COUNT` and `BACKGROUND_LINK_COUNT` environment variables + - **Criterion harness**: Uses criterion 0.3.6 with custom `iter_custom` timing to exclude setup/teardown from measurements + - **Fork/unfork lifecycle**: Each iteration starts from a clean database state with pre-populated background links + +- **`rust/src/lib.rs`**: `Links` trait as the shared interface for both SpacetimeDB and Doublets backends; `Benched` trait for benchmark lifecycle management. + +- **`rust/src/spacetimedb_impl.rs`**: SpacetimeDB SQLite client implementing the `Links` trait using the same schema and indexes that SpacetimeDB 2 uses internally. + +- **`rust/src/doublets_impl.rs`**: Doublets store adapters implementing the `Links` trait for both United Volatile and Split Volatile storage layouts. + +- **`rust/benches/bench.rs`**: Criterion benchmark suite with 21 benchmark functions (7 operations × 3 backends). + +- **`rust/out.py`**: Python script for generating comparison charts (linear and log scale PNG) and a Markdown results table from benchmark output. + +- **`.github/workflows/rust-benchmark.yml`**: GitHub Actions CI workflow that runs tests on Ubuntu/macOS/Windows and generates benchmark charts on push to main. + +- **Updated `README.md`**: Benchmark description, operation complexity table, and usage instructions. diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..436a863 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,875 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "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 = "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 = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "bitflags", + "textwrap", + "unicode-width", +] + +[[package]] +name = "criterion" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +dependencies = [ + "atty", + "cast", + "clap", + "criterion-plot", + "csv", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[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 = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "delegate" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70a2d4995466955a415223acf3c9c934b9ff2339631cdf4ffc893da4bacd717" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doublets" +version = "0.1.0-pre+beta.15" +source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#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 = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "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.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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 = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + +[[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 = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "platform-data" +version = "0.1.0-beta.3" +source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#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?rev=5522d91c536654934b7181829f6efb570fb8bb44#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?rev=5522d91c536654934b7181829f6efb570fb8bb44#5522d91c536654934b7181829f6efb570fb8bb44" +dependencies = [ + "funty", + "platform-data", +] + +[[package]] +name = "plotters" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +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.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[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.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spacetimedb-vs-doublets" +version = "0.1.0" +dependencies = [ + "bumpalo", + "criterion", + "doublets", + "fastrand", + "getrandom", + "itoa", + "once_cell", + "proc-macro2", + "quote", + "rayon", + "rayon-core", + "regex", + "regex-automata", + "regex-syntax", + "rusqlite", + "ryu", + "same-file", + "serde_derive", + "syn", + "tempfile", + "unicode-width", + "walkdir", + "winapi-util", +] + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..dadead6 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "spacetimedb-vs-doublets" +version = "0.1.0" +edition = "2021" +license = "Unlicense" +description = "Benchmark comparing SpacetimeDB 2 vs Doublets performance for basic CRUD operations with links" + +[[bench]] +name = "bench" +harness = false + +[dependencies] +rusqlite = { version = "=0.27.0", features = ["bundled"] } +doublets = { git = "https://github.com/linksplatform/doublets-rs.git", rev = "5522d91c536654934b7181829f6efb570fb8bb44" } +once_cell = "1.14" + +[dev-dependencies] +criterion = { version = "=0.3.6", default-features = false } + +# Pin dependencies for compatibility with nightly-2022-08-22 (rustc 1.65) +serde_derive = "=1.0.136" +proc-macro2 = "=1.0.36" +quote = "=1.0.15" +syn = "=1.0.86" +bumpalo = "=3.11.1" +rayon = "=1.6.1" +rayon-core = "=1.10.2" +itoa = "=1.0.5" +ryu = "=1.0.12" +unicode-width = "=0.1.10" +tempfile = "=3.3.0" +fastrand = "=1.8.0" +getrandom = "=0.2.8" +winapi-util = "=0.1.5" +walkdir = "=2.3.2" +same-file = "=1.0.6" +regex = "=1.7.1" +regex-automata = "=0.1.10" +regex-syntax = "=0.6.28" + +[profile.release] +lto = true +codegen-units = 1 diff --git a/rust/benches/bench.rs b/rust/benches/bench.rs new file mode 100644 index 0000000..a30aed2 --- /dev/null +++ b/rust/benches/bench.rs @@ -0,0 +1,659 @@ +//! SpacetimeDB vs Doublets benchmark suite. +//! +//! Runs criterion benchmarks for basic CRUD operations with links, +//! comparing SpacetimeDB's SQLite backend against Doublets in-memory stores. +//! +//! Run benchmarks: +//! ```bash +//! cargo bench --bench bench -- --output-format bencher | tee out.txt +//! ``` +//! +//! Configure scale via environment variables: +//! - `BENCHMARK_LINK_COUNT` — links to create/update/delete per iteration (default: 1000) +//! - `BACKGROUND_LINK_COUNT` — pre-populated links for realistic DB state (default: 3000) + +#![feature(allocator_api)] + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use spacetimedb_vs_doublets::{ + benched::{ + Benched, DoubletsSplitVolatileBenched, DoubletsUnitedVolatileBenched, + SpacetimeDbMemoryBenched, + }, + Links, BACKGROUND_LINK_COUNT, BENCHMARK_LINK_COUNT, +}; +use std::time::{Duration, Instant}; + +// ===================== HELPERS ===================== + +/// Populate background links before each measured iteration. +/// Uses the `Links` trait via deref from the fork. +macro_rules! setup_background { + ($fork:expr) => { + for _ in 0..*BACKGROUND_LINK_COUNT { + $fork.create_point(); + } + }; +} + +// ===================== CREATE ===================== + +fn spacetimedb_create(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("create/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let start = Instant::now(); + for _ in 0..n { + fork.create_point(); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_create(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("create/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let start = Instant::now(); + for _ in 0..n { + fork.create_point(); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_create(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("create/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let start = Instant::now(); + for _ in 0..n { + fork.create_point(); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== DELETE ===================== + +fn spacetimedb_delete(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("delete/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + fork.delete(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_delete(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("delete/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + fork.delete(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_delete(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("delete/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + fork.delete(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== UPDATE ===================== + +fn spacetimedb_update(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("update/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for &id in &ids { + fork.update(id, 0, 0); + } + for &id in &ids { + fork.update(id, id, id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_update(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("update/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for &id in &ids { + fork.update(id, 0, 0); + } + for &id in &ids { + fork.update(id, id, id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_update(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("update/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for &id in &ids { + fork.update(id, 0, 0); + } + for &id in &ids { + fork.update(id, id, id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== QUERY ALL ===================== + +fn spacetimedb_query_all(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_all/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + for _ in 0..n { + fork.create_point(); + } + let start = Instant::now(); + let _ = fork.query_all(); + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_query_all(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_all/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + for _ in 0..n { + fork.create_point(); + } + let start = Instant::now(); + let _ = fork.query_all(); + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_query_all(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_all/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + for _ in 0..n { + fork.create_point(); + } + let start = Instant::now(); + let _ = fork.query_all(); + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== QUERY BY ID ===================== + +fn spacetimedb_query_by_id(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_id/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + let _ = fork.query_by_id(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_query_by_id(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_id/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + let _ = fork.query_by_id(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_query_by_id(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_id/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + let ids: Vec = (0..n).map(|_| fork.create_point()).collect(); + let start = Instant::now(); + for id in ids { + let _ = fork.query_by_id(id); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== QUERY BY SOURCE ===================== + +fn spacetimedb_query_by_source(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_source/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed sources + for i in 1..=n as u64 { + fork.create(i % 10 + 1, i % 7 + 1); + } + let start = Instant::now(); + for src in 1..=(n.min(10) as u64) { + let _ = fork.query_by_source(src); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_query_by_source(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_source/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed sources + for i in 1..=n as u64 { + fork.create(i % 10 + 1, i % 7 + 1); + } + let start = Instant::now(); + for src in 1..=(n.min(10) as u64) { + let _ = fork.query_by_source(src); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_query_by_source(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_source/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed sources + for i in 1..=n as u64 { + fork.create(i % 10 + 1, i % 7 + 1); + } + let start = Instant::now(); + for src in 1..=(n.min(10) as u64) { + let _ = fork.query_by_source(src); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +// ===================== QUERY BY TARGET ===================== + +fn spacetimedb_query_by_target(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = SpacetimeDbMemoryBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_target/SpacetimeDB_Memory", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed targets + for i in 1..=n as u64 { + fork.create(i % 7 + 1, i % 10 + 1); + } + let start = Instant::now(); + for tgt in 1..=(n.min(10) as u64) { + let _ = fork.query_by_target(tgt); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_united_query_by_target(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsUnitedVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_target/Doublets_United_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed targets + for i in 1..=n as u64 { + fork.create(i % 7 + 1, i % 10 + 1); + } + let start = Instant::now(); + for tgt in 1..=(n.min(10) as u64) { + let _ = fork.query_by_target(tgt); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +fn doublets_split_query_by_target(c: &mut Criterion) { + let count = *BENCHMARK_LINK_COUNT; + let mut benched = DoubletsSplitVolatileBenched::setup(()); + c.bench_with_input( + BenchmarkId::new("query_by_target/Doublets_Split_Volatile", count), + &count, + |b, &n| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let mut fork = Benched::fork(&mut benched); + setup_background!(fork); + // Create links with distributed targets + for i in 1..=n as u64 { + fork.create(i % 7 + 1, i % 10 + 1); + } + let start = Instant::now(); + for tgt in 1..=(n.min(10) as u64) { + let _ = fork.query_by_target(tgt); + } + total += start.elapsed(); + } + total + }); + }, + ); +} + +criterion_group!( + create_benches, + spacetimedb_create, + doublets_united_create, + doublets_split_create, +); + +criterion_group!( + delete_benches, + spacetimedb_delete, + doublets_united_delete, + doublets_split_delete, +); + +criterion_group!( + update_benches, + spacetimedb_update, + doublets_united_update, + doublets_split_update, +); + +criterion_group!( + query_all_benches, + spacetimedb_query_all, + doublets_united_query_all, + doublets_split_query_all, +); + +criterion_group!( + query_by_id_benches, + spacetimedb_query_by_id, + doublets_united_query_by_id, + doublets_split_query_by_id, +); + +criterion_group!( + query_by_source_benches, + spacetimedb_query_by_source, + doublets_united_query_by_source, + doublets_split_query_by_source, +); + +criterion_group!( + query_by_target_benches, + spacetimedb_query_by_target, + doublets_united_query_by_target, + doublets_split_query_by_target, +); + +criterion_main!( + create_benches, + delete_benches, + update_benches, + query_all_benches, + query_by_id_benches, + query_by_source_benches, + query_by_target_benches, +); diff --git a/rust/out.py b/rust/out.py new file mode 100644 index 0000000..bad9b66 --- /dev/null +++ b/rust/out.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Benchmark result visualization for SpacetimeDB vs Doublets. + +Reads Criterion bencher-format output from out.txt and generates: +- bench_rust.png: Linear scale comparison chart +- bench_rust_log_scale.png: Logarithmic scale comparison chart +- results.md: Markdown table with speedup ratios + +Usage: + python3 out.py [out.txt] +""" + +import re +import sys +import os + +try: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + import numpy as np + HAS_MATPLOTLIB = True +except ImportError: + print("Warning: matplotlib/numpy not installed, skipping chart generation") + HAS_MATPLOTLIB = False + +# Bencher output line format: +# test /// ... bench: ns/iter (+/- ) +BENCHER_PATTERN = re.compile( + r'test (\w+)/(\w+)/(\w+)/(\d+)\s+\.\.\.\s+bench:\s+([\d,]+)\s+ns/iter' +) + +OPERATIONS = [ + 'create', + 'delete', + 'update', + 'query_all', + 'query_by_id', + 'query_by_source', + 'query_by_target', +] + +OPERATION_LABELS = { + 'create': 'Create', + 'delete': 'Delete', + 'update': 'Update', + 'query_all': 'Query All', + 'query_by_id': 'Query by Id', + 'query_by_source': 'Query by Source', + 'query_by_target': 'Query by Target', +} + +VARIANTS = { + 'SpacetimeDB_Memory': 'SpacetimeDB (SQLite/Memory)', + 'Doublets_United_Volatile': 'Doublets (United/Volatile)', + 'Doublets_Split_Volatile': 'Doublets (Split/Volatile)', +} + +COLORS = { + 'SpacetimeDB_Memory': '#e74c3c', + 'Doublets_United_Volatile': '#2ecc71', + 'Doublets_Split_Volatile': '#3498db', +} + + +def parse_results(filename='out.txt'): + """Parse bencher-format output into a nested dict: operation -> variant -> ns_per_iter.""" + results = {op: {} for op in OPERATIONS} + + if not os.path.exists(filename): + print(f"Warning: {filename} not found") + return results + + with open(filename, 'r') as f: + content = f.read() + + for line in content.splitlines(): + m = BENCHER_PATTERN.search(line) + if m: + group, op, variant, size, ns_str = m.groups() + ns = int(ns_str.replace(',', '')) + if op in results: + results[op][variant] = ns + + # Also handle Criterion's default output format + # test group::benchmark/variant/size ... bench: X ns/iter (+/- Y) + CRITERION_PATTERN = re.compile( + r'test (\w+)::(\w+)/(\w+)/\d+\s+\.\.\.\s+bench:\s+([\d,]+)\s+ns/iter' + ) + for line in content.splitlines(): + m = CRITERION_PATTERN.search(line) + if m: + _group, op, variant, ns_str = m.groups() + ns = int(ns_str.replace(',', '')) + if op in results: + results[op][variant] = ns + + return results + + +def generate_charts(results): + """Generate PNG comparison charts.""" + if not HAS_MATPLOTLIB: + return + + variant_keys = list(VARIANTS.keys()) + op_labels = [OPERATION_LABELS.get(op, op) for op in OPERATIONS] + n_ops = len(OPERATIONS) + n_variants = len(variant_keys) + + # Collect data + data = {} + for variant in variant_keys: + data[variant] = [] + for op in OPERATIONS: + ns = results[op].get(variant, 0) + data[variant].append(ns) + + x = np.arange(n_ops) + width = 0.8 / n_variants + + def make_chart(ax, log_scale=False): + for i, variant in enumerate(variant_keys): + vals = [v if v > 0 else float('nan') for v in data[variant]] + offset = (i - n_variants / 2 + 0.5) * width + bars = ax.bar( + x + offset, vals, width, + label=VARIANTS[variant], + color=COLORS[variant], + alpha=0.85 + ) + + ax.set_xlabel('Operation') + ax.set_ylabel('Time (ns/iter)') + title = 'SpacetimeDB vs Doublets — Link CRUD Benchmark' + if log_scale: + title += ' (Log Scale)' + ax.set_yscale('log') + ax.set_title(title) + ax.set_xticks(x) + ax.set_xticklabels(op_labels, rotation=30, ha='right') + ax.legend() + ax.grid(axis='y', alpha=0.3) + plt.tight_layout() + + # Linear scale + fig, ax = plt.subplots(figsize=(12, 6)) + make_chart(ax, log_scale=False) + fig.savefig('bench_rust.png', dpi=150, bbox_inches='tight') + plt.close(fig) + print("Generated bench_rust.png") + + # Log scale + fig, ax = plt.subplots(figsize=(12, 6)) + make_chart(ax, log_scale=True) + fig.savefig('bench_rust_log_scale.png', dpi=150, bbox_inches='tight') + plt.close(fig) + print("Generated bench_rust_log_scale.png") + + +def generate_markdown_table(results): + """Generate a Markdown results table with speedup ratios.""" + baseline = 'SpacetimeDB_Memory' + doublets_variants = ['Doublets_United_Volatile', 'Doublets_Split_Volatile'] + + header = ( + '| Operation | SpacetimeDB (ns/iter) ' + '| Doublets United (ns/iter) | Doublets United Speedup ' + '| Doublets Split (ns/iter) | Doublets Split Speedup |' + ) + sep = '|---|---|---|---|---|---|' + + rows = [header, sep] + for op in OPERATIONS: + label = OPERATION_LABELS.get(op, op) + baseline_ns = results[op].get(baseline, 0) + baseline_str = f'{baseline_ns:,}' if baseline_ns else 'N/A' + + row = f'| {label} | {baseline_str} ' + for variant in doublets_variants: + ns = results[op].get(variant, 0) + ns_str = f'{ns:,}' if ns else 'N/A' + if baseline_ns > 0 and ns > 0: + speedup = baseline_ns / ns + speedup_str = f'{speedup:.0f}x' + else: + speedup_str = 'N/A' + row += f'| {ns_str} | {speedup_str} ' + row += '|' + rows.append(row) + + table = '\n'.join(rows) + with open('results.md', 'w') as f: + f.write('# Benchmark Results\n\n') + f.write(f'> Background: {os.environ.get("BACKGROUND_LINK_COUNT", "3000")} links\n') + f.write(f'> Operations: {os.environ.get("BENCHMARK_LINK_COUNT", "1000")} links\n\n') + f.write(table) + f.write('\n') + + print("Generated results.md") + print('\n' + table) + + +def main(): + filename = sys.argv[1] if len(sys.argv) > 1 else 'out.txt' + results = parse_results(filename) + + if all(not v for v in results.values()): + print(f"No benchmark data found in {filename}") + return + + generate_charts(results) + generate_markdown_table(results) + + +if __name__ == '__main__': + main() diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml new file mode 100644 index 0000000..33c06c9 --- /dev/null +++ b/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2022-08-22" +components = ["rustfmt", "clippy"] diff --git a/rust/rustfmt.toml b/rust/rustfmt.toml new file mode 100644 index 0000000..3a26366 --- /dev/null +++ b/rust/rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/rust/src/benched/doublets_benched.rs b/rust/src/benched/doublets_benched.rs new file mode 100644 index 0000000..f4c3e10 --- /dev/null +++ b/rust/src/benched/doublets_benched.rs @@ -0,0 +1,85 @@ +//! `Benched` implementations for Doublets stores. + +use crate::{ + benched::Benched, + doublets_impl::{ + create_split_volatile, create_united_volatile, DoubletsLinks, DoubletsSplitVolatile, + DoubletsUnitedVolatile, + }, + Fork, Links, +}; +use std::ops::{Deref, DerefMut}; + +/// Benchmark subject for Doublets united volatile (in-memory, contiguous) store. +pub struct DoubletsUnitedVolatileBenched { + links: DoubletsLinks, +} + +impl Benched for DoubletsUnitedVolatileBenched { + type Builder = (); + + fn setup(_builder: Self::Builder) -> Self { + Self { + links: create_united_volatile(), + } + } + + fn fork(&mut self) -> Fork { + Fork::new(self) + } + + unsafe fn unfork(&mut self) { + self.links.delete_all(); + } +} + +impl Deref for DoubletsUnitedVolatileBenched { + type Target = DoubletsLinks; + + fn deref(&self) -> &Self::Target { + &self.links + } +} + +impl DerefMut for DoubletsUnitedVolatileBenched { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.links + } +} + +/// Benchmark subject for Doublets split volatile (in-memory, separate data/index) store. +pub struct DoubletsSplitVolatileBenched { + links: DoubletsLinks, +} + +impl Benched for DoubletsSplitVolatileBenched { + type Builder = (); + + fn setup(_builder: Self::Builder) -> Self { + Self { + links: create_split_volatile(), + } + } + + fn fork(&mut self) -> Fork { + Fork::new(self) + } + + unsafe fn unfork(&mut self) { + self.links.delete_all(); + } +} + +impl Deref for DoubletsSplitVolatileBenched { + type Target = DoubletsLinks; + + fn deref(&self) -> &Self::Target { + &self.links + } +} + +impl DerefMut for DoubletsSplitVolatileBenched { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.links + } +} diff --git a/rust/src/benched/mod.rs b/rust/src/benched/mod.rs new file mode 100644 index 0000000..449a1a3 --- /dev/null +++ b/rust/src/benched/mod.rs @@ -0,0 +1,36 @@ +//! Benched trait and implementations for SpacetimeDB and Doublets. +//! +//! The `Benched` trait defines the lifecycle for benchmark subjects: +//! 1. `setup()` — construct the database and prepare it for benchmarking +//! 2. `fork()` — create a `Fork` wrapper for a single iteration +//! 3. `unfork()` — reset state after each iteration (called by `Fork::drop`) + +mod doublets_benched; +mod spacetimedb_benched; + +pub use doublets_benched::*; +pub use spacetimedb_benched::*; + +use crate::Fork; + +/// Lifecycle trait for benchmark subjects. +/// +/// Implementors wrap a database and manage its state across iterations. +/// The fork/unfork pattern ensures each benchmark iteration starts from +/// a consistent baseline state (background links pre-populated). +pub trait Benched: Sized { + /// The builder type used to construct this benched subject. + type Builder; + + /// Set up the database and return a ready-to-benchmark instance. + fn setup(builder: Self::Builder) -> Self; + + /// Create a fork for a single isolated benchmark iteration. + fn fork(&mut self) -> Fork; + + /// Reset database state after an iteration (called by `Fork::drop`). + /// + /// # Safety + /// Must only be called from `Fork::drop`. + unsafe fn unfork(&mut self); +} diff --git a/rust/src/benched/spacetimedb_benched.rs b/rust/src/benched/spacetimedb_benched.rs new file mode 100644 index 0000000..65ad283 --- /dev/null +++ b/rust/src/benched/spacetimedb_benched.rs @@ -0,0 +1,44 @@ +//! `Benched` implementation for SpacetimeDB (SQLite backend). + +use crate::{benched::Benched, spacetimedb_impl::SpacetimeDbLinks, Fork, Links}; +use std::ops::{Deref, DerefMut}; + +/// Benchmark subject for SpacetimeDB with in-memory SQLite backend. +/// +/// Uses SpacetimeDB's SQLite storage layer directly, which is the same +/// storage engine SpacetimeDB 2 uses internally for its tables. +pub struct SpacetimeDbMemoryBenched { + links: SpacetimeDbLinks, +} + +impl Benched for SpacetimeDbMemoryBenched { + type Builder = (); + + fn setup(_builder: Self::Builder) -> Self { + Self { + links: SpacetimeDbLinks::new_memory(), + } + } + + fn fork(&mut self) -> Fork { + Fork::new(self) + } + + unsafe fn unfork(&mut self) { + self.links.delete_all(); + } +} + +impl Deref for SpacetimeDbMemoryBenched { + type Target = SpacetimeDbLinks; + + fn deref(&self) -> &Self::Target { + &self.links + } +} + +impl DerefMut for SpacetimeDbMemoryBenched { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.links + } +} diff --git a/rust/src/doublets_impl.rs b/rust/src/doublets_impl.rs new file mode 100644 index 0000000..c0b4823 --- /dev/null +++ b/rust/src/doublets_impl.rs @@ -0,0 +1,233 @@ +//! Doublets storage implementation for links. +//! +//! Adapts the Doublets in-memory link store to the shared `Links` trait +//! used in benchmarks. +//! +//! # Doublets Storage Types +//! +//! Two in-memory storage layouts are benchmarked: +//! +//! - **United Volatile**: Each link stored as a contiguous unit `(index, source, target)`. +//! Single allocation; best cache locality for small stores. +//! - **Split Volatile**: Data part and index part in separate memory regions. +//! Better cache efficiency for index-heavy workloads. +//! +//! # Operation Complexity +//! +//! | Operation | United | Split | +//! |-----------------------|--------|-------| +//! | Create | O(log n) | O(log n) | +//! | Update | O(log n) | O(log n) | +//! | Delete | O(log n) | O(log n) | +//! | Query All | O(n) | O(n) | +//! | Query by Id | O(1) | O(1) | +//! | Query by Source | O(log n + k) | O(log n + k) | +//! | Query by Target | O(log n + k) | O(log n + k) | +//! | Query by Source+Target| O(log n + k) | O(log n + k) | + +use crate::{Link, Links}; +use doublets::{ + mem::Alloc, + split::{self, DataPart, IndexPart}, + unit::{self, LinkPart}, + Doublets, DoubletsExt, +}; +use std::alloc::Global; + +/// In-memory united (single contiguous region) doublets store. +pub type DoubletsUnitedVolatile = unit::Store, Global>>; + +/// In-memory split (separate data and index regions) doublets store. +pub type DoubletsSplitVolatile = + split::Store, Global>, Alloc, Global>>; + +/// Wrapper adapting a `doublets::Doublets` store to the shared `Links` trait. +pub struct DoubletsLinks { + store: S, +} + +impl DoubletsLinks { + pub fn new(store: S) -> Self { + Self { store } + } +} + +impl + DoubletsExt> Links for DoubletsLinks { + fn create(&mut self, source: u64, target: u64) -> u64 { + self.store + .create_by([source as usize, target as usize]) + .expect("Failed to create link") as u64 + } + + fn create_point(&mut self) -> u64 { + self.store.create_point().expect("Failed to create point") as u64 + } + + fn update(&mut self, id: u64, source: u64, target: u64) { + self.store + .update(id as usize, source as usize, target as usize) + .expect("Failed to update link"); + } + + fn delete(&mut self, id: u64) { + self.store + .delete(id as usize) + .expect("Failed to delete link"); + } + + fn delete_all(&mut self) { + let any = self.store.constants().any; + let ids: Vec = self + .store + .each_iter([any, any, any]) + .map(|link| link.index) + .collect(); + for id in ids { + let _ = self.store.delete(id); + } + } + + fn query_all(&self) -> Vec { + let any = self.store.constants().any; + self.store + .each_iter([any, any, any]) + .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) + .collect() + } + + fn query_by_id(&self, id: u64) -> Option { + self.store + .get_link(id as usize) + .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) + } + + fn query_by_source(&self, source: u64) -> Vec { + let any = self.store.constants().any; + self.store + .each_iter([any, source as usize, any]) + .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) + .collect() + } + + fn query_by_target(&self, target: u64) -> Vec { + let any = self.store.constants().any; + self.store + .each_iter([any, any, target as usize]) + .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) + .collect() + } + + fn query_by_source_target(&self, source: u64, target: u64) -> Vec { + let any = self.store.constants().any; + self.store + .each_iter([any, source as usize, target as usize]) + .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) + .collect() + } + + fn count(&self) -> usize { + self.store.count() + } +} + +/// Create a new in-memory doublets united store. +pub fn create_united_volatile() -> DoubletsLinks { + let mem = Alloc::new(Global); + let store = DoubletsUnitedVolatile::new(mem).expect("Failed to create doublets united store"); + DoubletsLinks::new(store) +} + +/// Create a new in-memory doublets split store. +pub fn create_split_volatile() -> DoubletsLinks { + let data_mem = Alloc::new(Global); + let index_mem = Alloc::new(Global); + let store = DoubletsSplitVolatile::new(data_mem, index_mem) + .expect("Failed to create doublets split store"); + DoubletsLinks::new(store) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_and_query_united() { + let mut db = create_united_volatile(); + let id = db.create_point(); + assert_eq!(id, 1); + + let link = db.query_by_id(id).unwrap(); + assert_eq!(link.source, id); + assert_eq!(link.target, id); + } + + #[test] + fn test_create_and_query_split() { + let mut db = create_split_volatile(); + let id = db.create_point(); + assert_eq!(id, 1); + + let link = db.query_by_id(id).unwrap(); + assert_eq!(link.source, id); + assert_eq!(link.target, id); + } + + #[test] + fn test_update_united() { + let mut db = create_united_volatile(); + let id = db.create(1, 2); + db.update(id, 3, 4); + + let link = db.query_by_id(id).unwrap(); + assert_eq!(link.source, 3); + assert_eq!(link.target, 4); + } + + #[test] + fn test_delete_united() { + let mut db = create_united_volatile(); + let id = db.create_point(); + db.delete(id); + assert!(db.query_by_id(id).is_none()); + } + + #[test] + fn test_query_by_source_united() { + let mut db = create_united_volatile(); + let id1 = db.create_point(); + let id2 = db.create_point(); + db.update(id1, id1, id2); + db.update(id2, id1, id1); + + let links = db.query_by_source(id1); + assert_eq!(links.len(), 2); + } + + #[test] + fn test_query_by_target_split() { + let mut db = create_split_volatile(); + // Create independent links: (1,1,1), (2,2,2), (3,3,3) + let id1 = db.create_point(); + let id2 = db.create_point(); + let id3 = db.create_point(); + // Update link3 to point at id1 as target; use id3 as source (independent) + db.update(id3, id3, id1); + // Update link2 to also point at id1 as target; use id2 as source (still points to itself but as source) + db.update(id2, id2, id1); + + let links = db.query_by_target(id1); + // id1 itself is a point (source=1, target=1), id2 now has target=id1, id3 now has target=id1 + assert!(links.len() >= 2); + } + + #[test] + fn test_delete_all_united() { + let mut db = create_united_volatile(); + for _ in 0..10 { + db.create_point(); + } + assert_eq!(db.count(), 10); + db.delete_all(); + assert_eq!(db.count(), 0); + } +} diff --git a/rust/src/exclusive.rs b/rust/src/exclusive.rs new file mode 100644 index 0000000..031e014 --- /dev/null +++ b/rust/src/exclusive.rs @@ -0,0 +1,52 @@ +//! Thread-safe exclusive access wrapper. +//! +//! Provides `Exclusive` which allows shared access to an `UnsafeCell` +//! across thread boundaries, bypassing Rust's borrow checker. +//! Used to allow sharing mutable state between benchmark iterations. + +use std::cell::UnsafeCell; +use std::ops::{Deref, DerefMut}; + +/// A wrapper that provides exclusive mutable access to an inner value +/// via `UnsafeCell`. Implements `Sync` to allow sharing across threads. +/// +/// # Safety +/// The caller must ensure that only one thread accesses the inner value at a time. +pub struct Exclusive { + inner: UnsafeCell, +} + +impl Exclusive { + pub fn new(value: T) -> Self { + Self { + inner: UnsafeCell::new(value), + } + } + + /// Get a mutable reference to the inner value. + /// + /// # Safety + /// Caller must ensure exclusive access. + #[allow(clippy::mut_from_ref)] + pub unsafe fn get_mut(&self) -> &mut T { + &mut *self.inner.get() + } +} + +impl Deref for Exclusive { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.inner.get() } + } +} + +impl DerefMut for Exclusive { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.get_mut() + } +} + +// SAFETY: Benchmarks run single-threaded; Exclusive access is manually enforced. +unsafe impl Send for Exclusive {} +unsafe impl Sync for Exclusive {} diff --git a/rust/src/fork.rs b/rust/src/fork.rs new file mode 100644 index 0000000..01976e8 --- /dev/null +++ b/rust/src/fork.rs @@ -0,0 +1,44 @@ +//! Fork mechanism for isolated benchmark iterations. +//! +//! `Fork` wraps a `Benched` implementation and automatically calls +//! `unfork()` when dropped, resetting the database state between +//! benchmark iterations. + +use crate::Benched; +use std::ops::{Deref, DerefMut}; + +/// A fork of a `Benched` instance that resets database state on drop. +/// +/// Created by `Benched::fork()`. Provides direct access to the underlying +/// `Benched` type via `Deref`/`DerefMut`. On drop, calls `Benched::unfork()` +/// to clean up state for the next iteration. +pub struct Fork<'a, B: Benched> { + benched: &'a mut B, +} + +impl<'a, B: Benched> Fork<'a, B> { + pub fn new(benched: &'a mut B) -> Self { + Self { benched } + } +} + +impl<'a, B: Benched> Deref for Fork<'a, B> { + type Target = B; + + fn deref(&self) -> &Self::Target { + self.benched + } +} + +impl<'a, B: Benched> DerefMut for Fork<'a, B> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.benched + } +} + +impl<'a, B: Benched> Drop for Fork<'a, B> { + fn drop(&mut self) { + // SAFETY: Called only from Drop, ensuring no other references exist. + unsafe { self.benched.unfork() }; + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..2e303e2 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,96 @@ +//! SpacetimeDB vs Doublets benchmark library. +//! +//! Provides implementations for benchmarking SpacetimeDB and Doublets +//! storage systems on basic CRUD operations with links. + +#![feature(allocator_api)] + +pub mod benched; +pub mod doublets_impl; +pub mod exclusive; +pub mod fork; +pub mod spacetimedb_impl; + +pub use benched::Benched; +pub use exclusive::Exclusive; +pub use fork::Fork; + +use once_cell::sync::Lazy; +use std::env; + +/// Number of links to use for benchmarking. +/// Configurable via `BENCHMARK_LINK_COUNT` environment variable. +/// Defaults to 1000 for main branch, 10 for PRs (controlled by CI). +pub static BENCHMARK_LINK_COUNT: Lazy = Lazy::new(|| { + env::var("BENCHMARK_LINK_COUNT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(1000) +}); + +/// Number of background links to create before benchmarking to simulate a realistic database state. +/// Configurable via `BACKGROUND_LINK_COUNT` environment variable. +/// Defaults to 3000 for main branch, 100 for PRs (controlled by CI). +pub static BACKGROUND_LINK_COUNT: Lazy = Lazy::new(|| { + env::var("BACKGROUND_LINK_COUNT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(3000) +}); + +/// A link structure representing a doublet (source -> target relationship). +/// Each link has a unique id, a source, and a target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Link { + pub id: u64, + pub source: u64, + pub target: u64, +} + +impl Link { + #[must_use] + pub fn new(id: u64, source: u64, target: u64) -> Self { + Self { id, source, target } + } +} + +/// Shared trait for database operations on links. +/// Both SpacetimeDB and Doublets implement this interface. +pub trait Links { + /// Create a link with given source and target. Returns the new link's id. + fn create(&mut self, source: u64, target: u64) -> u64; + + /// Create a self-referential point link (id == source == target). Returns the new link's id. + fn create_point(&mut self) -> u64 { + let id = self.create(0, 0); + self.update(id, id, id); + id + } + + /// Update an existing link's source and target. + fn update(&mut self, id: u64, source: u64, target: u64); + + /// Delete a link by id. + fn delete(&mut self, id: u64); + + /// Delete all links (used to reset state between benchmark iterations). + fn delete_all(&mut self); + + /// Retrieve all links. + fn query_all(&self) -> Vec; + + /// Retrieve a link by its id. + fn query_by_id(&self, id: u64) -> Option; + + /// Retrieve all links with the given source. + fn query_by_source(&self, source: u64) -> Vec; + + /// Retrieve all links with the given target. + fn query_by_target(&self, target: u64) -> Vec; + + /// Retrieve all links matching both source and target. + fn query_by_source_target(&self, source: u64, target: u64) -> Vec; + + /// Count all links in the database. + fn count(&self) -> usize; +} diff --git a/rust/src/spacetimedb_impl.rs b/rust/src/spacetimedb_impl.rs new file mode 100644 index 0000000..740af12 --- /dev/null +++ b/rust/src/spacetimedb_impl.rs @@ -0,0 +1,311 @@ +//! SpacetimeDB storage implementation for links (SQLite backend). +//! +//! This implementation uses the same SQLite storage that SpacetimeDB 2 uses +//! internally. SpacetimeDB stores data in SQLite tables with column-oriented +//! layout; this implementation uses the equivalent schema. +//! +//! SpacetimeDB is benchmarked via its SQLite backend to establish a fair +//! baseline comparison with Doublets' in-memory data structures. +//! +//! # Schema +//! +//! ```sql +//! CREATE TABLE links ( +//! id INTEGER PRIMARY KEY, +//! source INTEGER NOT NULL, +//! target INTEGER NOT NULL +//! ); +//! CREATE INDEX idx_source ON links(source); +//! CREATE INDEX idx_target ON links(target); +//! CREATE INDEX idx_source_target ON links(source, target); +//! ``` +//! +//! # Operation Complexity +//! +//! | Operation | Complexity | +//! |------------------------|-----------------------| +//! | Create | O(log n) + disk I/O | +//! | Update | O(log n) + disk I/O | +//! | Delete | O(log n) + disk I/O | +//! | Query All | O(n) + disk I/O | +//! | Query by Id | O(log n) | +//! | Query by Source | O(log n + k) | +//! | Query by Target | O(log n + k) | +//! | Query by Source+Target | O(log n + k) | + +use crate::{Link, Links}; +use rusqlite::{params, Connection}; + +/// SQLite-based links storage using SpacetimeDB's internal schema. +/// +/// SpacetimeDB 2 stores table data in SQLite with auto-incrementing +/// primary keys and B-tree indexes on all searchable columns. +pub struct SpacetimeDbLinks { + conn: Connection, + next_id: u64, +} + +impl SpacetimeDbLinks { + /// Create a new in-memory SpacetimeDB links storage (no persistence). + /// + /// This matches SpacetimeDB's behavior when running without a persistent + /// data directory (e.g., for testing/benchmarking). + pub fn new_memory() -> Self { + let conn = Connection::open_in_memory().expect("Failed to open in-memory SQLite database"); + Self::init(conn) + } + + fn init(conn: Connection) -> Self { + // Enable WAL mode for better write performance (SpacetimeDB default) + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;") + .expect("Failed to set SQLite pragmas"); + + conn.execute( + "CREATE TABLE IF NOT EXISTS links ( + id INTEGER PRIMARY KEY, + source INTEGER NOT NULL, + target INTEGER NOT NULL + )", + [], + ) + .expect("Failed to create links table"); + + // B-tree indexes on source, target, and composite (source, target) + // matching SpacetimeDB's automatic index generation for filtered columns + conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON links(source)", []) + .expect("Failed to create source index"); + + conn.execute("CREATE INDEX IF NOT EXISTS idx_target ON links(target)", []) + .expect("Failed to create target index"); + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_source_target ON links(source, target)", + [], + ) + .expect("Failed to create source_target index"); + + let next_id: u64 = conn + .query_row("SELECT COALESCE(MAX(id), 0) + 1 FROM links", [], |row| { + row.get(0) + }) + .unwrap_or(1); + + Self { conn, next_id } + } + + /// Drop and recreate all tables and indexes (used by `delete_all`). + fn reset_schema(&mut self) { + self.conn + .execute("DELETE FROM links", []) + .expect("Failed to delete all links"); + self.next_id = 1; + } +} + +impl Links for SpacetimeDbLinks { + fn create(&mut self, source: u64, target: u64) -> u64 { + let id = self.next_id; + self.conn + .execute( + "INSERT INTO links (id, source, target) VALUES (?1, ?2, ?3)", + params![id as i64, source as i64, target as i64], + ) + .expect("Failed to insert link"); + self.next_id += 1; + id + } + + fn update(&mut self, id: u64, source: u64, target: u64) { + self.conn + .execute( + "UPDATE links SET source = ?1, target = ?2 WHERE id = ?3", + params![source as i64, target as i64, id as i64], + ) + .expect("Failed to update link"); + } + + fn delete(&mut self, id: u64) { + self.conn + .execute("DELETE FROM links WHERE id = ?1", params![id as i64]) + .expect("Failed to delete link"); + } + + fn delete_all(&mut self) { + self.reset_schema(); + } + + fn query_all(&self) -> Vec { + let mut stmt = self + .conn + .prepare("SELECT id, source, target FROM links") + .expect("Failed to prepare query_all"); + + stmt.query_map([], |row| { + Ok(Link::new( + row.get::<_, i64>(0)? as u64, + row.get::<_, i64>(1)? as u64, + row.get::<_, i64>(2)? as u64, + )) + }) + .expect("Failed to execute query_all") + .filter_map(|r| r.ok()) + .collect() + } + + fn query_by_id(&self, id: u64) -> Option { + self.conn + .query_row( + "SELECT id, source, target FROM links WHERE id = ?1", + params![id as i64], + |row| { + Ok(Link::new( + row.get::<_, i64>(0)? as u64, + row.get::<_, i64>(1)? as u64, + row.get::<_, i64>(2)? as u64, + )) + }, + ) + .ok() + } + + fn query_by_source(&self, source: u64) -> Vec { + let mut stmt = self + .conn + .prepare("SELECT id, source, target FROM links WHERE source = ?1") + .expect("Failed to prepare query_by_source"); + + stmt.query_map(params![source as i64], |row| { + Ok(Link::new( + row.get::<_, i64>(0)? as u64, + row.get::<_, i64>(1)? as u64, + row.get::<_, i64>(2)? as u64, + )) + }) + .expect("Failed to execute query_by_source") + .filter_map(|r| r.ok()) + .collect() + } + + fn query_by_target(&self, target: u64) -> Vec { + let mut stmt = self + .conn + .prepare("SELECT id, source, target FROM links WHERE target = ?1") + .expect("Failed to prepare query_by_target"); + + stmt.query_map(params![target as i64], |row| { + Ok(Link::new( + row.get::<_, i64>(0)? as u64, + row.get::<_, i64>(1)? as u64, + row.get::<_, i64>(2)? as u64, + )) + }) + .expect("Failed to execute query_by_target") + .filter_map(|r| r.ok()) + .collect() + } + + fn query_by_source_target(&self, source: u64, target: u64) -> Vec { + let mut stmt = self + .conn + .prepare("SELECT id, source, target FROM links WHERE source = ?1 AND target = ?2") + .expect("Failed to prepare query_by_source_target"); + + stmt.query_map(params![source as i64, target as i64], |row| { + Ok(Link::new( + row.get::<_, i64>(0)? as u64, + row.get::<_, i64>(1)? as u64, + row.get::<_, i64>(2)? as u64, + )) + }) + .expect("Failed to execute query_by_source_target") + .filter_map(|r| r.ok()) + .collect() + } + + fn count(&self) -> usize { + self.conn + .query_row("SELECT COUNT(*) FROM links", [], |row| row.get::<_, i64>(0)) + .unwrap_or(0) as usize + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_and_query_memory() { + let mut db = SpacetimeDbLinks::new_memory(); + let id = db.create_point(); + assert_eq!(id, 1); + + let link = db.query_by_id(id).unwrap(); + assert_eq!(link.source, id); + assert_eq!(link.target, id); + } + + #[test] + fn test_update_memory() { + let mut db = SpacetimeDbLinks::new_memory(); + let id = db.create(1, 2); + db.update(id, 3, 4); + + let link = db.query_by_id(id).unwrap(); + assert_eq!(link.source, 3); + assert_eq!(link.target, 4); + } + + #[test] + fn test_delete_memory() { + let mut db = SpacetimeDbLinks::new_memory(); + let id = db.create_point(); + db.delete(id); + assert!(db.query_by_id(id).is_none()); + } + + #[test] + fn test_query_by_source() { + let mut db = SpacetimeDbLinks::new_memory(); + let id1 = db.create_point(); + let id2 = db.create_point(); + db.update(id1, id1, id2); + db.update(id2, id1, id1); + + let links = db.query_by_source(id1); + assert_eq!(links.len(), 2); + } + + #[test] + fn test_query_by_target() { + let mut db = SpacetimeDbLinks::new_memory(); + let id1 = db.create_point(); + let id2 = db.create_point(); + db.update(id1, id2, id1); + db.update(id2, id2, id1); + + let links = db.query_by_target(id1); + assert_eq!(links.len(), 2); + } + + #[test] + fn test_delete_all() { + let mut db = SpacetimeDbLinks::new_memory(); + for _ in 0..10 { + db.create_point(); + } + assert_eq!(db.count(), 10); + db.delete_all(); + assert_eq!(db.count(), 0); + } + + #[test] + fn test_query_by_source_target() { + let mut db = SpacetimeDbLinks::new_memory(); + let id1 = db.create(10, 20); + let _id2 = db.create(10, 30); + + let links = db.query_by_source_target(10, 20); + assert_eq!(links.len(), 1); + assert_eq!(links[0].id, id1); + } +}