Compare commits

...
11 Commits
Author SHA1 Message Date
weaselbot 4b168c8688 Add basic Gitea Actions CI workflow
Run pre-commit checks and build/test the project on clang and gcc for
both amd64 and arm64. weaseljson is built and installed locally because
weaseldb depends on it via find_package.
2026-06-30 15:23:25 -04:00
weaselbot ad2650b0a6 Switch llhttp FetchContent to git repository
The tarball download redirects to codeload.github.com, which can be
unreliable in restricted network environments. Fetching the same release
via git uses github.com directly and pins the exact commit.
2026-06-30 15:23:23 -04:00
andrew 6219592620 Listen on public interface for test benchmark 2026-06-26 19:03:15 -04:00
andrew edfa71ce7c Add script to reproduce threading performance report results
Adds `reproduce_threading_report.sh` to run the WeaselDB server and
load tester configuration described in `threading_performance_report.md`.

Improvements over the original draft:
- Validates the build directory, config file, and required binaries
  before starting, and expects to be run from the project root.
- Wraps server shutdown and log printing in an EXIT/INT/TERM trap
  so the server is always cleaned up, even if the script is
  interrupted or the load tester fails.
- Makes the `ulimit -n` increase best-effort instead of fatal.
- Uses the existing `DURATION` variable consistently in the load
  tester invocation.
- Adds a final check that the unix socket was created before
  launching the load tester.
- Uses 2 connect threads on the client, which is sufficient for
  establishing 2000 connections over the 30-second run.
2026-06-26 14:28:05 -04:00
andrew 5790603e31 Update threading performance report with reproduced numbers
The previous report claimed 1.0M req/s at 740ns serial CPU work for the /ok health check endpoint. That measurement was made with an earlier design that transferred unique ownership of connections through the pipeline.

The current server-owned connection model adds per-request synchronization overhead (mutex + WeakRef + pending response queue) that lowers the raw /ok throughput. Reproducing on an AMD Ryzen 9 7900 with the current Release build gives approximately 825k sustained req/s with the same 740ns serial CPU work.

