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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
/docker/build
/logs
/jetstream
/voipgrid

# Architecture specific extensions/prefixes
*.[568vq]
Expand Down Expand Up @@ -81,3 +82,6 @@ go.work*

# helm chart
helm/dendrite/charts/

# agents
CLAUDE.md
3 changes: 3 additions & 0 deletions benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
matrix_key.pem
dendrite-benchmark
results.txt
65 changes: 65 additions & 0 deletions benchmark/dendrite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
version: 2

global:
server_name: localhost
private_key: matrix_key.pem
key_validity_period: 168h0m0s

database:
connection_string: postgresql://dendrite:benchsecret@postgres/dendrite?sslmode=disable
max_open_conns: 90
max_idle_conns: 5
conn_max_lifetime: -1

cache:
max_size_estimated: 1gb
max_age: 1h

disable_federation: true

presence:
enable_inbound: false
enable_outbound: false

report_stats:
enabled: false

jetstream:
storage_path: ./
topic_prefix: Dendrite

client_api:
registration_disabled: true
guests_disabled: true
registration_shared_secret: "benchmarksecret"
enable_registration_captcha: false

rate_limiting:
enabled: false

media_api:
base_path: ./media_store
max_file_size_bytes: 10485760
dynamic_thumbnails: false
max_thumbnail_generators: 10
thumbnail_sizes:
- width: 32
height: 32
method: crop
- width: 96
height: 96
method: crop
- width: 640
height: 480
method: scale

sync_api:
search:
enabled: false

user_api:
bcrypt_cost: 4 # Low cost for benchmarking speed

logging:
- type: std
level: warn
49 changes: 49 additions & 0 deletions benchmark/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
version: "3.4"

services:
postgres:
hostname: postgres
image: postgres:15-alpine
restart: "no"
volumes:
- dendrite_bench_pg:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: benchsecret
POSTGRES_USER: dendrite
POSTGRES_DATABASE: dendrite
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dendrite"]
interval: 2s
timeout: 5s
retries: 10
networks:
- bench

dendrite:
build:
context: ..
dockerfile: Dockerfile
hostname: dendrite
ports:
- "8008:8008"
volumes:
- ./dendrite.yaml:/etc/dendrite/dendrite.yaml
- ./matrix_key.pem:/etc/dendrite/matrix_key.pem
- dendrite_bench_media:/var/dendrite/media
- dendrite_bench_jetstream:/var/dendrite/jetstream
- dendrite_bench_search:/var/dendrite/searchindex
depends_on:
postgres:
condition: service_healthy
networks:
- bench
restart: "no"

networks:
bench:

volumes:
dendrite_bench_pg:
dendrite_bench_media:
dendrite_bench_jetstream:
dendrite_bench_search:
171 changes: 171 additions & 0 deletions benchmark/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# Dendrite Performance Benchmark - Automated Runner
# Usage: ./benchmark/run.sh [--rooms N] [--users N] [--concurrent N]
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BENCHMARK_DIR="$SCRIPT_DIR"

# Defaults
ROOMS=100
USERS=10
CONCURRENT=10
SHARED_SECRET="benchmarksecret"
BASE_URL="http://localhost:8008"
BENCHMARKS="all"
JSON_OUTPUT="benchmark/results.json"
DURATION="60s"
MESSAGE_COUNT=1000
PPROF_URL="http://localhost:6060"
SYNAPSE_BASELINE=""

# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--rooms) ROOMS="$2"; shift 2 ;;
--users) USERS="$2"; shift 2 ;;
--concurrent) CONCURRENT="$2"; shift 2 ;;
--url) BASE_URL="$2"; shift 2 ;;
--benchmarks) BENCHMARKS="$2"; shift 2 ;;
--json-output) JSON_OUTPUT="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
--message-count) MESSAGE_COUNT="$2"; shift 2 ;;
--pprof-url) PPROF_URL="$2"; shift 2 ;;
--synapse-baseline) SYNAPSE_BASELINE="$2"; shift 2 ;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --rooms N Number of rooms to create (default: 100)"
echo " --users N Number of test users (default: 10)"
echo " --concurrent N Concurrent workers (default: 10)"
echo " --url URL Dendrite base URL (default: http://localhost:8008)"
echo " --benchmarks LIST Comma-separated benchmarks (default: all)"
echo " Options: incremental-sync,sliding-sync,message-send,mixed,profile,report"
echo " --json-output PATH Path for JSON results (default: benchmark/results.json)"
echo " --duration DUR Mixed workload duration (default: 60s)"
echo " --message-count N Messages for message-send benchmark (default: 1000)"
echo " --pprof-url URL Dendrite pprof endpoint (default: http://localhost:6060)"
echo " --synapse-baseline F Path to Synapse baseline JSON for comparison"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done

echo "============================================"
echo " Dendrite Performance Benchmark"
echo "============================================"
echo ""

# Step 1: Generate signing key if needed
if [ ! -f "$BENCHMARK_DIR/matrix_key.pem" ]; then
echo "[1/5] Generating signing key..."
cd "$PROJECT_DIR"
go run ./cmd/generate-keys --private-key "$BENCHMARK_DIR/matrix_key.pem"
else
echo "[1/5] Signing key exists."
fi

