Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/clippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions crates/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Comment thread
parsnips marked this conversation as resolved.
Resolved

[dependencies]
extenddb-app = { workspace = true }
Expand All @@ -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 }
Expand Down
7 changes: 7 additions & 0 deletions crates/bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
95 changes: 95 additions & 0 deletions docs/adr/0030-optional-mimalloc-allocator.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
3 changes: 3 additions & 0 deletions docs/manuals/04-quickstart-setup-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Loading