diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index fafd81908..76fabbabd 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -34,8 +34,9 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} # With the Cassandra and MongoDB crates' `test-hooks` features, so the # hook modules (`src/test_hooks.rs`) and their call sites, which only - # the integration test binaries build otherwise, are linted too. - - run: cargo clippy --all-targets --features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks -- -D warnings + # the integration test binaries build otherwise, are linted too. With the + # bin crate's `mimalloc` feature as well, which no other job builds. + - run: cargo clippy --all-targets --features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks,extenddb/mimalloc -- -D warnings # The backup job's kill-point pauses (`EXTENDDB_BACKUP_PAUSE`), which # `devtools/backup-kill-points.sh` runs by hand on a build with the # feature; no other job builds it. diff --git a/Cargo.lock b/Cargo.lock index 0209610a9..741252de9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1645,6 +1645,7 @@ dependencies = [ "extenddb-storage-mongodb", "extenddb-storage-postgres", "extenddb-storage-sqlite", + "mimalloc", "sqlx", "tokio", "uuid", @@ -2892,6 +2893,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.16" @@ -3059,6 +3069,15 @@ dependencies = [ "rapidhash", ] +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -5252,7 +5271,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c22b5a57b..c1bba77b6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,9 @@ extenddb-auth = { path = "crates/auth", version = "=0.1.14" } extenddb-server = { path = "crates/server", version = "=0.1.14" } extenddb-app = { path = "crates/app", version = "=0.1.14" } +# Memory allocator, opt-in through the `mimalloc` feature of the bin crate +mimalloc = { version = "0.1", default-features = false } + # Serialization serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 7a2705c20..31536922b 100755 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -38,6 +38,9 @@ mongodb-test-hooks = [ # fails (`main.rs` checks this at compile time). A production `sqlite` build # leaves it off. dev-mode = ["extenddb-app/dev-mode"] +# Use mimalloc as the global allocator in place of the system allocator. Off by +# default. See ADR-0030 for the measurements. +mimalloc = ["dep:mimalloc"] [dependencies] extenddb-app = { workspace = true } @@ -48,6 +51,7 @@ extenddb-storage-postgres = { workspace = true, optional = true } extenddb-storage-sqlite = { workspace = true, optional = true } anyhow = { workspace = true } extenddb-config = { workspace = true } +mimalloc = { workspace = true, optional = true } [dev-dependencies] sqlx = { workspace = true } diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs index 0f8e81bdd..bfd5a8cc3 100755 --- a/crates/bin/src/main.rs +++ b/crates/bin/src/main.rs @@ -17,6 +17,13 @@ //! installs one backend per process, so a build with more than one would be ambiguous //! and is rejected at compile time. +// The `mimalloc` feature replaces the system allocator with mimalloc. The +// request path allocates for each JSON value, attribute and string that it +// builds, on many tokio worker threads. See ADR-0030 for the measurements. +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + // Exactly one backend feature must be enabled. #[cfg(any( all(feature = "postgres", feature = "cassandra"), diff --git a/docs/adr/0030-optional-mimalloc-allocator.md b/docs/adr/0030-optional-mimalloc-allocator.md new file mode 100644 index 000000000..7f3912acf --- /dev/null +++ b/docs/adr/0030-optional-mimalloc-allocator.md @@ -0,0 +1,95 @@ +# ADR-0030: mimalloc is an opt-in global allocator for the server binary + +- Status: Proposed +- Date: 2026-11-10 +- Deciders: @parsnips + +## Context + +The `extenddb` binary uses the system allocator. On Linux that is glibc +malloc. + +The request path allocates often. A request body becomes a `serde_json::Value`, +then `AttributeValue`s. A PartiQL statement becomes a `PutItem` body, which is +parsed again. The item becomes JSONB for the storage backend. Each step +allocates strings, maps and vectors, and frees them on one of many tokio worker +threads. + +We measured the PostgreSQL build of `main` at 9c3fbfb under a load generator +that sends conditional inserts with unique keys to a table with a hash and a +range key and no indexes. It sends them as `PutItem` with +`attribute_not_exists` on the partition key, and as `ExecuteStatement` with an +`INSERT` statement. Each item has a 256-byte value. The host was a 24-core +x86_64 Linux machine. The server ran in a Debian 12 container (glibc 2.36), +with PostgreSQL 16 in a second container and connection pools of 20. + +We profiled the server with `perf` (frame pointers on) under the `PutItem` +load with 32 requests in flight. Code in `libc.so.6` took 16% of the server's +samples, which is 22% of the samples in user space. + +## Options Considered + +1. **Keep the system allocator.** No change. +2. **mimalloc as an opt-in feature.** A `mimalloc` feature on the bin crate + sets `mimalloc::MiMalloc` as the `#[global_allocator]`. Default builds do + not change. +3. **mimalloc by default.** The same change with the feature on in the default + feature set, so every release binary uses it. +4. **jemalloc (`tikv-jemallocator`).** Not measured here. Its build needs + `make` and runs a configure script, and it does not build for MSVC + targets. + +## Decision + +The bin crate has an opt-in `mimalloc` feature that makes mimalloc the global +allocator (Option 2). + +## Rationale + +- Measured on the workload above, with 32 requests in flight for 10 seconds + for each operation. Each measurement used a fresh database, and the two + builds were the same apart from the `mimalloc` feature. The runs alternated + between the builds. The host also ran other work, which disturbed one + window (marked \*). + + | Operation | Allocator | Throughput (ops/s), per measurement | Server CPU per request (µs) | + |---|---|---|---| + | `PutItem` | glibc malloc | 15,092, 15,099, 14,992 | 257, 257, 251 | + | `PutItem` | mimalloc | 15,679, 15,065, 15,415 | 226, 222, 227 | + | `INSERT` | glibc malloc | 14,613, 6,616\*, 14,058 | 297, 319\*, 287 | + | `INSERT` | mimalloc | 14,909, 14,595, 15,134 | 259, 263, 253 | + + Server CPU for each request fell by about 12% for both operations. + Throughput rose by about 2% to 4%, which is close to the variation between + measurements. With mimalloc, `libc.so.6` took 5% of the server's samples. +- Peak resident memory of the server after each measurement (about 600,000 + inserts) was 309 to 359 MB with glibc malloc and 398 to 428 MB with + mimalloc. +- The `mimalloc` crate (0.1.52, which builds mimalloc v3) compiles its C + sources with the `cc` crate. The workspace already builds C through `cc` for + `aws-lc-sys`. The crate's `secure` feature (guard pages, encoded free lists) + stays off. +- An opt-in feature does not change memory behaviour for existing + deployments. mimalloc holds and returns memory differently from glibc, and + resident memory was higher in the measurement above. Operators can measure + it on their own workload before the default changes. +- A `#[global_allocator]` in the bin crate applies to the whole process and + does not touch any library crate. A third-party thin bin can choose its own + allocator. + +## Consequences + +- A build that spends less CPU in the allocator is one flag away: + `cargo build --release --features mimalloc`. +- Release images and packages keep the system allocator until a later decision + turns the feature on by default. That decision needs measurements from more + workloads (read-heavy, Scan, the other backends) and of resident memory. +- The feature adds a C dependency to builds that turn it on. +- Reverting is a one-line change to the feature set. + +--- + +## License + +Copyright 2026 ExtendDB contributors. Licensed under the Apache License, Version 2.0. +See [LICENSE](../../LICENSE) for the full text. diff --git a/docs/adr/README.md b/docs/adr/README.md index 372bf38f3..43a68be68 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -55,3 +55,4 @@ decision, write a new ADR. | [0026](0026-credential-crypto-on-aws-lc.md) | Credential cryptography uses AWS-LC through aws-lc-rs | Accepted | | [0027](0027-cassandra-index-row-version-stamps.md) | Cassandra: index rows carry the base-row version as their write timestamp | Accepted | | [0029](0029-crates-io-packaging.md) | The workspace crates go to crates.io together, at one version with exact pins | Accepted | +| [0030](0030-optional-mimalloc-allocator.md) | mimalloc is an opt-in global allocator for the server binary | Proposed | diff --git a/docs/manuals/04-quickstart-setup-guide.md b/docs/manuals/04-quickstart-setup-guide.md index d3d197414..7a8920e7d 100755 --- a/docs/manuals/04-quickstart-setup-guide.md +++ b/docs/manuals/04-quickstart-setup-guide.md @@ -121,6 +121,9 @@ cargo build # Release build (slower compilation, optimized runtime) cargo build --release + +# Release build with mimalloc in place of the system allocator (ADR-0030) +cargo build --release --features mimalloc ``` The binary is at `target/release/extenddb` (or `target/debug/extenddb`).