# Step 2: Build benchmark binary
echo "[2/5] Building benchmark tool..."
cd "$PROJECT_DIR"
go build -o "$BENCHMARK_DIR/dendrite-benchmark" ./cmd/dendrite-benchmark/

# Step 3: Start Dendrite + PostgreSQL
echo "[3/5] Starting Dendrite with PostgreSQL..."
cd "$BENCHMARK_DIR"
docker compose down -v 2>/dev/null || true
docker compose up -d --build

# Wait for Dendrite to be ready
echo " Waiting for Dendrite to start..."
for i in $(seq 1 60); do
if curl -sf "$BASE_URL/_matrix/client/v3/login" > /dev/null 2>&1; then
echo " Dendrite is ready! (took ${i}s)"
break
fi
if [ "$i" -eq 60 ]; then
echo " ERROR: Dendrite failed to start within 60s"
docker compose logs dendrite | tail -30
exit 1
fi
sleep 1
done

# Step 4: Create admin user via shared secret registration
echo "[4/5] Creating admin user..."
ADMIN_NONCE=$(curl -sf "$BASE_URL/_synapse/admin/v1/register" | python3 -c "import sys,json; print(json.load(sys.stdin)['nonce'])" 2>/dev/null || true)

if [ -z "$ADMIN_NONCE" ]; then
# Dendrite uses a different registration endpoint - use create-account
echo " Using create-account for admin user..."
docker compose exec -T dendrite /usr/bin/create-account \
-config /etc/dendrite/dendrite.yaml \
-username admin \
-password adminpass123 \
-admin 2>/dev/null || echo " (admin user may already exist)"

# Login to get token
ADMIN_TOKEN=$(curl -sf "$BASE_URL/_matrix/client/v3/login" -d '{
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "admin"},
"password": "adminpass123"
}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
else
# Synapse-style shared secret registration
MAC=$(echo -n "${ADMIN_NONCE}\x00admin\x00adminpass123\x00admin" | openssl dgst -sha1 -hmac "$SHARED_SECRET" | awk '{print $2}')
ADMIN_TOKEN=$(curl -sf "$BASE_URL/_synapse/admin/v1/register" -d "{
\"nonce\": \"$ADMIN_NONCE\",
\"username\": \"admin\",
\"password\": \"adminpass123\",
\"admin\": true,
\"mac\": \"$MAC\"
}" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
fi

if [ -z "$ADMIN_TOKEN" ]; then
echo " ERROR: Failed to get admin token"
exit 1
fi
echo " Admin token obtained."

# Step 5: Run benchmark
echo "[5/5] Running benchmark..."
echo ""

BENCH_ARGS=(
-url "$BASE_URL"
-admin-token "$ADMIN_TOKEN"
-rooms "$ROOMS"
-users "$USERS"
-concurrent "$CONCURRENT"
-benchmarks "$BENCHMARKS"
-duration "$DURATION"
-message-count "$MESSAGE_COUNT"
-pprof-url "$PPROF_URL"
)

if [ -n "$JSON_OUTPUT" ]; then
BENCH_ARGS+=(-json-output "$JSON_OUTPUT")
fi

if [ -n "$SYNAPSE_BASELINE" ]; then
BENCH_ARGS+=(-synapse-baseline "$SYNAPSE_BASELINE")
fi

"$BENCHMARK_DIR/dendrite-benchmark" "${BENCH_ARGS[@]}" \
2>&1 | tee "$BENCHMARK_DIR/results.txt"

echo ""
echo "============================================"
echo " Benchmark complete!"
echo " Results saved to: $BENCHMARK_DIR/results.txt"
if [ -n "$JSON_OUTPUT" ]; then
echo " JSON results: $JSON_OUTPUT"
fi
echo " Report: benchmark/report.txt"
echo ""
echo " To clean up: cd $BENCHMARK_DIR && docker compose down -v"
echo "============================================"
13 changes: 13 additions & 0 deletions benchmark/scale-results.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Connected to server.
Creating 10 test users...
10 users ready.

=== Admin Join Scale Test ===
Scales: [10 100 1000 10000]
Target: join one user into all rooms progressively

Phase 1: Creating 10000 rooms (30 workers)...
Progress: 1000/10000 rooms
Progress: 2000/10000 rooms
Progress: 3000/10000 rooms
Progress: 4000/10000 rooms
1 change: 1 addition & 0 deletions clientapi/auth/authtypes/logintypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ const (
LoginTypeRecaptcha = "m.login.recaptcha"
LoginTypeApplicationService = "m.login.application_service"
LoginTypeToken = "m.login.token"
LoginTypeVoysSingleUser = "nl.voys.single_user"
)
20 changes: 20 additions & 0 deletions clientapi/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ func LoginFromJSONReader(
UserAPI: userAPI,
Config: cfg,
}
case authtypes.LoginTypeVoysSingleUser:
if cfg.VoysSSOURL == "" {
err := util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam("nl.voys.single_user login is not configured"),
}
return nil, nil, &err
}
voysAPI, ok := useraccountAPI.(VoysUserAPI)
if !ok {
err := util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
return nil, nil, &err
}
typ = &LoginTypeVoysSingleUser{
Config: cfg,
UserAPI: voysAPI,
}
case authtypes.LoginTypeApplicationService:
token, err := ExtractAccessToken(req)
if err != nil {
Expand Down
Loading