Compare commits

..

No commits in common. "6b8fcffd00ad220439f892d72af8bfc325d54950" and "4ba25a9ad0a97f43f7fdcf3235f945026f2a24f6" have entirely different histories.

10 changed files with 42 additions and 247 deletions

View File

@ -1,6 +1,6 @@
# kv_bench (Mace vs RocksDB)
Quick start for reproducible Mace vs RocksDB comparison. Full guide: [docs/repro.md](./docs/repro.md).
Quick start for reproducible comparison. Full guide: [docs/repro.md](./docs/repro.md).
## 5-Minute Quickstart
1. Set your storage root (any mount path, not hardcoded to `/nvme`):
@ -13,13 +13,13 @@ mkdir -p "${KV_BENCH_STORAGE_ROOT}"
2. Initialize Python env once:
```bash
cd "$HOME/kv_bench/scripts"
cd /home/abby/kv_bench/scripts
./init.sh
source ./bin/activate
cd "$HOME/kv_bench"
cd /home/abby/kv_bench
```
3. Run baseline comparison (both engines append to the same CSV):
3. Run baseline comparison (both engines write to the same CSV):
```bash
rm -rf "${KV_BENCH_STORAGE_ROOT}/basic_mace" "${KV_BENCH_STORAGE_ROOT}/basic_rocks"
@ -29,32 +29,20 @@ mkdir -p "${KV_BENCH_STORAGE_ROOT}/basic_mace" "${KV_BENCH_STORAGE_ROOT}/basic_r
./scripts/rocksdb.sh "${KV_BENCH_STORAGE_ROOT}/basic_rocks" ./scripts/benchmark_results.csv
```
4. Plot results:
4. View and plot results:
```bash
./scripts/bin/python ./scripts/plot.py ./scripts/benchmark_results.csv ./scripts
```
5. Print a direct comparison table from the CSV:
```bash
./scripts/bin/python ./scripts/compare_baseline.py ./scripts/benchmark_results.csv
```
## What Is Compared
- Comparison unit: rows with identical `workload_id`, `threads`, `key_size`, `value_size`, `durability_mode`, `read_path`
- Throughput metric: workload-level `ops_per_sec` (higher is better)
- `W1/W2/W3/W4`: mixed read+update throughput
- `W5`: mixed read+update+scan throughput
- `W6`: scan throughput (counted by scan requests, not scanned key count)
- Tail latency metric: workload-level `p99_us` (lower is better)
- This is the mixed p99 of all operations executed in that workload row, not per-op-type p99
- `W1/W2/W3/W4`: mixed read+update p99
- `W5`: mixed read+update+scan p99
- `W6`: scan p99
- Reliability gate: if `error_ops > 0`, debug that case before drawing conclusions
Raw CSV path: `./scripts/benchmark_results.csv`
## Fast Result Reading
- Raw input CSV: `./scripts/benchmark_results.csv`
- Key columns:
- `engine` (`mace` / `rocksdb`)
- `workload_id` (`W1..W6`)
- `ops_per_sec` (higher is better)
- `p99_us` (lower is better)
- `error_ops` (must be 0 before drawing conclusions)
## Phase Reports
- Phase 1 (stability CV):

View File

