forked from weaselab/weaseldb
Compare commits
11
Commits
main
..
4b168c8688
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b168c8688 | ||
|
|
ad2650b0a6 | ||
|
|
6219592620 | ||
|
|
edfa71ce7c | ||
|
|
5790603e31 | ||
|
|
273f288020 | ||
|
|
14f8552906 | ||
|
|
c71bdf13c4 | ||
|
|
a0d64afd6f | ||
|
|
a377772e63 | ||
|
|
d7de96ef94 |
@@ -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
@@ -80,10 +80,9 @@ FetchContent_MakeAvailable(simdutf)
|
||||
|
||||
FetchContent_Declare(
|
||||
llhttp
|
||||
URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v9.2.1.tar.gz"
|
||||
URL_HASH
|
||||
SHA256=3c163891446e529604b590f9ad097b2e98b5ef7e4d3ddcf1cf98b62ca668f23e
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP ON)
|
||||
GIT_REPOSITORY https://github.com/nodejs/llhttp.git
|
||||
GIT_TAG 610a87d755f6bae466cd871c2ba97574ccac5483 # release/v9.2.1
|
||||
)
|
||||
set(BUILD_SHARED_LIBS
|
||||
OFF
|
||||
CACHE INTERNAL "")
|
||||
|
||||
Executable
+89
@@ -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
@@ -55,7 +55,7 @@ asm(".text\n"
|
||||
" b.ne .L_loop\n" // Branch back if not zero
|
||||
".L_end:\n" // End
|
||||
" ret\n" // Return
|
||||
".size spend_cpu_cycles, spend_cpu_cycles\n");
|
||||
".size spend_cpu_cycles, .-spend_cpu_cycles\n");
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
+58
-4
@@ -22,7 +22,11 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64)
|
||||
#include <immintrin.h>
|
||||
#elif defined(__aarch64__)
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
#include <simdutf.h>
|
||||
|
||||
#include "arena.hpp"
|
||||
@@ -1398,8 +1402,10 @@ void Gauge::set(double x) {
|
||||
Histogram::Histogram() = default;
|
||||
|
||||
// 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
|
||||
update_histogram_buckets_simd(std::span<const double> thresholds,
|
||||
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) {
|
||||
assert(p->thresholds.size() == p->shared.bucket_counts.size());
|
||||
@@ -1459,15 +1514,14 @@ void Histogram::observe(double x) {
|
||||
}
|
||||
|
||||
// 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.observations++;
|
||||
|
||||
p->mutex.unlock();
|
||||
} else {
|
||||
// Slow path: accumulate in pending (lock-free)
|
||||
update_histogram_buckets_simd(p->thresholds, p->pending.bucket_counts, x,
|
||||
0);
|
||||
update_histogram_buckets(p->thresholds, p->pending.bucket_counts, x, 0);
|
||||
p->pending.sum += x;
|
||||
p->pending.observations++;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[server]
|
||||
# Network interfaces to listen on - both TCP for external access and Unix socket for high-performance local testing
|
||||
interfaces = [
|
||||
{ type = "tcp", address = "127.0.0.1", port = 8080 },
|
||||
{ type = "tcp", address = "0.0.0.0", port = 8123 },
|
||||
{ type = "unix", path = "weaseldb.sock" }
|
||||
]
|
||||
# Maximum request size in bytes (for 413 Content Too Large responses)
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
- Load tester used 12 network threads
|
||||
- Load tester used 10 network threads
|
||||
- **0% CPU usage when idle** (optimized futex wake implementation)
|
||||
|
||||
### 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)**:
|
||||
|
||||
- **Throughput**: 1.0M requests/second
|
||||
- **Throughput**: ~825k requests/second (sustained over a 30-second run)
|
||||
- **Configurable CPU work**: 740ns (4000 iterations, validated with nanobench)
|
||||
- **Theoretical maximum CPU time**: 1000ns (1,000,000,000ns ÷ 1,000,000 req/s)
|
||||
- **CPU work efficiency**: 74% (740ns ÷ 1000ns)
|
||||
- **Theoretical maximum CPU time at this throughput**: ~1212ns (1,000,000,000ns ÷ 825,000 req/s)
|
||||
- **CPU work efficiency**: ~61% (740ns ÷ 1212ns)
|
||||
- **Pipeline stages**: Sequence (noop) → Resolve (CPU work) → Persist (response) → Release (cleanup)
|
||||
- **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
|
||||
- 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
|
||||
- Build: ninja
|
||||
- Build: ninja Release
|
||||
- Command: ./weaseldb --config test_benchmark_config.toml
|
||||
|
||||
Reference in New Issue
Block a user