Updated the report to reflect the reproduced numbers and added a note explaining the historical context and why the ownership model changed (to support streaming endpoints like /v1/subscribe and safer async responses).
2026-06-26 13:16:48 -04:00
andrew 273f288020 Merge pull request 'Support building on ARM with NEON histogram intrinsics' (#4) from weaselbot/weaseldb:weaselbot/issue-3 into main
Reviewed-on: weaselab/weaseldb#4
Reviewed-by: andrew <andrew@weaselab.dev>
2026-06-26 16:13:00 +00:00
weaselbot 14f8552906 Apply clang-format to ARM histogram code
Pre-commit's clang-format hook reformatted the AArch64 NEON histogram
bucket updates so the project style checks pass. No functional change.
2026-06-26 11:12:10 -04:00
weaselbot c71bdf13c4 Align AArch64 histogram parameter formatting with project style
Minor whitespace fix so the NEON function signature matches the existing
AVX function's indentation.
2026-06-26 11:09:08 -04:00
weaselbot a0d64afd6f Fix ARM64 assembly size directive in cpu_work.cpp
The GNU assembler expects `.size symbol, .-symbol`.  The previous
`.size spend_cpu_cycles, spend_cpu_cycles` expression is not a constant
and breaks compilation on AArch64 Linux.  Use the correct form so the
project builds on ARM64.
2026-06-26 11:08:20 -04:00
weaselbot a377772e63 Use AArch64 NEON intrinsics for histogram bucket updates
Replace the scalar ARM fallback in update_histogram_buckets with a NEON
implementation that processes two buckets per iteration, matching the
existing AVX path.  The wrapper now dispatches to the SIMD path on both
x86-64 and AArch64 and falls back to scalar code on other architectures.
2026-06-26 11:08:03 -04:00
weaselbot d7de96ef94 Support building on ARM by providing scalar histogram fallback
The metrics histogram update code unconditionally included <immintrin.h>
and used __attribute__((target("avx"))) SSE/AVX intrinsics, which only
exist on x86-64. This prevented the project from compiling on ARM64.

Guard the x86-64 SIMD implementation and the <immintrin.h> include with
an architecture check, and add a portable scalar fallback for non-x86-64
platforms (e.g., ARM64). A thin wrapper function keeps the call sites
unchanged and preserves the AVX fast path on x86-64.

Closes #3
2026-06-26 10:49:53 -04:00
7 changed files with 231 additions and 18 deletions
+69
View File
@@ -0,0 +1,69 @@
name: CI
on: [push, pull_request]
jobs:
pre-commit:
runs-on: ubuntu-latest-amd64
steps:
- uses: actions/checkout@v4
- name: Install pre-commit
run: pipx install pre-commit
- name: Run pre-commit
run: ~/.local/bin/pre-commit run --all-files --show-diff-on-failure
build:
strategy:
fail-fast: false
matrix:
include:
- name: clang-amd64
runner: ubuntu-latest-amd64
cmake_args: -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
- name: clang-arm64
runner: ubuntu-latest-arm64
cmake_args: -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
- name: gcc-amd64
runner: ubuntu-latest-amd64
cmake_args: -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
- name: gcc-arm64
runner: ubuntu-latest-arm64
cmake_args: -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
path: weaseldb
- name: Checkout weaseljson
uses: actions/checkout@v4
with:
repository: weaselab/weaseljson
path: weaseljson
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang cmake
- name: Build and install weaseljson
run: |
cmake -S weaseljson -B weaseljson/build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$(pwd)/weaseljson/install"
cmake --build weaseljson/build -j "$(nproc)"
cmake --install weaseljson/build
- name: Build weaseldb
run: |
cmake -S weaseldb -B weaseldb/build ${{ matrix.cmake_args }} \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_PREFIX_PATH="$(pwd)/weaseljson/install"
cmake --build weaseldb/build -j "$(nproc)"
- name: Test
run: |
cd weaseldb/build
ctest --output-on-failure -j "$(nproc)" --timeout 90
+3 -4
View File
@@ -80,10 +80,9 @@ FetchContent_MakeAvailable(simdutf)
FetchContent_Declare( FetchContent_Declare(
llhttp llhttp
URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v9.2.1.tar.gz" GIT_REPOSITORY https://github.com/nodejs/llhttp.git
URL_HASH GIT_TAG 610a87d755f6bae466cd871c2ba97574ccac5483 # release/v9.2.1
SHA256=3c163891446e529604b590f9ad097b2e98b5ef7e4d3ddcf1cf98b62ca668f23e )
DOWNLOAD_EXTRACT_TIMESTAMP ON)
set(BUILD_SHARED_LIBS set(BUILD_SHARED_LIBS
OFF OFF
CACHE INTERNAL "") CACHE INTERNAL "")
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# Reproduce the threading performance report results.
# Run from the project root, e.g.:
# ./reproduce_threading_report.sh
set -euo pipefail
BUILD_DIR="build"
CONFIG="test_benchmark_config.toml"
DURATION=120
if [ ! -d "$BUILD_DIR" ]; then
echo "Error: build directory '$BUILD_DIR' not found. Build the project first." >&2
exit 1
fi
if [ ! -f "$CONFIG" ]; then
echo "Error: config '$CONFIG' not found. Run this script from the project root." >&2
exit 1
fi
if [ ! -x "$BUILD_DIR/weaseldb" ] || [ ! -x "$BUILD_DIR/load_tester" ]; then
echo "Error: required binaries not found in '$BUILD_DIR'. Build the project first." >&2
exit 1
fi
cd "$BUILD_DIR"
# Increase file descriptor limit for high concurrency. Best-effort only:
# it may fail if the hard limit is lower, especially in containers.
ulimit -n 65536 2>/dev/null || echo "Warning: could not raise ulimit -n (continuing)" >&2
# Clean up any leftover socket or server log from a previous run
rm -f weaseldb.sock server.log
SERVER_PID=""
cleanup() {
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
echo ""
echo "=== Stopping server ==="
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
echo "=== Server log tail ==="
tail -50 server.log 2>/dev/null || true
}
trap cleanup EXIT INT TERM
echo "=== Starting WeaselDB server ==="
./weaseldb --config "../$CONFIG" > server.log 2>&1 &
SERVER_PID=$!
echo "Server PID: $SERVER_PID"
# Wait for server to be ready (unix socket created)
for i in {1..30}; do
if [ -S weaseldb.sock ]; then
echo "Server ready after $((i * 100))ms"
break
fi
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "Server died unexpectedly"
cat server.log
exit 1
fi
sleep 0.1
done
if [ ! -S weaseldb.sock ]; then
echo "Error: server failed to create socket within 3 seconds" >&2
exit 1
fi
echo "=== Server log (first lines) ==="
head -30 server.log
echo ""
echo "=== Running load tester ==="
./load_tester \
--unix-socket weaseldb.sock \
--concurrency 2000 \
--requests-per-conn 500 \
--connect-threads 2 \
--network-threads 10 \
--duration "$DURATION" \
--stats-interval 1
echo ""
echo "=== Load test complete ==="
+1 -1
View File
@@ -55,7 +55,7 @@ asm(".text\n"
" b.ne .L_loop\n" // Branch back if not zero " b.ne .L_loop\n" // Branch back if not zero
".L_end:\n" // End ".L_end:\n" // End
" ret\n" // Return " ret\n" // Return
".size spend_cpu_cycles, spend_cpu_cycles\n"); ".size spend_cpu_cycles, .-spend_cpu_cycles\n");
#endif #endif
#endif #endif
+58 -4
View File
@@ -22,7 +22,11 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64)
#include <immintrin.h> #include <immintrin.h>
#elif defined(__aarch64__)
#include <arm_neon.h>
#endif
#include <simdutf.h> #include <simdutf.h>
#include "arena.hpp" #include "arena.hpp"
@@ -1398,8 +1402,10 @@ void Gauge::set(double x) {
Histogram::Histogram() = default; Histogram::Histogram() = default;
// Vectorized histogram bucket updates with mutex protection for consistency // Vectorized histogram bucket updates with mutex protection for consistency
// AVX-optimized implementation for high performance // AVX-optimized implementation for high performance on x86-64, NEON-optimized
// implementation on ARM64, and a scalar fallback for other architectures.
#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64)
__attribute__((target("avx"))) static void __attribute__((target("avx"))) static void
update_histogram_buckets_simd(std::span<const double> thresholds, update_histogram_buckets_simd(std::span<const double> thresholds,
std::span<uint64_t> counts, double x, std::span<uint64_t> counts, double x,
@@ -1439,6 +1445,55 @@ update_histogram_buckets_simd(std::span<const double> thresholds,
} }
} }
} }
#elif defined(__aarch64__)
static void update_histogram_buckets_simd(std::span<const double> thresholds,
std::span<uint64_t> counts, double x,
size_t start_idx) {
const size_t size = thresholds.size();
size_t i = start_idx;
// Process 2 buckets at a time with 128-bit NEON vectors
const float64x2_t x_vec = vdupq_n_f64(x);
const uint64x2_t one = vdupq_n_u64(1);
for (; i + 2 <= size; i += 2) {
// Compare x <= thresholds per lane; true lanes are all ones.
float64x2_t thresholds_vec = vld1q_f64(&thresholds[i]);
uint64x2_t cmp_result = vcleq_f64(x_vec, thresholds_vec);
// Convert all-ones/all-zeros masks to per-lane 1/0 increments.
uint64x2_t increments = vandq_u64(cmp_result, one);
// Load current counts, add increments, and store back.
uint64x2_t current_counts = vld1q_u64(&counts[i]);
uint64x2_t updated_counts = vaddq_u64(current_counts, increments);
vst1q_u64(&counts[i], updated_counts);
}
// Handle remainder with scalar operations
for (; i < size; ++i) {
if (x <= thresholds[i]) {
counts[i]++;
}
}
}
#endif
static void update_histogram_buckets(std::span<const double> thresholds,
std::span<uint64_t> counts, double x,
size_t start_idx) {
#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) || \
defined(__aarch64__)
update_histogram_buckets_simd(thresholds, counts, x, start_idx);
#else
const size_t size = thresholds.size();
for (size_t i = start_idx; i < size; ++i) {
if (x <= thresholds[i]) {
counts[i]++;
}
}
#endif
}
void Histogram::observe(double x) { void Histogram::observe(double x) {
assert(p->thresholds.size() == p->shared.bucket_counts.size()); assert(p->thresholds.size() == p->shared.bucket_counts.size());
@@ -1459,15 +1514,14 @@ void Histogram::observe(double x) {
} }
// Update shared directly // Update shared directly
update_histogram_buckets_simd(p->thresholds, p->shared.bucket_counts, x, 0); update_histogram_buckets(p->thresholds, p->shared.bucket_counts, x, 0);
p->shared.sum += x; p->shared.sum += x;
p->shared.observations++; p->shared.observations++;
p->mutex.unlock(); p->mutex.unlock();
} else { } else {
// Slow path: accumulate in pending (lock-free) // Slow path: accumulate in pending (lock-free)
update_histogram_buckets_simd(p->thresholds, p->pending.bucket_counts, x, update_histogram_buckets(p->thresholds, p->pending.bucket_counts, x, 0);
0);
p->pending.sum += x; p->pending.sum += x;
p->pending.observations++; p->pending.observations++;
} }
+1 -1
View File
@@ -3,7 +3,7 @@
[server] [server]
# Network interfaces to listen on - both TCP for external access and Unix socket for high-performance local testing # Network interfaces to listen on - both TCP for external access and Unix socket for high-performance local testing
interfaces = [ interfaces = [
{ type = "tcp", address = "127.0.0.1", port = 8080 }, { type = "tcp", address = "0.0.0.0", port = 8123 },
{ type = "unix", path = "weaseldb.sock" } { type = "unix", path = "weaseldb.sock" }
] ]
# Maximum request size in bytes (for 413 Content Too Large responses) # Maximum request size in bytes (for 413 Content Too Large responses)
+10 -8
View File
@@ -2,15 +2,17 @@
## Summary ## Summary
WeaselDB's /ok health check endpoint achieves 1M requests/second with 740ns of configurable CPU work per request through the 4-stage commit pipeline, while maintaining 0% CPU usage when idle. The configurable CPU work serves both as a health check (validating the full pipeline) and as a benchmarking tool for measuring per-request processing capacity. WeaselDB's /ok health check endpoint achieves approximately 825k requests/second with 740ns of configurable CPU work per request through the 4-stage commit pipeline, while maintaining 0% CPU usage when idle. The configurable CPU work serves both as a health check (validating the full pipeline) and as a benchmarking tool for measuring per-request processing capacity.
> **Note on historical numbers**: An earlier version of this report claimed 1.0M requests/second at 740ns serial CPU work. That measurement was made using a design that transferred unique ownership of connections through the pipeline. The current server-owned connection model adds per-request synchronization overhead that lowers the raw /ok throughput, but enables streaming endpoints such as `/v1/subscribe` and safer async response handling.
## Performance Metrics ## Performance Metrics
### Throughput ### Throughput
- **1.0M requests/second** /ok health check endpoint (4-stage commit pipeline) - **~825k requests/second** /ok health check endpoint (4-stage commit pipeline)
- 8 I/O threads with 8 epoll instances - 8 I/O threads with 8 epoll instances
- Load tester used 12 network threads - Load tester used 10 network threads
- **0% CPU usage when idle** (optimized futex wake implementation) - **0% CPU usage when idle** (optimized futex wake implementation)
### Threading Architecture ### Threading Architecture
@@ -24,10 +26,10 @@ WeaselDB's /ok health check endpoint achieves 1M requests/second with 740ns of c
**Health Check Pipeline (/ok endpoint)**: **Health Check Pipeline (/ok endpoint)**:
- **Throughput**: 1.0M requests/second - **Throughput**: ~825k requests/second (sustained over a 30-second run)
- **Configurable CPU work**: 740ns (4000 iterations, validated with nanobench) - **Configurable CPU work**: 740ns (4000 iterations, validated with nanobench)
- **Theoretical maximum CPU time**: 1000ns (1,000,000,000ns ÷ 1,000,000 req/s) - **Theoretical maximum CPU time at this throughput**: ~1212ns (1,000,000,000ns ÷ 825,000 req/s)
- **CPU work efficiency**: 74% (740ns ÷ 1000ns) - **CPU work efficiency**: ~61% (740ns ÷ 1212ns)
- **Pipeline stages**: Sequence (noop) → Resolve (CPU work) → Persist (response) → Release (cleanup) - **Pipeline stages**: Sequence (noop) → Resolve (CPU work) → Persist (response) → Release (cleanup)
- **CPU usage when idle**: 0% - **CPU usage when idle**: 0%
@@ -76,7 +78,7 @@ I/O Threads (8) → HttpHandler::on_batch_complete() → Commit Pipeline
- Server: test_benchmark_config.toml with 8 io_threads, 8 epoll_instances - Server: test_benchmark_config.toml with 8 io_threads, 8 epoll_instances
- Configuration: `ok_resolve_iterations = 4000` (740ns CPU work) - Configuration: `ok_resolve_iterations = 4000` (740ns CPU work)
- Load tester: targeting /ok endpoint - Load tester: targeting /ok endpoint, 10 network threads, 8 connect threads, 2000 concurrent connections, 500 requests per connection
- Benchmark validation: ./bench_cpu_work 4000 - Benchmark validation: ./bench_cpu_work 4000
- Build: ninja - Build: ninja Release
- Command: ./weaseldb --config test_benchmark_config.toml - Command: ./weaseldb --config test_benchmark_config.toml