@ -22,8 +22,8 @@ For the local profile, assume approximately:
Before running, set paths and verify filesystem type/capacity:
```bash
export KV_BENCH_STORAGE_ROOT="$HOME/kv_bench/target/repro_storage"
export KV_BENCH_RESULT_ROOT="$HOME/kv_bench/target/repro_results"
export KV_BENCH_STORAGE_ROOT=/home/abby/kv_bench/target/repro_storage
export KV_BENCH_RESULT_ROOT=/home/abby/kv_bench/target/repro_results
mkdir -p "${KV_BENCH_STORAGE_ROOT}" "${KV_BENCH_RESULT_ROOT}"
df -hT "${KV_BENCH_STORAGE_ROOT}" "${KV_BENCH_RESULT_ROOT}"
@ -36,10 +36,10 @@ Requirements:
## 3. Initialization
```bash
cd "$HOME/kv_bench/scripts"
cd /home/abby/kv_bench/scripts
./init.sh
source ./bin/activate
cd "$HOME/kv_bench"
cd /home/abby/kv_bench
```
## 4. Quick Baseline (W1~W6)
@ -68,17 +68,6 @@ Generate plots:
```
## 5. Phase Reproduction
Default thread points now follow host CPU count:
- If CPU count is a power of two: `1 2 4 ... N` (e.g., `4 -> 1 2 4`, `8 -> 1 2 4 8`, `16 -> 1 2 4 8 16`)
- Otherwise: odd-number progression up to near-full CPU usage (e.g., `12 -> 1 3 5 7 9 11`)
You can still override via `PHASE1_THREADS` / `PHASE2_THREADS_TIER_M` / `PHASE3_THREADS`.
Default KV profiles in scripts:
- `P1`: `16/128`
- `P2`: `32/1024`
- `P3`: `32/4096`
- `P4`: `32/16384` (large-value group)
### 5.1 Phase 1
```bash
@ -88,6 +77,7 @@ rm -f "${KV_BENCH_RESULT_ROOT}/phase1_results.csv"
WARMUP_SECS=10 MEASURE_SECS=20 REPEATS=2 \
PHASE1_WORKLOADS="W1 W3 W6" \
PHASE1_THREADS="1 6" \
PHASE1_PROFILES="P2" \
PHASE1_PREFILL_TIER_S_P2=200000 \
./scripts/phase1.sh "${KV_BENCH_STORAGE_ROOT}/phase1" "${KV_BENCH_RESULT_ROOT}/phase1_results.csv"
@ -101,6 +91,7 @@ rm -f "${KV_BENCH_RESULT_ROOT}/phase2_results.csv"
WARMUP_SECS=10 MEASURE_SECS=20 REPEATS=2 \
PHASE2_WORKLOADS_TIER_M="W1 W3 W6" \
PHASE2_THREADS_TIER_M="1 6" \
PHASE2_PROFILES="P2" \
PHASE2_PREFILL_TIER_M_P2=500000 \
RUN_TIER_L_REPRESENTATIVE=0 \
@ -122,6 +113,7 @@ rm -f "${KV_BENCH_RESULT_ROOT}/phase3_results.csv"
WARMUP_SECS=10 MEASURE_SECS=20 REPEATS=2 \
PHASE3_WORKLOADS="W1 W3" \
PHASE3_THREADS="1 6" \
PHASE3_DURABILITIES="relaxed durable" \
PHASE3_KEY_SIZE=32 PHASE3_VALUE_SIZE=1024 PHASE3_PREFILL_KEYS=500000 \
./scripts/phase3.sh "${KV_BENCH_STORAGE_ROOT}/phase3" "${KV_BENCH_RESULT_ROOT}/phase3_results.csv"

9
maritx.md Normal file
View File

@ -0,0 +1,9 @@
| 组合类型 | Key 大小 | Value 大小 | 典型场景 | 测试目标 |
|------------------|-------------------|---------------------------|------------------------------|--------------------------------------|
| 小 Key + 小 Value | 10-100 字节 | 100 字节 - 1 KB | 缓存用户信息、配置、热点数据 | 高吞吐量QPS、低延迟、内存效率 |
| 小 Key + 大 Value | 10-100 字节 | 1 KB - 数 MB | 存储文档、图片缩略图、日志 | 网络带宽利用率、持久化性能、压缩效率 |
| 大 Key + 小 Value | 1 KB - 10 KB | 100 字节 - 1 KB | 分布式锁、复杂命名空间 | Key 哈希性能、分片均衡性、元数据内存占用 |
| 大 Key + 大 Value | 1 KB - 10 KB | 数 MB - 100 MB | 极端负载测试、BLOB 存储 | 系统极限性能、I/O 吞吐、GC 压力 |
| 可变大小组合 | 10 字节 - 1 KB | 100 字节 - 10 MB | 混合业务负载(如电商系统) | 稳定性、缓存命中率、碎片化影响 |
| 超小 Value | 10-50 字节 | 1-8 字节 | 分布式锁、计数器(如 Redis 原子操作) | 高并发竞争性能、CAS 效率 |
| 超大 Value | 10-100 字节 | 接近系统上限(如 512 MB | 单 Value 极限测试 | 单线程阻塞风险、序列化/反序列化瓶颈 |

View File

@ -1,90 +0,0 @@
#!/usr/bin/env python3
import argparse
import sys
import pandas as pd
def main() -> int:
parser = argparse.ArgumentParser(
description="Compare mace vs rocksdb from benchmark_results.csv"
)
parser.add_argument(
"csv_path",
nargs="?",
default="./scripts/benchmark_results.csv",
help="Path to benchmark CSV (default: ./scripts/benchmark_results.csv)",
)
args = parser.parse_args()
df = pd.read_csv(args.csv_path)
required = {
"engine",
"workload_id",
"threads",
"key_size",
"value_size",
"durability_mode",
"read_path",
"ops_per_sec",
"p99_us",
"error_ops",
}
missing = required - set(df.columns)
if missing:
raise ValueError(f"Missing columns in csv: {sorted(missing)}")
keys = [
"workload_id",
"threads",
"key_size",
"value_size",
"durability_mode",
"read_path",
]
ok = df[df["error_ops"] == 0].copy()
if ok.empty:
print("No rows with error_ops == 0, cannot compare.")
return 0
agg = ok.groupby(keys + ["engine"], as_index=False).agg(
ops_per_sec=("ops_per_sec", "median"),
p99_us=("p99_us", "median"),
)
piv = agg.pivot_table(
index=keys,
columns="engine",
values=["ops_per_sec", "p99_us"],
aggfunc="first",
)
piv.columns = [f"{metric}_{engine}" for metric, engine in piv.columns]
out = piv.reset_index()
for col in [
"ops_per_sec_mace",
"ops_per_sec_rocksdb",
"p99_us_mace",
"p99_us_rocksdb",
]:
if col not in out.columns:
out[col] = pd.NA
out["qps_ratio_mace_over_rocksdb"] = (
out["ops_per_sec_mace"] / out["ops_per_sec_rocksdb"]
)
out["p99_ratio_mace_over_rocksdb"] = out["p99_us_mace"] / out["p99_us_rocksdb"]
out = out.sort_values(keys)
print(out.to_string(index=False))
print("\nInterpretation:")
print("- qps_ratio_mace_over_rocksdb > 1: mace has higher throughput")
print("- p99_ratio_mace_over_rocksdb < 1: mace has lower p99 latency")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -9,8 +9,6 @@ fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "${script_dir}/.." && pwd)"
# shellcheck source=./thread_points.sh
. "${script_dir}/thread_points.sh"
# The runner creates per-case unique paths under this root; each path must not exist.
db_root="$1"
@ -27,16 +25,9 @@ mkdir -p "$(dirname -- "${result_file}")"
cargo build --release --manifest-path "${root_dir}/Cargo.toml"
workloads=(W1 W2 W3 W4 W5 W6)
mace_threads_raw="${MACE_THREADS:-$(default_thread_points)}"
IFS=' ' read -r -a threads <<< "${mace_threads_raw}"
if [ "${#threads[@]}" -eq 0 ]; then
printf "mace threads list must not be empty\n" >&2
exit 1
fi
threads=(1 6 12)
profiles=(
"16 128"
"32 1024"
"32 4096"
"32 16384"
)

View File

@ -9,8 +9,6 @@ fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "${script_dir}/.." && pwd)"
# shellcheck source=./thread_points.sh
. "${script_dir}/thread_points.sh"
python_bin="${PYTHON_BIN:-${root_dir}/scripts/bin/python}"
if [ ! -x "${python_bin}" ]; then
python_bin="${PYTHON:-python3}"
@ -25,15 +23,10 @@ repeats="${REPEATS:-3}"
read_path="${READ_PATH:-snapshot}"
phase1_workloads_raw="${PHASE1_WORKLOADS:-W1 W3 W6}"
phase1_threads_raw="${PHASE1_THREADS:-}"
if [ -z "${phase1_threads_raw}" ]; then
phase1_threads_raw="$(default_thread_points)"
fi
phase1_profiles_raw="${PHASE1_PROFILES:-P1 P2 P3 P4}"
phase1_prefill_tier_s_p1="${PHASE1_PREFILL_TIER_S_P1:-44739242}"
phase1_threads_raw="${PHASE1_THREADS:-1 12}"
phase1_profiles_raw="${PHASE1_PROFILES:-P2 P3}"
phase1_prefill_tier_s_p2="${PHASE1_PREFILL_TIER_S_P2:-6100805}"
phase1_prefill_tier_s_p3="${PHASE1_PREFILL_TIER_S_P3:-1560671}"
phase1_prefill_tier_s_p4="${PHASE1_PREFILL_TIER_S_P4:-392449}"
phase1_prefill_tier_s_p3="${PHASE1_PREFILL_TIER_S_P3:-392449}"
mkdir -p "${db_root}"
mkdir -p "$(dirname -- "${result_file}")"
@ -53,30 +46,24 @@ fi
profile_key() {
case "$1" in
P1) echo 16 ;;
P2) echo 32 ;;
P3) echo 32 ;;
P4) echo 32 ;;
*) printf "unknown profile: %s\n" "$1" >&2; exit 1 ;;
esac
}
profile_val() {
case "$1" in
P1) echo 128 ;;
P2) echo 1024 ;;
P3) echo 4096 ;;
P4) echo 16384 ;;
P3) echo 16384 ;;
*) printf "unknown profile: %s\n" "$1" >&2; exit 1 ;;
esac
}
profile_prefill_tier_s() {
case "$1" in
P1) echo "${phase1_prefill_tier_s_p1}" ;;
P2) echo "${phase1_prefill_tier_s_p2}" ;;
P3) echo "${phase1_prefill_tier_s_p3}" ;;
P4) echo "${phase1_prefill_tier_s_p4}" ;;
*) printf "unknown profile: %s\n" "$1" >&2; exit 1 ;;
esac
}

View File

@ -9,8 +9,6 @@ fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "${script_dir}/.." && pwd)"
# shellcheck source=./thread_points.sh
. "${script_dir}/thread_points.sh"
python_bin="${PYTHON_BIN:-${root_dir}/scripts/bin/python}"
if [ ! -x "${python_bin}" ]; then
python_bin="${PYTHON:-python3}"
@ -28,23 +26,13 @@ tier_l_repeats="${TIER_L_REPEATS:-1}"
phase2_workloads_tier_m_raw="${PHASE2_WORKLOADS_TIER_M:-W1 W2 W3 W4 W6}"
phase2_workloads_tier_l_rep_raw="${PHASE2_WORKLOADS_TIER_L_REP:-W1 W3 W6}"
phase2_threads_tier_m_raw="${PHASE2_THREADS_TIER_M:-}"
if [ -z "${phase2_threads_tier_m_raw}" ]; then
phase2_threads_tier_m_raw="$(default_thread_points)"
fi
phase2_threads_tier_l_rep_raw="${PHASE2_THREADS_TIER_L_REP:-}"
if [ -z "${phase2_threads_tier_l_rep_raw}" ]; then
phase2_threads_tier_l_rep_raw="$(default_thread_points)"
fi
phase2_profiles_raw="${PHASE2_PROFILES:-P1 P2 P3 P4}"
phase2_prefill_tier_m_p1="${PHASE2_PREFILL_TIER_M_P1:-134217728}"
phase2_threads_tier_m_raw="${PHASE2_THREADS_TIER_M:-1 6 12}"
phase2_threads_tier_l_rep_raw="${PHASE2_THREADS_TIER_L_REP:-1 12}"
phase2_profiles_raw="${PHASE2_PROFILES:-P2 P3}"
phase2_prefill_tier_m_p2="${PHASE2_PREFILL_TIER_M_P2:-18302417}"
phase2_prefill_tier_m_p3="${PHASE2_PREFILL_TIER_M_P3:-4682013}"
phase2_prefill_tier_m_p4="${PHASE2_PREFILL_TIER_M_P4:-1177348}"
phase2_prefill_tier_l_p1="${PHASE2_PREFILL_TIER_L_P1:-208783132}"
phase2_prefill_tier_m_p3="${PHASE2_PREFILL_TIER_M_P3:-1177348}"
phase2_prefill_tier_l_p2="${PHASE2_PREFILL_TIER_L_P2:-28470427}"
phase2_prefill_tier_l_p3="${PHASE2_PREFILL_TIER_L_P3:-7283132}"
phase2_prefill_tier_l_p4="${PHASE2_PREFILL_TIER_L_P4:-1831430}"
phase2_prefill_tier_l_p3="${PHASE2_PREFILL_TIER_L_P3:-1831430}"
mkdir -p "${db_root}"
mkdir -p "$(dirname -- "${result_file}")"
@ -71,20 +59,16 @@ fi
profile_key() {
case "$1" in
P1) echo 16 ;;
P2) echo 32 ;;
P3) echo 32 ;;
P4) echo 32 ;;
*) printf "unknown profile: %s\n" "$1" >&2; exit 1 ;;
esac
}
profile_val() {
case "$1" in
P1) echo 128 ;;
P2) echo 1024 ;;
P3) echo 4096 ;;
P4) echo 16384 ;;
P3) echo 16384 ;;
*) printf "unknown profile: %s\n" "$1" >&2; exit 1 ;;
esac
}
@ -94,18 +78,14 @@ prefill_for() {
local profile="$2"
if [ "${tier}" = "tier-m" ]; then
case "${profile}" in
P1) echo "${phase2_prefill_tier_m_p1}" ;;
P2) echo "${phase2_prefill_tier_m_p2}" ;;
P3) echo "${phase2_prefill_tier_m_p3}" ;;
P4) echo "${phase2_prefill_tier_m_p4}" ;;
*) printf "unknown profile: %s\n" "${profile}" >&2; exit 1 ;;
esac
elif [ "${tier}" = "tier-l" ]; then
case "${profile}" in
P1) echo "${phase2_prefill_tier_l_p1}" ;;
P2) echo "${phase2_prefill_tier_l_p2}" ;;
P3) echo "${phase2_prefill_tier_l_p3}" ;;
P4) echo "${phase2_prefill_tier_l_p4}" ;;
*) printf "unknown profile: %s\n" "${profile}" >&2; exit 1 ;;
esac
else

View File

@ -9,8 +9,6 @@ fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "${script_dir}/.." && pwd)"
# shellcheck source=./thread_points.sh
. "${script_dir}/thread_points.sh"
python_bin="${PYTHON_BIN:-${root_dir}/scripts/bin/python}"
if [ ! -x "${python_bin}" ]; then
python_bin="${PYTHON:-python3}"
@ -25,10 +23,7 @@ repeats="${REPEATS:-5}"
read_path="${READ_PATH:-snapshot}"
phase3_workloads_raw="${PHASE3_WORKLOADS:-W1 W3 W6}"
phase3_threads_raw="${PHASE3_THREADS:-}"
if [ -z "${phase3_threads_raw}" ]; then
phase3_threads_raw="$(default_thread_points)"
fi
phase3_threads_raw="${PHASE3_THREADS:-1 12}"
phase3_durabilities_raw="${PHASE3_DURABILITIES:-relaxed durable}"
key_size="${PHASE3_KEY_SIZE:-32}"
value_size="${PHASE3_VALUE_SIZE:-1024}"

View File

@ -10,8 +10,6 @@ fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "${script_dir}/.." && pwd)"
rocksdb_dir="${root_dir}/rocksdb"
# shellcheck source=./thread_points.sh
. "${script_dir}/thread_points.sh"
db_root="$1"
result_file="${2:-${script_dir}/benchmark_results.csv}"
@ -28,16 +26,9 @@ mkdir -p "$(dirname -- "${result_file}")"
(cd "${rocksdb_dir}" && cmake --build --preset release)
workloads=(W1 W2 W3 W4 W5 W6)
rocksdb_threads_raw="${ROCKSDB_THREADS:-$(default_thread_points)}"
IFS=' ' read -r -a threads <<< "${rocksdb_threads_raw}"
if [ "${#threads[@]}" -eq 0 ]; then
printf "rocksdb threads list must not be empty\n" >&2
exit 1
fi
threads=(1 6 12)
profiles=(
"16 128"
"32 1024"
"32 4096"
"32 16384"
)

View File

@ -1,48 +0,0 @@
#!/usr/bin/env bash
# Shared helpers for choosing default thread points from host CPU count.
detect_logical_cpus() {
local detected
if detected="$(nproc 2>/dev/null)"; then
:
elif detected="$(getconf _NPROCESSORS_ONLN 2>/dev/null)"; then
:
else
detected=1
fi
if [[ ! "${detected}" =~ ^[0-9]+$ ]] || [ "${detected}" -lt 1 ]; then
detected=1
fi
printf "%s\n" "${detected}"
}
is_power_of_two() {
local n="$1"
[ "${n}" -gt 0 ] && [ $((n & (n - 1))) -eq 0 ]
}
default_thread_points() {
local cpu_count="${1:-$(detect_logical_cpus)}"
local points=()
local t
if is_power_of_two "${cpu_count}"; then
t=1
while [ "${t}" -le "${cpu_count}" ]; do
points+=("${t}")
t=$((t * 2))
done
else
t=1
while [ "${t}" -le "${cpu_count}" ]; do
points+=("${t}")
t=$((t + 2))
done
fi
printf "%s\n" "${points[*]}"
}