diff --git a/.gitignore b/.gitignore index 1f91a05..be39c63 100644 --- a/.gitignore +++ b/.gitignore @@ -60,10 +60,15 @@ package-lock.json # Project specific bench_results/ +benchmark_results/ perf_data/ *.log *.tmp *.temp +*.prof +*.pprof +perf.data* +*.flamegraph # OS specific Thumbs.db diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 0000000..c060e86 --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,250 @@ +# Abyssbook Benchmarking Methodology + +## Overview + +This document describes the comprehensive benchmarking methodology for the Abyssbook orderbook implementation, ensuring reproducible, statistically valid performance measurements across different environments and configurations. + +## Benchmark Architecture + +### Core Metrics + +1. **Latency Measurements** + - Average execution time (µs) + - Percentile latencies (P50, P95, P99, P99.9) + - Jitter and variance analysis + +2. **Throughput Measurements** + - Operations per second (ops/sec) + - Orders per second for different operation types + - Sustained throughput under load + +3. **Memory Performance** + - Memory footprint per operation + - Cache hit/miss ratios + - Memory bandwidth utilization + +4. **Scalability Metrics** + - Performance vs. number of shards + - Performance vs. order book depth + - Performance vs. concurrent operations + +### Benchmark Categories + +#### 1. Core Operations +- **Place Orders**: Limit order insertion at various price levels +- **Cancel Orders**: Order cancellation and price level cleanup +- **Market Orders**: Immediate execution against existing liquidity +- **Price Level Updates**: Bulk operations at same price level + +#### 2. Advanced Order Types +- **Stop Orders**: Conditional order triggering +- **Iceberg Orders**: Large order management with display amounts +- **TWAP Orders**: Time-weighted average price execution +- **Peg Orders**: Dynamic price-following orders + +#### 3. Stress Testing +- **Burst Patterns**: High-frequency order submission spikes +- **Mixed Workloads**: Realistic trading pattern simulation +- **Price Level Stress**: Many price levels with few orders each +- **Large Market Orders**: Cross multiple price levels + +#### 4. System Integration +- **Blockchain Integration**: Onchain data synchronization +- **Cache Performance**: Multi-level caching efficiency +- **Memory Management**: Allocation and cleanup patterns + +## Statistical Methodology + +### Sample Collection +```zig +// Statistical sampling approach for large iteration counts +const sample_size = @min(iterations, 10_000); +const sample_interval = @max(1, iterations / sample_size); + +// Collect samples at regular intervals to reduce memory usage +if (i % sample_interval == 0) { + try latencies.append(elapsed); +} +``` + +### Percentile Calculation +- **P50 (Median)**: 50th percentile - typical performance +- **P95**: 95th percentile - good service level target +- **P99**: 99th percentile - tail latency analysis +- **P99.9**: 99.9th percentile - extreme outlier detection + +### Environment Detection +```zig +// Adaptive configuration based on environment +fn getConfig() BenchmarkConfig { + const ci_env = std.process.getEnvVarOwned(allocator, "CI") catch null; + const github_actions = std.process.getEnvVarOwned(allocator, "GITHUB_ACTIONS") catch null; + + if (ci_env != null or github_actions != null) { + return BenchmarkConfig.forCI(); + } + return BenchmarkConfig{}; +} +``` + +## Benchmark Configuration + +### Default Configuration +```zig +const BenchmarkConfig = struct { + num_shards: usize = 32, + iterations: usize = 100_000, + order_count: usize = 10_000, + price_range: u64 = 1000, + amount_range: u64 = 100, + burst_size: usize = 1000, + num_price_levels: usize = 100, +}; +``` + +### CI-Optimized Configuration +```zig +// Reduced parameters for CI environments +const ci_config = BenchmarkConfig{ + .num_shards = 4, + .iterations = 1_000, + .order_count = 1_000, + .price_range = 100, + .amount_range = 50, + .burst_size = 100, + .num_price_levels = 20, +}; +``` + +## Running Benchmarks + +### Local Development +```bash +# Build and run benchmarks +zig build bench + +# Expected output format: +# Operation Avg (µs) P50 (µs) P95 (µs) P99 (µs) Ops/sec Total (ms) +# Place Orders 0.85 0.75 1.20 2.10 1176471 85.0 +``` + +### Continuous Integration +Benchmarks automatically detect CI environments and use optimized parameters to ensure stable execution within resource constraints. + +### Performance Regression Detection +```bash +# Run comparative benchmarks +zig build bench > current_results.txt +git checkout baseline +zig build bench > baseline_results.txt + +# Compare results (implementation needed) +./scripts/compare_benchmarks.py baseline_results.txt current_results.txt +``` + +## Validation Methodology + +### Empirical Validation vs. AI Estimates +1. **Baseline Establishment**: Run benchmarks on known configurations +2. **Cross-Validation**: Compare results across different hardware +3. **Repeatability Testing**: Multiple runs with statistical analysis +4. **Load Testing**: Validate performance under sustained load + +### Hardware Profiling +```bash +# CPU performance counters (Linux) +perf stat -e cache-misses,cache-references,instructions,cycles zig build bench + +# Memory profiling +valgrind --tool=massif --time-unit=B zig build bench + +# Cache analysis +perf record -e cache-misses zig build bench +perf report +``` + +## Optimization Tracking + +### Data Structure Performance +- **HashMap vs TreeMap**: Order storage performance comparison +- **Cache Alignment**: 64-byte alignment impact measurement +- **SIMD Utilization**: Vector operation effectiveness +- **Memory Layout**: Struct-of-arrays vs array-of-structs analysis + +### Caching Strategy Validation +- **Hit Ratios**: L1/L2/L3 cache effectiveness +- **Prefetching**: Hardware prefetch utilization +- **Working Set**: Memory footprint optimization +- **Eviction Policies**: Cache replacement strategy effectiveness + +## Expected Performance Targets + +### Latency Targets (x86_64, 3.0GHz, 32GB RAM) +- **Place Order**: P50 < 1µs, P99 < 5µs +- **Cancel Order**: P50 < 0.8µs, P99 < 4µs +- **Market Order**: P50 < 2µs, P99 < 10µs +- **Bulk Operations**: P50 < 0.5µs per order, P99 < 3µs per order + +### Throughput Targets +- **Sustained Load**: 1M+ orders/second +- **Burst Capacity**: 5M+ orders/second (short duration) +- **Mixed Workload**: 800K+ operations/second +- **Memory Efficiency**: < 1KB per active order + +### Scalability Targets +- **Linear Scaling**: Up to CPU core count +- **Memory Usage**: O(n) with active orders +- **Cache Efficiency**: 95%+ hit ratio for hot data + +## Benchmark Data Analysis + +### Statistical Significance +- Minimum 1000 samples for percentile calculations +- Confidence intervals for mean measurements +- Outlier detection and filtering (beyond 3 standard deviations) +- Warmup periods to eliminate JIT/allocation effects + +### Result Interpretation +```zig +// Example benchmark result interpretation +const BenchmarkResult = struct { + operation: []const u8, + iterations: usize, + total_time_ns: u64, + avg_time_ns: u64, + throughput: f64, + latency_p50: u64, + latency_p95: u64, + latency_p99: u64, + + pub fn isWithinTarget(self: *const BenchmarkResult, target: PerformanceTarget) bool { + return self.latency_p99 <= target.max_p99_latency_ns and + self.throughput >= target.min_throughput_ops_sec; + } +}; +``` + +## Future Enhancements + +### Planned Improvements +1. **Automated Regression Detection**: CI integration with performance alerts +2. **Hardware-Specific Tuning**: Auto-detection and optimization +3. **Load Pattern Analysis**: Real-world trading pattern simulation +4. **Memory Pool Optimization**: Custom allocation strategies +5. **Network Latency Simulation**: Distributed system performance testing + +### Research Areas +1. **Lock-Free Data Structures**: Evaluate CAS-based implementations +2. **NUMA Optimization**: Multi-socket system performance +3. **GPU Acceleration**: Parallel matching algorithm exploration +4. **Persistent Memory**: Storage-class memory integration + +## Conclusion + +This benchmarking methodology ensures that Abyssbook performance measurements are: +- **Reproducible**: Consistent results across environments +- **Statistically Valid**: Proper sampling and analysis techniques +- **Comprehensive**: Coverage of all critical performance aspects +- **Actionable**: Clear targets and optimization guidance + +Regular benchmark execution and analysis will drive continuous performance improvements while maintaining system reliability and correctness. \ No newline at end of file diff --git a/build.zig b/build.zig index 1caec23..47de543 100644 --- a/build.zig +++ b/build.zig @@ -33,6 +33,54 @@ pub fn build(b: *std.Build) void { const bench_cmd = b.addRunArtifact(bench_exe); const bench_step = b.step("bench", "Run benchmarks"); bench_step.dependOn(&bench_cmd.step); + + // Add profiler executable + const profiler_exe = b.addExecutable(.{ + .name = "profiler", + .root_source_file = .{ .cwd_relative = "src/profiler.zig" }, + .target = target, + .optimize = .ReleaseFast, + }); + + const profiler_cmd = b.addRunArtifact(profiler_exe); + const profiler_step = b.step("profile", "Run performance profiler"); + profiler_step.dependOn(&profiler_cmd.step); + + // Add load test executable + const load_test_exe = b.addExecutable(.{ + .name = "load_test", + .root_source_file = .{ .cwd_relative = "src/load_test.zig" }, + .target = target, + .optimize = .ReleaseFast, + }); + + const load_test_cmd = b.addRunArtifact(load_test_exe); + const load_test_step = b.step("load-test", "Run load testing"); + load_test_step.dependOn(&load_test_cmd.step); + + // Add regression test executable + const regression_test_exe = b.addExecutable(.{ + .name = "regression_test", + .root_source_file = .{ .cwd_relative = "src/regression_test.zig" }, + .target = target, + .optimize = .ReleaseFast, + }); + + const regression_test_cmd = b.addRunArtifact(regression_test_exe); + const regression_test_step = b.step("regression-test", "Run performance regression tests"); + regression_test_step.dependOn(®ression_test_cmd.step); + + // Add metrics reporter test executable + const metrics_reporter_exe = b.addExecutable(.{ + .name = "metrics_reporter", + .root_source_file = .{ .cwd_relative = "src/metrics_reporter.zig" }, + .target = target, + .optimize = .ReleaseFast, + }); + + const metrics_reporter_cmd = b.addRunArtifact(metrics_reporter_exe); + const metrics_reporter_step = b.step("test-metrics", "Test metrics reporting formats"); + metrics_reporter_step.dependOn(&metrics_reporter_cmd.step); // Unit tests const unit_tests = b.addTest(.{ @@ -88,4 +136,11 @@ pub fn build(b: *std.Build) void { all_tests_step.dependOn(&run_e2e_tests.step); all_tests_step.dependOn(&run_security_tests.step); all_tests_step.dependOn(&run_blockchain_security_tests.step); + + // Performance testing step + const perf_test_step = b.step("perf-test", "Run all performance tests (benchmarks, profiling, load test)"); + perf_test_step.dependOn(&bench_cmd.step); + perf_test_step.dependOn(&profiler_cmd.step); + perf_test_step.dependOn(&load_test_cmd.step); + perf_test_step.dependOn(®ression_test_cmd.step); } \ No newline at end of file diff --git a/docs/performance.md b/docs/performance.md index fca35e4..e95f19d 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -23,7 +23,64 @@ cpupower frequency-set --governor performance ## Performance Optimization +### 1. Enhanced Benchmarking + +Run comprehensive benchmarks to validate performance: + +```bash +# Quick benchmarks +zig build bench + +# Full performance test suite +./scripts/perf_test.sh all + +# Individual test types +./scripts/perf_test.sh bench # Benchmarks only +./scripts/perf_test.sh profile # Profiling analysis +./scripts/perf_test.sh load-test # Load testing +./scripts/perf_test.sh regression # Regression testing +``` + +### 2. Performance Monitoring + +Monitor key metrics during operation: + +```zig +// Initialize performance monitor +var monitor = perf_monitor.PerformanceMonitor.init(allocator); +defer monitor.deinit(); + +// Record metrics during operations +monitor.simd_metrics.recordVectorOperation(8); +monitor.batch_metrics.recordBatch(true, 128); + +// Generate performance report +try monitor.generateReport(std.io.getStdOut().writer()); +``` + +### 3. Cache Optimization + +Use the enhanced multi-level cache for better performance: + +```zig +const enhanced_cache = @import("cache/enhanced_cache.zig"); + +// Initialize multi-level cache +var cache = enhanced_cache.MultiLevelCache(u64, []const u8).init(allocator); +defer cache.deinit(); + +// Cache operations with automatic promotion/demotion +try cache.put(key, value); +if (cache.get(key)) |cached_value| { + // Use cached value +} + +// Monitor cache performance +cache.printStatistics(); +``` + ### 1. Shard Configuration + Optimal shard count depends on your system: ```zig @@ -42,9 +99,6 @@ var book = try orderbook.ShardedOrderbook.init( ); ``` -### 2. Memory Layout -Optimize data structure alignment: - ```zig // Cache-aligned order structure const CacheAlignedOrder = struct { diff --git a/scripts/ci_regression_check.sh b/scripts/ci_regression_check.sh new file mode 100755 index 0000000..9d699cb --- /dev/null +++ b/scripts/ci_regression_check.sh @@ -0,0 +1,222 @@ +#!/bin/bash + +# Performance Regression CI Script +# Compares current benchmark results with baseline + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +BASELINE_FILE="benchmark_results/baseline.json" +CURRENT_FILE="benchmark_results/current.json" +TOLERANCE_PERCENT=10 + +print_header() { + echo -e "${BLUE}===========================================${NC}" + echo -e "${BLUE} Performance Regression CI Check ${NC}" + echo -e "${BLUE}===========================================${NC}" + echo +} + +run_current_benchmarks() { + echo -e "${GREEN}Running current benchmarks...${NC}" + zig build bench > /dev/null + + # Find the latest benchmark result file + LATEST_RESULT=$(find benchmark_results -name "results_*.json" | sort | tail -1) + if [ -f "$LATEST_RESULT" ]; then + cp "$LATEST_RESULT" "$CURRENT_FILE" + echo "Using benchmark results from: $LATEST_RESULT" + else + echo -e "${RED}Error: No benchmark results found${NC}" + exit 1 + fi +} + +check_baseline() { + if [ ! -f "$BASELINE_FILE" ]; then + echo -e "${YELLOW}Warning: No baseline file found at $BASELINE_FILE${NC}" + echo "Creating baseline from current results..." + cp "$CURRENT_FILE" "$BASELINE_FILE" + echo -e "${GREEN}Baseline created successfully${NC}" + return 0 + fi + return 1 +} + +compare_results() { + echo -e "${GREEN}Comparing performance results...${NC}" + echo + + # Use Python for JSON comparison (more reliable than manual parsing) + cat << 'EOF' > /tmp/compare_benchmarks.py +import json +import sys + +def load_results(filename): + try: + with open(filename, 'r') as f: + data = json.load(f) + return data.get('results', []) + except Exception as e: + print(f"Error loading {filename}: {e}") + return [] + +def compare_operation(baseline_op, current_op, tolerance): + name = baseline_op['operation'] + baseline_p99 = baseline_op['latency_p99'] + current_p99 = current_op['latency_p99'] + baseline_throughput = baseline_op['throughput'] + current_throughput = current_op['throughput'] + + # Calculate percentage changes + latency_change = 0 + if baseline_p99 > 0: + latency_change = ((current_p99 - baseline_p99) / baseline_p99) * 100 + + throughput_change = 0 + if baseline_throughput > 0: + throughput_change = ((current_throughput - baseline_throughput) / baseline_throughput) * 100 + + # Check if within tolerance + latency_regression = latency_change > tolerance + throughput_regression = throughput_change < -tolerance + + status = "PASS" + if latency_regression or throughput_regression: + status = "FAIL" + + return { + 'name': name, + 'baseline_p99': baseline_p99, + 'current_p99': current_p99, + 'baseline_throughput': baseline_throughput, + 'current_throughput': current_throughput, + 'latency_change': latency_change, + 'throughput_change': throughput_change, + 'status': status, + 'latency_regression': latency_regression, + 'throughput_regression': throughput_regression + } + +def main(): + baseline_file = sys.argv[1] + current_file = sys.argv[2] + tolerance = float(sys.argv[3]) + + baseline_results = load_results(baseline_file) + current_results = load_results(current_file) + + if not baseline_results or not current_results: + print("Error: Could not load benchmark results") + sys.exit(1) + + # Create lookup for current results + current_lookup = {op['operation']: op for op in current_results} + + comparisons = [] + for baseline_op in baseline_results: + op_name = baseline_op['operation'] + if op_name in current_lookup: + comparison = compare_operation(baseline_op, current_lookup[op_name], tolerance) + comparisons.append(comparison) + + # Print results + print(f"{'Operation':<25} {'Status':<8} {'P99 Change':<12} {'Throughput Change':<16}") + print("-" * 70) + + failed_count = 0 + for comp in comparisons: + status_color = "🔴" if comp['status'] == 'FAIL' else "🟢" + print(f"{comp['name']:<25} {status_color + comp['status']:<8} {comp['latency_change']:+.1f}%{'':<7} {comp['throughput_change']:+.1f}%") + if comp['status'] == 'FAIL': + failed_count += 1 + print(f" P99: {comp['baseline_p99']/1000:.2f}µs -> {comp['current_p99']/1000:.2f}µs") + print(f" Throughput: {comp['baseline_throughput']:.0f} -> {comp['current_throughput']:.0f} ops/sec") + + print() + print(f"Summary: {len(comparisons) - failed_count} passed, {failed_count} failed") + + if failed_count > 0: + print("❌ Performance regression detected!") + sys.exit(1) + else: + print("✅ All performance checks passed!") + sys.exit(0) + +if __name__ == "__main__": + main() +EOF + + # Run the comparison + python3 /tmp/compare_benchmarks.py "$BASELINE_FILE" "$CURRENT_FILE" "$TOLERANCE_PERCENT" + comparison_result=$? + + # Cleanup + rm -f /tmp/compare_benchmarks.py + + return $comparison_result +} + +update_baseline() { + if [ "${CI_UPDATE_BASELINE:-false}" = "true" ]; then + echo -e "${YELLOW}Updating baseline with current results...${NC}" + cp "$CURRENT_FILE" "$BASELINE_FILE" + echo -e "${GREEN}Baseline updated${NC}" + fi +} + +cleanup() { + if [ -f "$CURRENT_FILE" ]; then + rm -f "$CURRENT_FILE" + fi +} + +# Main execution +print_header + +# Check if Python is available +if ! command -v python3 &> /dev/null; then + echo -e "${RED}Error: Python3 is required for benchmark comparison${NC}" + exit 1 +fi + +# Ensure benchmark results directory exists +mkdir -p benchmark_results + +# Run current benchmarks +run_current_benchmarks + +# Check if baseline exists, create if needed +if check_baseline; then + echo -e "${GREEN}Using existing baseline for comparison${NC}" + comparison_needed=false +else + comparison_needed=true +fi + +# Compare results if baseline exists +if [ "$comparison_needed" = true ]; then + if compare_results; then + echo -e "${GREEN}Performance regression check passed${NC}" + exit_code=0 + else + echo -e "${RED}Performance regression detected${NC}" + exit_code=1 + fi +else + echo -e "${YELLOW}No comparison performed - baseline was just created${NC}" + exit_code=0 +fi + +# Update baseline if requested +update_baseline + +# Cleanup temporary files +cleanup + +exit $exit_code \ No newline at end of file diff --git a/scripts/perf_test.sh b/scripts/perf_test.sh new file mode 100755 index 0000000..118af29 --- /dev/null +++ b/scripts/perf_test.sh @@ -0,0 +1,236 @@ +#!/bin/bash + +# Abyssbook Performance Testing Script +# This script provides easy access to all performance testing tools + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_header() { + echo -e "${BLUE}===========================================${NC}" + echo -e "${BLUE} Abyssbook Performance Testing Suite ${NC}" + echo -e "${BLUE}===========================================${NC}" + echo +} + +print_usage() { + echo "Usage: $0 [COMMAND]" + echo + echo "Commands:" + echo " bench Run benchmark suite" + echo " profile Run performance profiler" + echo " load-test Run load testing" + echo " regression Run regression tests" + echo " ci-regression Run CI regression check" + echo " metrics Test metrics reporting formats" + echo " all Run all performance tests" + echo " build Build all performance tools" + echo " clean Clean build artifacts" + echo " help Show this help message" + echo + echo "Examples:" + echo " $0 bench # Run benchmarks" + echo " $0 all # Run all performance tests" + echo " $0 build # Build performance tools" + echo " $0 ci-regression # Run CI regression check" + echo " $0 metrics # Test metrics formats" + echo +} + +check_zig() { + if ! command -v zig &> /dev/null; then + echo -e "${RED}Error: Zig compiler not found${NC}" + echo "Please install Zig from https://ziglang.org/" + exit 1 + fi +} + +run_benchmark() { + echo -e "${GREEN}Running benchmark suite...${NC}" + echo "This may take a few minutes depending on your system." + echo + # Ensure benchmark tool is available + if [ ! -f "zig-out/bin/bench" ]; then + echo "Building benchmark tool..." + zig build bench + fi + zig build bench +} + +run_profiler() { + echo -e "${GREEN}Running performance profiler...${NC}" + echo "Analyzing hotspots and performance bottlenecks..." + echo + zig build profile +} + +run_load_test() { + echo -e "${GREEN}Running load test...${NC}" + echo "Testing sustained performance under load..." + echo + zig build load-test +} + +run_regression_test() { + echo -e "${GREEN}Running regression tests...${NC}" + echo "Checking for performance regressions..." + echo + # Ensure regression test tool is available + if [ ! -f "zig-out/bin/regression_test" ]; then + echo "Building regression test tool..." + zig build regression-test + fi + zig build regression-test +} + +run_ci_regression() { + echo -e "${GREEN}Running CI regression check...${NC}" + echo "Comparing against baseline performance..." + echo + ./scripts/ci_regression_check.sh +} + +test_metrics_formats() { + echo -e "${GREEN}Testing metrics reporting formats...${NC}" + echo "Generating sample reports in different formats..." + echo + # Ensure metrics reporter is available + if [ ! -f "zig-out/bin/metrics_reporter" ]; then + echo "Building metrics reporter..." + zig build test-metrics + fi + zig build test-metrics +} + +run_all_tests() { + echo -e "${GREEN}Running all performance tests...${NC}" + echo "This will take several minutes to complete." + echo + + echo -e "${YELLOW}1/4: Running benchmarks...${NC}" + run_benchmark + echo + + echo -e "${YELLOW}2/4: Running profiler...${NC}" + run_profiler + echo + + echo -e "${YELLOW}3/4: Running load test...${NC}" + run_load_test + echo + + echo -e "${YELLOW}4/4: Running regression tests...${NC}" + run_regression_test + echo + + echo -e "${GREEN}All performance tests completed!${NC}" +} + +build_tools() { + echo -e "${GREEN}Building performance tools...${NC}" + + # Check if tools need to be built or rebuilt + local needs_build=false + local tools=("bench" "profiler" "load_test" "regression_test") + + for tool in "${tools[@]}"; do + if [ ! -f "zig-out/bin/$tool" ]; then + needs_build=true + break + fi + done + + # Also check if source files are newer than binaries + if [ ! "$needs_build" = true ]; then + for src_file in src/*.zig src/*/*.zig; do + if [ -f "$src_file" ]; then + for tool in "${tools[@]}"; do + if [ "$src_file" -nt "zig-out/bin/$tool" ]; then + needs_build=true + break 2 + fi + done + fi + done + fi + + if [ "$needs_build" = true ]; then + echo "Building tools..." + zig build bench + zig build profile + zig build load-test + zig build regression-test + echo -e "${GREEN}All tools built successfully!${NC}" + else + echo -e "${YELLOW}Tools are up to date, skipping build${NC}" + fi +} + +clean_artifacts() { + echo -e "${YELLOW}Cleaning build artifacts...${NC}" + rm -rf zig-cache zig-out benchmark_results + echo -e "${GREEN}Clean completed!${NC}" +} + +# Main script logic +case "${1:-help}" in + "bench") + print_header + check_zig + run_benchmark + ;; + "profile") + print_header + check_zig + run_profiler + ;; + "load-test") + print_header + check_zig + run_load_test + ;; + "regression") + print_header + check_zig + run_regression_test + ;; + "ci-regression") + print_header + check_zig + run_ci_regression + ;; + "metrics") + print_header + check_zig + test_metrics_formats + ;; + "all") + print_header + check_zig + run_all_tests + ;; + "build") + print_header + check_zig + build_tools + ;; + "clean") + print_header + clean_artifacts + ;; + "help"|"-h"|"--help") + print_header + print_usage + ;; + *) + echo -e "${RED}Error: Unknown command '$1'${NC}" + echo + print_usage + exit 1 + ;; +esac \ No newline at end of file diff --git a/src/bench.zig b/src/bench.zig index ce86e55..6ced2ce 100644 --- a/src/bench.zig +++ b/src/bench.zig @@ -11,6 +11,37 @@ const BenchmarkResult = struct { latency_p50: u64 = 0, latency_p95: u64 = 0, latency_p99: u64 = 0, + latency_p999: u64 = 0, + min_latency: u64 = 0, + max_latency: u64 = 0, + std_deviation: f64 = 0, + + pub fn isWithinTarget(self: *const BenchmarkResult, target: PerformanceTarget) bool { + return self.latency_p99 <= target.max_p99_latency_ns and + self.throughput >= target.min_throughput_ops_sec; + } + + pub fn printSummary(self: *const BenchmarkResult) void { + std.debug.print("=== {s} Performance Summary ===\n", .{self.operation}); + std.debug.print(" Iterations: {d}\n", .{self.iterations}); + std.debug.print(" Total Time: {d:.2} ms\n", .{@as(f64, @floatFromInt(self.total_time_ns)) / 1_000_000.0}); + std.debug.print(" Latency Stats (µs):\n"); + std.debug.print(" Min: {d:.2}\n", .{@as(f64, @floatFromInt(self.min_latency)) / 1000.0}); + std.debug.print(" Avg: {d:.2}\n", .{@as(f64, @floatFromInt(self.avg_time_ns)) / 1000.0}); + std.debug.print(" P50: {d:.2}\n", .{@as(f64, @floatFromInt(self.latency_p50)) / 1000.0}); + std.debug.print(" P95: {d:.2}\n", .{@as(f64, @floatFromInt(self.latency_p95)) / 1000.0}); + std.debug.print(" P99: {d:.2}\n", .{@as(f64, @floatFromInt(self.latency_p99)) / 1000.0}); + std.debug.print(" P99.9: {d:.2}\n", .{@as(f64, @floatFromInt(self.latency_p999)) / 1000.0}); + std.debug.print(" Max: {d:.2}\n", .{@as(f64, @floatFromInt(self.max_latency)) / 1000.0}); + std.debug.print(" StdDev: {d:.2}\n", .{self.std_deviation / 1000.0}); + std.debug.print(" Throughput: {d:.0} ops/sec\n", .{self.throughput}); + std.debug.print("\n"); + } +}; + +const PerformanceTarget = struct { + max_p99_latency_ns: u64, + min_throughput_ops_sec: f64, }; const BenchmarkConfig = struct { @@ -49,6 +80,45 @@ const BenchmarkConfig = struct { return BenchmarkConfig{}; } + + fn getPerformanceTargets(self: *const BenchmarkConfig) PerformanceTargets { + const is_ci = self.num_shards < 32; + + return PerformanceTargets{ + .place_orders = .{ + .max_p99_latency_ns = if (is_ci) 20_000 else 5_000, // 20µs CI, 5µs production + .min_throughput_ops_sec = if (is_ci) 10_000 else 200_000, + }, + .cancel_orders = .{ + .max_p99_latency_ns = if (is_ci) 15_000 else 4_000, // 15µs CI, 4µs production + .min_throughput_ops_sec = if (is_ci) 15_000 else 250_000, + }, + .market_orders = .{ + .max_p99_latency_ns = if (is_ci) 50_000 else 10_000, // 50µs CI, 10µs production + .min_throughput_ops_sec = if (is_ci) 5_000 else 100_000, + }, + .bulk_operations = .{ + .max_p99_latency_ns = if (is_ci) 10_000 else 3_000, // 10µs CI, 3µs production per order + .min_throughput_ops_sec = if (is_ci) 20_000 else 500_000, + }, + }; + } +}; + +const PerformanceTargets = struct { + place_orders: PerformanceTarget, + cancel_orders: PerformanceTarget, + market_orders: PerformanceTarget, + bulk_operations: PerformanceTarget, + + fn getTargetForOperation(self: *const PerformanceTargets, operation: []const u8) ?PerformanceTarget { + if (std.mem.eql(u8, operation, "Place Orders")) return self.place_orders; + if (std.mem.eql(u8, operation, "Cancel Orders")) return self.cancel_orders; + if (std.mem.eql(u8, operation, "Market Orders")) return self.market_orders; + if (std.mem.eql(u8, operation, "Burst Orders")) return self.bulk_operations; + if (std.mem.eql(u8, operation, "HFT Burst Pattern")) return self.bulk_operations; + return null; + } }; // Global order ID counter to ensure uniqueness across all benchmarks @@ -73,14 +143,29 @@ fn runBenchmark( try latencies.ensureTotalCapacity(sample_size); var total_time: u64 = 0; + var min_latency: u64 = std.math.maxInt(u64); + var max_latency: u64 = 0; var timer = try std.time.Timer.start(); + // Warmup phase - 10% of iterations or 100, whichever is smaller + const warmup_iterations = @min(iterations / 10, 100); + var warmup_i: usize = 0; + while (warmup_i < warmup_iterations) : (warmup_i += 1) { + timer.reset(); + try @call(.auto, func, args); + _ = timer.read(); // Discard warmup results + } + + // Actual benchmark measurement var i: usize = 0; while (i < iterations) : (i += 1) { timer.reset(); try @call(.auto, func, args); const elapsed = timer.read(); + min_latency = @min(min_latency, elapsed); + max_latency = @max(max_latency, elapsed); + // Only collect latency samples at intervals to reduce memory usage if (i % sample_interval == 0) { try latencies.append(elapsed); @@ -94,6 +179,15 @@ fn runBenchmark( const avg_time = total_time / iterations; const throughput = @as(f64, @floatFromInt(iterations)) / (@as(f64, @floatFromInt(total_time)) / 1_000_000_000.0); + // Calculate standard deviation + var variance_sum: f64 = 0; + for (latencies.items) |latency| { + const diff = @as(f64, @floatFromInt(latency)) - @as(f64, @floatFromInt(avg_time)); + variance_sum += diff * diff; + } + const variance = variance_sum / @as(f64, @floatFromInt(latencies.items.len)); + const std_deviation = @sqrt(variance); + const sample_count = latencies.items.len; return BenchmarkResult{ .operation = operation, @@ -104,6 +198,10 @@ fn runBenchmark( .latency_p50 = if (sample_count > 0) latencies.items[sample_count * 50 / 100] else 0, .latency_p95 = if (sample_count > 0) latencies.items[sample_count * 95 / 100] else 0, .latency_p99 = if (sample_count > 0) latencies.items[sample_count * 99 / 100] else 0, + .latency_p999 = if (sample_count > 0) latencies.items[@min(sample_count * 999 / 1000, sample_count - 1)] else 0, + .min_latency = min_latency, + .max_latency = max_latency, + .std_deviation = std_deviation, }; } @@ -368,6 +466,8 @@ pub fn main() !void { pub fn runBenchmarks() !void { const config = BenchmarkConfig.getConfig(); // Use CI-optimized config when detected + const targets = config.getPerformanceTargets(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); @@ -403,14 +503,28 @@ pub fn runBenchmarks() !void { // Print header with CI status const ci_detected = config.num_shards < 32; - std.debug.print("\nOrderbook Benchmark Results{s}:\n", .{if (ci_detected) " (CI Optimized)" else ""}); - std.debug.print("Configuration:\n", .{}); + std.debug.print("\nAbyssbook Orderbook Benchmark Results{s}:\n", .{if (ci_detected) " (CI Optimized)" else ""}); + std.debug.print("=" ** 60 ++ "\n"); + std.debug.print("Configuration:\n"); std.debug.print(" Shards: {d}\n", .{config.num_shards}); std.debug.print(" Iterations: {d}\n", .{config.iterations}); std.debug.print(" Order Count: {d}\n", .{config.order_count}); std.debug.print(" Burst Size: {d}\n", .{config.burst_size}); std.debug.print(" Price Levels: {d}\n", .{config.num_price_levels}); - std.debug.print("\n{s:<25} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12}\n", .{ "Operation", "Avg (µs)", "P50 (µs)", "P95 (µs)", "P99 (µs)", "Ops/sec", "Total (ms)" }); + std.debug.print(" Environment: {s}\n", .{if (ci_detected) "CI/Testing" else "Production"}); + std.debug.print("\n"); + + // Print table header + std.debug.print("{s:<25} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12} {s:>10}\n", .{ + "Operation", "Avg (µs)", "P50 (µs)", "P95 (µs)", "P99 (µs)", "Ops/sec", "Total (ms)", "Status" + }); + std.debug.print("-" ** 105 ++ "\n"); + + var results = std.ArrayList(BenchmarkResult).init(allocator); + defer results.deinit(); + + var passed_count: usize = 0; + var failed_count: usize = 0; // Run and print results with memory cleanup between benchmarks inline for (benchmarks) |bench| { @@ -422,8 +536,8 @@ pub fn runBenchmarks() !void { book.ask_levels[i].clearRetainingCapacity(); book.stop_orders[i].clearRetainingCapacity(); } - book.best_bid = null; - book.best_ask = null; + book.best_bid_cache = null; + book.best_ask_cache = null; global_order_id.store(1, .seq_cst); const result = try runBenchmark( @@ -433,7 +547,22 @@ pub fn runBenchmarks() !void { .{ &book, config }, ); - std.debug.print("{s:<25} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.2}\n", .{ + try results.append(result); + + // Check against performance targets + const target = targets.getTargetForOperation(bench.name); + const status = if (target) |t| + if (result.isWithinTarget(t)) "PASS" else "FAIL" + else + "N/A"; + + if (target != null and result.isWithinTarget(target.?)) { + passed_count += 1; + } else if (target != null) { + failed_count += 1; + } + + std.debug.print("{s:<25} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.0} {d:>12.2} {s:>10}\n", .{ result.operation, @as(f64, @floatFromInt(result.avg_time_ns)) / 1000.0, @as(f64, @floatFromInt(result.latency_p50)) / 1000.0, @@ -441,6 +570,113 @@ pub fn runBenchmarks() !void { @as(f64, @floatFromInt(result.latency_p99)) / 1000.0, result.throughput, @as(f64, @floatFromInt(result.total_time_ns)) / 1_000_000.0, + status, }); } + + // Print summary + std.debug.print("\n" ++ "=" ** 60 ++ "\n"); + std.debug.print("Benchmark Summary:\n"); + std.debug.print(" Total benchmarks: {d}\n", .{benchmarks.len}); + std.debug.print(" Passed targets: {d}\n", .{passed_count}); + std.debug.print(" Failed targets: {d}\n", .{failed_count}); + std.debug.print(" Success rate: {d:.1}%\n", .{@as(f64, @floatFromInt(passed_count)) / @as(f64, @floatFromInt(passed_count + failed_count)) * 100.0}); + + // Print detailed results for failed benchmarks + if (failed_count > 0) { + std.debug.print("\nDetailed Analysis for Failed Benchmarks:\n"); + std.debug.print("-" ** 60 ++ "\n"); + for (results.items) |result| { + if (targets.getTargetForOperation(result.operation)) |target| { + if (!result.isWithinTarget(target)) { + result.printSummary(); + std.debug.print(" Target P99: {d:.2} µs (Actual: {d:.2} µs)\n", .{ + @as(f64, @floatFromInt(target.max_p99_latency_ns)) / 1000.0, + @as(f64, @floatFromInt(result.latency_p99)) / 1000.0, + }); + std.debug.print(" Target Throughput: {d:.0} ops/sec (Actual: {d:.0} ops/sec)\n", .{ + target.min_throughput_ops_sec, + result.throughput, + }); + std.debug.print("\n"); + } + } + } + } + + // Export results for CI integration (future enhancement) + try exportBenchmarkResults(allocator, results.items, config); +} + +// Export benchmark results for CI integration and historical tracking +fn exportBenchmarkResults(allocator: std.mem.Allocator, results: []const BenchmarkResult, config: BenchmarkConfig) !void { + // Create results directory if it doesn't exist + std.fs.cwd().makeDir("benchmark_results") catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => { + std.log.err("Failed to create benchmark_results directory: {}", .{err}); + return err; + } + }; + + // Generate timestamp for filename + const timestamp = std.time.timestamp(); + const filename = try std.fmt.allocPrint(allocator, "benchmark_results/results_{d}.json", .{timestamp}); + defer allocator.free(filename); + + const file = std.fs.cwd().createFile(filename, .{}) catch |err| { + std.log.err("Failed to create benchmark results file {s}: {}", .{filename, err}); + return err; + }; + defer file.close(); + + var writer = file.writer(); + + // Write JSON with proper error handling + writeJSON(&writer, results, config, timestamp) catch |err| { + std.log.err("Failed to write JSON to {s}: {}", .{filename, err}); + return err; + }; + + std.debug.print("Benchmark results exported to: {s}\n", .{filename}); +} + +fn writeJSON(writer: anytype, results: []const BenchmarkResult, config: BenchmarkConfig, timestamp: i64) !void { + try writer.writeAll("{\n"); + try writer.print(" \"timestamp\": {d},\n", .{timestamp}); + try writer.print(" \"config\": {{\n"); + try writer.print(" \"num_shards\": {d},\n", .{config.num_shards}); + try writer.print(" \"iterations\": {d},\n", .{config.iterations}); + try writer.print(" \"order_count\": {d},\n", .{config.order_count}); + try writer.print(" \"price_range\": {d},\n", .{config.price_range}); + try writer.print(" \"amount_range\": {d},\n", .{config.amount_range}); + try writer.print(" \"burst_size\": {d},\n", .{config.burst_size}); + try writer.print(" \"num_price_levels\": {d}\n", .{config.num_price_levels}); + try writer.writeAll(" },\n"); + try writer.writeAll(" \"results\": [\n"); + + // Write benchmark results + for (results, 0..) |result, i| { + try writer.writeAll(" {\n"); + try writer.print(" \"operation\": \"{s}\",\n", .{result.operation}); + try writer.print(" \"iterations\": {d},\n", .{result.iterations}); + try writer.print(" \"total_time_ns\": {d},\n", .{result.total_time_ns}); + try writer.print(" \"avg_time_ns\": {d},\n", .{result.avg_time_ns}); + try writer.print(" \"throughput\": {d:.2},\n", .{result.throughput}); + try writer.print(" \"latency_p50\": {d},\n", .{result.latency_p50}); + try writer.print(" \"latency_p95\": {d},\n", .{result.latency_p95}); + try writer.print(" \"latency_p99\": {d},\n", .{result.latency_p99}); + try writer.print(" \"latency_p999\": {d},\n", .{result.latency_p999}); + try writer.print(" \"min_latency\": {d},\n", .{result.min_latency}); + try writer.print(" \"max_latency\": {d},\n", .{result.max_latency}); + try writer.print(" \"std_deviation\": {d:.2}\n", .{result.std_deviation}); + if (i < results.len - 1) { + try writer.writeAll(" },\n"); + } else { + try writer.writeAll(" }\n"); + } + } + + try writer.writeAll(" ]\n"); + try writer.writeAll("}\n"); } diff --git a/src/cache/enhanced_cache.zig b/src/cache/enhanced_cache.zig new file mode 100644 index 0000000..ac0e01e --- /dev/null +++ b/src/cache/enhanced_cache.zig @@ -0,0 +1,501 @@ +const std = @import("std"); + +/// Enhanced multi-level cache with LRU eviction and performance monitoring +pub fn MultiLevelCache(comptime Key: type, comptime Value: type) type { + return struct { + const Self = @This(); + + // Configuration constants + const HOT_ACCESS_THRESHOLD = 10; + const PROMOTION_ACCESS_THRESHOLD = 3; + const L1_DEFAULT_SIZE = 1000; + const L1_TTL_MS = 5_000; + const L2_DEFAULT_SIZE = 10_000; + const L2_TTL_MS = 30_000; + const L3_DEFAULT_SIZE = 100_000; + const L3_TTL_MS = 300_000; + + pub const CacheEntry = struct { + key: Key, + value: Value, + timestamp: i64, + access_count: usize, + last_access: i64, + is_hot: bool, + }; + + pub const CacheLevel = struct { + entries: std.HashMap(Key, CacheEntry, std.hash_map.DefaultContext(Key), std.hash_map.default_max_load_percentage), + max_size: usize, + ttl_ms: u64, + hit_count: usize, + miss_count: usize, + eviction_count: usize, + // LRU tracking with doubly linked list concept - simplified with timestamps + next_eviction_check: usize, + + const EVICTION_CHECK_INTERVAL = 100; + + pub fn init(allocator: std.mem.Allocator, max_size: usize, ttl_ms: u64) CacheLevel { + return .{ + .entries = std.HashMap(Key, CacheEntry, std.hash_map.DefaultContext(Key), std.hash_map.default_max_load_percentage).init(allocator), + .max_size = max_size, + .ttl_ms = ttl_ms, + .hit_count = 0, + .miss_count = 0, + .eviction_count = 0, + .next_eviction_check = 0, + }; + } + + pub fn deinit(self: *CacheLevel) void { + self.entries.deinit(); + } + + pub fn get(self: *CacheLevel, key: Key) ?Value { + const current_time = std.time.milliTimestamp(); + + if (self.entries.getPtr(key)) |entry| { + // Check if entry is expired + const age_ms = @as(u64, @intCast(current_time - entry.timestamp)); + if (age_ms <= self.ttl_ms) { + // Update access statistics + entry.last_access = current_time; + entry.access_count += 1; + entry.is_hot = entry.access_count > HOT_ACCESS_THRESHOLD; + + self.hit_count += 1; + return entry.value; + } else { + // Entry expired, remove it + _ = self.entries.remove(key); + self.eviction_count += 1; + } + } + + self.miss_count += 1; + return null; + } + + pub fn put(self: *CacheLevel, key: Key, value: Value) !void { + const current_time = std.time.milliTimestamp(); + + // More efficient eviction - only check periodically or when near capacity + if (self.entries.count() >= self.max_size) { + try self.evictLRU(); + } else if (self.entries.count() % EVICTION_CHECK_INTERVAL == 0) { + // Periodic cleanup of expired entries + try self.cleanupExpired(); + } + + const entry = CacheEntry{ + .key = key, + .value = value, + .timestamp = current_time, + .access_count = 1, + .last_access = current_time, + .is_hot = false, + }; + + try self.entries.put(key, entry); + } + + fn cleanupExpired(self: *CacheLevel) !void { + const current_time = std.time.milliTimestamp(); + var keys_to_remove = std.ArrayList(Key).init(self.entries.allocator); + defer keys_to_remove.deinit(); + + var it = self.entries.iterator(); + while (it.next()) |entry| { + const age_ms = @as(u64, @intCast(current_time - entry.value_ptr.timestamp)); + if (age_ms > self.ttl_ms) { + try keys_to_remove.append(entry.key_ptr.*); + } + } + + for (keys_to_remove.items) |key| { + _ = self.entries.remove(key); + self.eviction_count += 1; + } + } + + fn evictLRU(self: *CacheLevel) !void { + if (self.entries.count() == 0) return; + + // More efficient LRU: collect candidates in batches + const MAX_CANDIDATES = 10; + var candidates: [MAX_CANDIDATES]struct { key: Key, last_access: i64, is_hot: bool } = undefined; + var candidate_count: usize = 0; + + var it = self.entries.iterator(); + var checked: usize = 0; + const max_check = @min(self.entries.count(), 50); // Limit scan + + while (it.next()) |entry| { + if (checked >= max_check) break; + checked += 1; + + if (candidate_count < MAX_CANDIDATES) { + candidates[candidate_count] = .{ + .key = entry.key_ptr.*, + .last_access = entry.value_ptr.last_access, + .is_hot = entry.value_ptr.is_hot, + }; + candidate_count += 1; + } else { + // Replace worst candidate if this entry is older + var worst_idx: usize = 0; + var worst_time = candidates[0].last_access; + var worst_is_hot = candidates[0].is_hot; + + for (candidates[1..candidate_count], 1..) |candidate, i| { + // Prefer evicting non-hot entries, then oldest + if ((!candidate.is_hot and worst_is_hot) or + (!candidate.is_hot == !worst_is_hot and candidate.last_access < worst_time)) { + worst_idx = i; + worst_time = candidate.last_access; + worst_is_hot = candidate.is_hot; + } + } + + if ((!entry.value_ptr.is_hot and worst_is_hot) or + (!entry.value_ptr.is_hot == !worst_is_hot and entry.value_ptr.last_access < worst_time)) { + candidates[worst_idx] = .{ + .key = entry.key_ptr.*, + .last_access = entry.value_ptr.last_access, + .is_hot = entry.value_ptr.is_hot, + }; + } + } + } + + if (candidate_count > 0) { + // Find the best candidate to evict (prefer non-hot, then oldest) + var evict_idx: usize = 0; + var evict_time = candidates[0].last_access; + var evict_is_hot = candidates[0].is_hot; + + for (candidates[1..candidate_count], 1..) |candidate, i| { + if ((!candidate.is_hot and evict_is_hot) or + (!candidate.is_hot == !evict_is_hot and candidate.last_access < evict_time)) { + evict_idx = i; + evict_time = candidate.last_access; + evict_is_hot = candidate.is_hot; + } + } + + _ = self.entries.remove(candidates[evict_idx].key); + self.eviction_count += 1; + } + } + + pub fn getHitRatio(self: *const CacheLevel) f64 { + const total = self.hit_count + self.miss_count; + return if (total > 0) @as(f64, @floatFromInt(self.hit_count)) / @as(f64, @floatFromInt(total)) * 100.0 else 0.0; + } + + pub fn clear(self: *CacheLevel) void { + self.entries.clearRetainingCapacity(); + self.hit_count = 0; + self.miss_count = 0; + self.eviction_count = 0; + } + }; + + allocator: std.mem.Allocator, + l1_cache: CacheLevel, // Hot data - small, fast + l2_cache: CacheLevel, // Warm data - medium size + l3_cache: CacheLevel, // Cold data - large, slower + + // Performance monitoring + total_gets: usize, + total_puts: usize, + l1_promotions: usize, + l2_promotions: usize, + l3_demotions: usize, + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .allocator = allocator, + .l1_cache = CacheLevel.init(allocator, L1_DEFAULT_SIZE, L1_TTL_MS), + .l2_cache = CacheLevel.init(allocator, L2_DEFAULT_SIZE, L2_TTL_MS), + .l3_cache = CacheLevel.init(allocator, L3_DEFAULT_SIZE, L3_TTL_MS), + .total_gets = 0, + .total_puts = 0, + .l1_promotions = 0, + .l2_promotions = 0, + .l3_demotions = 0, + }; + } + + pub fn deinit(self: *Self) void { + self.l1_cache.deinit(); + self.l2_cache.deinit(); + self.l3_cache.deinit(); + } + + pub fn get(self: *Self, key: Key) ?Value { + self.total_gets += 1; + + // Try L1 cache first (hottest data) + if (self.l1_cache.get(key)) |value| { + return value; + } + + // Try L2 cache + if (self.l2_cache.get(key)) |value| { + // Promote to L1 if accessed frequently + if (self.l2_cache.entries.get(key)) |entry| { + if (entry.is_hot) { + self.l1_cache.put(key, value) catch |err| { + std.log.warn("Failed to promote L2->L1: {}", .{err}); + }; + self.l1_promotions += 1; + } + } + return value; + } + + // Try L3 cache + if (self.l3_cache.get(key)) |value| { + // Promote to L2 if accessed frequently + if (self.l3_cache.entries.get(key)) |entry| { + if (entry.access_count > PROMOTION_ACCESS_THRESHOLD) { + self.l2_cache.put(key, value) catch |err| { + std.log.warn("Failed to promote L3->L2: {}", .{err}); + }; + self.l2_promotions += 1; + } + } + return value; + } + + return null; + } + + pub fn put(self: *Self, key: Key, value: Value) !void { + self.total_puts += 1; + + // Always put new entries in L3 first + // They'll be promoted based on access patterns + try self.l3_cache.put(key, value); + } + + pub fn remove(self: *Self, key: Key) void { + _ = self.l1_cache.entries.remove(key); + _ = self.l2_cache.entries.remove(key); + _ = self.l3_cache.entries.remove(key); + } + + pub fn clear(self: *Self) void { + self.l1_cache.clear(); + self.l2_cache.clear(); + self.l3_cache.clear(); + self.total_gets = 0; + self.total_puts = 0; + self.l1_promotions = 0; + self.l2_promotions = 0; + self.l3_demotions = 0; + } + + pub fn getOverallHitRatio(self: *const Self) f64 { + const l1_hits = self.l1_cache.hit_count; + const l2_hits = self.l2_cache.hit_count; + const l3_hits = self.l3_cache.hit_count; + const total_hits = l1_hits + l2_hits + l3_hits; + + return if (self.total_gets > 0) @as(f64, @floatFromInt(total_hits)) / @as(f64, @floatFromInt(self.total_gets)) * 100.0 else 0.0; + } + + pub fn printStatistics(self: *const Self) void { + std.debug.print("\n=== Multi-Level Cache Statistics ===\n"); + std.debug.print("Total Gets: {d}\n", .{self.total_gets}); + std.debug.print("Total Puts: {d}\n", .{self.total_puts}); + std.debug.print("Overall Hit Ratio: {d:.1}%\n", .{self.getOverallHitRatio()}); + std.debug.print("\nL1 Cache (Hot):\n"); + std.debug.print(" Entries: {d}/{d}\n", .{ self.l1_cache.entries.count(), self.l1_cache.max_size }); + std.debug.print(" Hit Ratio: {d:.1}%\n", .{self.l1_cache.getHitRatio()}); + std.debug.print(" Hits: {d}, Misses: {d}\n", .{ self.l1_cache.hit_count, self.l1_cache.miss_count }); + std.debug.print(" Evictions: {d}\n", .{self.l1_cache.eviction_count}); + + std.debug.print("\nL2 Cache (Warm):\n"); + std.debug.print(" Entries: {d}/{d}\n", .{ self.l2_cache.entries.count(), self.l2_cache.max_size }); + std.debug.print(" Hit Ratio: {d:.1}%\n", .{self.l2_cache.getHitRatio()}); + std.debug.print(" Hits: {d}, Misses: {d}\n", .{ self.l2_cache.hit_count, self.l2_cache.miss_count }); + std.debug.print(" Evictions: {d}\n", .{self.l2_cache.eviction_count}); + + std.debug.print("\nL3 Cache (Cold):\n"); + std.debug.print(" Entries: {d}/{d}\n", .{ self.l3_cache.entries.count(), self.l3_cache.max_size }); + std.debug.print(" Hit Ratio: {d:.1}%\n", .{self.l3_cache.getHitRatio()}); + std.debug.print(" Hits: {d}, Misses: {d}\n", .{ self.l3_cache.hit_count, self.l3_cache.miss_count }); + std.debug.print(" Evictions: {d}\n", .{self.l3_cache.eviction_count}); + + std.debug.print("\nPromotion Statistics:\n"); + std.debug.print(" L1 Promotions: {d}\n", .{self.l1_promotions}); + std.debug.print(" L2 Promotions: {d}\n", .{self.l2_promotions}); + std.debug.print(" L3 Demotions: {d}\n", .{self.l3_demotions}); + std.debug.print("\n"); + } + + // Preload hot data based on historical access patterns + pub fn warmCache(self: *Self, hot_keys: []const Key, warm_keys: []const Key) !void { + // This would typically load from a persistence layer or analytics + // For now, it's a placeholder for intelligent cache warming + _ = self; + _ = hot_keys; + _ = warm_keys; + std.debug.print("Cache warming not implemented yet\n"); + } + + // Adaptive cache sizing based on hit ratios + pub fn optimizeSizes(self: *Self) void { + const l1_hit_ratio = self.l1_cache.getHitRatio(); + const l2_hit_ratio = self.l2_cache.getHitRatio(); + const l3_hit_ratio = self.l3_cache.getHitRatio(); + + // If L1 hit ratio is low, consider reducing its size + if (l1_hit_ratio < 70.0 and self.l1_cache.max_size > 500) { + self.l1_cache.max_size = @max(500, self.l1_cache.max_size - 100); + } + // If L1 hit ratio is very high, consider increasing its size + else if (l1_hit_ratio > 95.0 and self.l1_cache.max_size < 2000) { + self.l1_cache.max_size = @min(2000, self.l1_cache.max_size + 100); + } + + // Similar logic for L2 and L3 + if (l2_hit_ratio < 60.0 and self.l2_cache.max_size > 5000) { + self.l2_cache.max_size = @max(5000, self.l2_cache.max_size - 1000); + } else if (l2_hit_ratio > 90.0 and self.l2_cache.max_size < 20000) { + self.l2_cache.max_size = @min(20000, self.l2_cache.max_size + 1000); + } + + if (l3_hit_ratio < 50.0 and self.l3_cache.max_size > 50000) { + self.l3_cache.max_size = @max(50000, self.l3_cache.max_size - 10000); + } else if (l3_hit_ratio > 80.0 and self.l3_cache.max_size < 200000) { + self.l3_cache.max_size = @min(200000, self.l3_cache.max_size + 10000); + } + } + }; +} + +// Specialized orderbook cache with price level awareness +pub const OrderbookLevelCache = struct { + price_level_cache: MultiLevelCache(u64, PriceLevelCacheEntry), + best_bid_cache: ?u64, + best_ask_cache: ?u64, + last_update: i64, + + const PriceLevelCacheEntry = struct { + total_volume: u64, + order_count: usize, + orders: []const OrderCacheEntry, + }; + + const OrderCacheEntry = struct { + id: u64, + amount: u64, + timestamp: i64, + }; + + pub fn init(allocator: std.mem.Allocator) OrderbookLevelCache { + return .{ + .price_level_cache = MultiLevelCache(u64, PriceLevelCacheEntry).init(allocator), + .best_bid_cache = null, + .best_ask_cache = null, + .last_update = 0, + }; + } + + pub fn deinit(self: *OrderbookLevelCache) void { + self.price_level_cache.deinit(); + } + + pub fn updateBestPrices(self: *OrderbookLevelCache, best_bid: ?u64, best_ask: ?u64) void { + self.best_bid_cache = best_bid; + self.best_ask_cache = best_ask; + self.last_update = std.time.milliTimestamp(); + } + + pub fn getBestBid(self: *OrderbookLevelCache) ?u64 { + // Check if cache is fresh (within 100ms) + const age = std.time.milliTimestamp() - self.last_update; + if (age <= 100) { + return self.best_bid_cache; + } + return null; + } + + pub fn getBestAsk(self: *OrderbookLevelCache) ?u64 { + // Check if cache is fresh (within 100ms) + const age = std.time.milliTimestamp() - self.last_update; + if (age <= 100) { + return self.best_ask_cache; + } + return null; + } + + pub fn cachePriceLevel(self: *OrderbookLevelCache, price: u64, entry: PriceLevelCacheEntry) !void { + try self.price_level_cache.put(price, entry); + } + + pub fn getPriceLevel(self: *OrderbookLevelCache, price: u64) ?PriceLevelCacheEntry { + return self.price_level_cache.get(price); + } + + pub fn printStatistics(self: *const OrderbookLevelCache) void { + std.debug.print("\n=== Orderbook Cache Statistics ===\n"); + std.debug.print("Best Bid Cache: {?d}\n", .{self.best_bid_cache}); + std.debug.print("Best Ask Cache: {?d}\n", .{self.best_ask_cache}); + std.debug.print("Last Update: {d}ms ago\n", .{std.time.milliTimestamp() - self.last_update}); + self.price_level_cache.printStatistics(); + } +}; + +// Test function for the enhanced cache +pub fn testEnhancedCache() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var cache = MultiLevelCache(u64, []const u8).init(allocator); + defer cache.deinit(); + + std.debug.print("Testing Enhanced Multi-Level Cache...\n"); + + // Test basic operations + try cache.put(1, "value1"); + try cache.put(2, "value2"); + try cache.put(3, "value3"); + + if (cache.get(1)) |value| { + std.debug.print("Found: {s}\n", .{value}); + } + + // Simulate access patterns + var i: u64 = 0; + while (i < 10000) : (i += 1) { + // Hot data - accessed frequently + if (i % 10 < 3) { + _ = cache.get(1); + _ = cache.get(2); + } + // Warm data - accessed occasionally + else if (i % 100 < 10) { + _ = cache.get(3); + _ = cache.get(4); + } + // Cold data - accessed rarely + else { + try cache.put(i + 100, "cold_value"); + _ = cache.get(i + 100); + } + } + + cache.printStatistics(); + cache.optimizeSizes(); + std.debug.print("Cache sizes optimized based on hit ratios\n"); +} + +pub fn main() !void { + try testEnhancedCache(); +} diff --git a/src/load_test.zig b/src/load_test.zig new file mode 100644 index 0000000..d7c7fbe --- /dev/null +++ b/src/load_test.zig @@ -0,0 +1,529 @@ +const std = @import("std"); +const orderbook = @import("orderbook.zig"); + +pub const LoadTestConfig = struct { + duration_seconds: u64 = 60, + target_ops_per_second: usize = 100_000, + burst_intensity: f64 = 2.0, // Multiplier for burst periods + burst_duration_ms: u64 = 1000, + burst_interval_ms: u64 = 10000, + + // Order mix percentages (must sum to 100) + place_order_pct: u8 = 40, + cancel_order_pct: u8 = 30, + market_order_pct: u8 = 20, + query_pct: u8 = 10, + + // Price and amount ranges + price_min: u64 = 1000, + price_max: u64 = 2000, + amount_min: u64 = 1, + amount_max: u64 = 1000, + + // Threading + worker_threads: usize = 4, + + pub fn validate(self: *const LoadTestConfig) bool { + return (self.place_order_pct + self.cancel_order_pct + self.market_order_pct + self.query_pct) == 100; + } +}; + +pub const LoadTestResult = struct { + total_operations: usize, + successful_operations: usize, + failed_operations: usize, + + total_duration_ns: u64, + avg_latency_ns: u64, + min_latency_ns: u64, + max_latency_ns: u64, + + latency_p50: u64, + latency_p95: u64, + latency_p99: u64, + latency_p999: u64, + + actual_ops_per_second: f64, + target_ops_per_second: f64, + + // Operation-specific results + place_order_results: OperationStats, + cancel_order_results: OperationStats, + market_order_results: OperationStats, + query_results: OperationStats, + + // Resource utilization + peak_memory_mb: f64, + avg_cpu_percent: f64, + + pub fn printSummary(self: *const LoadTestResult) void { + std.debug.print("\n" ++ "=" ** 80 ++ "\n"); + std.debug.print("LOAD TEST RESULTS\n"); + std.debug.print("=" ** 80 ++ "\n"); + + std.debug.print("Overall Performance:\n"); + std.debug.print(" Total Operations: {d}\n", .{self.total_operations}); + std.debug.print(" Successful: {d} ({d:.1}%)\n", .{ + self.successful_operations, + @as(f64, @floatFromInt(self.successful_operations)) / @as(f64, @floatFromInt(self.total_operations)) * 100.0 + }); + std.debug.print(" Failed: {d} ({d:.1}%)\n", .{ + self.failed_operations, + @as(f64, @floatFromInt(self.failed_operations)) / @as(f64, @floatFromInt(self.total_operations)) * 100.0 + }); + std.debug.print(" Target Ops/sec: {d:.0}\n", .{self.target_ops_per_second}); + std.debug.print(" Actual Ops/sec: {d:.0}\n", .{self.actual_ops_per_second}); + std.debug.print(" Achievement: {d:.1}%\n", .{self.actual_ops_per_second / self.target_ops_per_second * 100.0}); + + std.debug.print("\nLatency Distribution:\n"); + std.debug.print(" Avg: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.avg_latency_ns)) / 1000.0}); + std.debug.print(" Min: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.min_latency_ns)) / 1000.0}); + std.debug.print(" P50: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.latency_p50)) / 1000.0}); + std.debug.print(" P95: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.latency_p95)) / 1000.0}); + std.debug.print(" P99: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.latency_p99)) / 1000.0}); + std.debug.print(" P99.9: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.latency_p999)) / 1000.0}); + std.debug.print(" Max: {d:.2} µs\n", .{@as(f64, @floatFromInt(self.max_latency_ns)) / 1000.0}); + + std.debug.print("\nOperation Breakdown:\n"); + std.debug.print(" Place Orders: {d} ops, {d:.2} µs avg\n", .{ + self.place_order_results.count, + @as(f64, @floatFromInt(self.place_order_results.avg_latency_ns)) / 1000.0, + }); + std.debug.print(" Cancel Orders: {d} ops, {d:.2} µs avg\n", .{ + self.cancel_order_results.count, + @as(f64, @floatFromInt(self.cancel_order_results.avg_latency_ns)) / 1000.0, + }); + std.debug.print(" Market Orders: {d} ops, {d:.2} µs avg\n", .{ + self.market_order_results.count, + @as(f64, @floatFromInt(self.market_order_results.avg_latency_ns)) / 1000.0, + }); + std.debug.print(" Queries: {d} ops, {d:.2} µs avg\n", .{ + self.query_results.count, + @as(f64, @floatFromInt(self.query_results.avg_latency_ns)) / 1000.0, + }); + + std.debug.print("\nResource Utilization:\n"); + std.debug.print(" Peak Memory: {d:.2} MB\n", .{self.peak_memory_mb}); + std.debug.print(" Avg CPU: {d:.1}%\n", .{self.avg_cpu_percent}); + + std.debug.print("\n" ++ "=" ** 80 ++ "\n"); + } +}; + +pub const OperationStats = struct { + count: usize = 0, + total_latency_ns: u64 = 0, + avg_latency_ns: u64 = 0, + min_latency_ns: u64 = std.math.maxInt(u64), + max_latency_ns: u64 = 0, + errors: usize = 0, + + pub fn recordLatency(self: *OperationStats, latency_ns: u64) void { + self.count += 1; + self.total_latency_ns += latency_ns; + self.min_latency_ns = @min(self.min_latency_ns, latency_ns); + self.max_latency_ns = @max(self.max_latency_ns, latency_ns); + self.avg_latency_ns = self.total_latency_ns / self.count; + } + + pub fn recordError(self: *OperationStats) void { + self.errors += 1; + } +}; + +pub const LoadTester = struct { + allocator: std.mem.Allocator, + config: LoadTestConfig, + orderbook: *orderbook.ShardedOrderbook, + + // Statistics + total_operations: std.atomic.Value(usize), + successful_operations: std.atomic.Value(usize), + failed_operations: std.atomic.Value(usize), + + // Latency tracking + latencies: std.ArrayList(u64), + latencies_mutex: std.Thread.Mutex, + + // Operation-specific stats + place_order_stats: OperationStats, + cancel_order_stats: OperationStats, + market_order_stats: OperationStats, + query_stats: OperationStats, + stats_mutex: std.Thread.Mutex, + + // Test control + start_time: i128, + should_stop: std.atomic.Value(bool), + + // Order tracking for cancellations + active_orders: std.ArrayList(u64), + active_orders_mutex: std.Thread.Mutex, + next_order_id: std.atomic.Value(u64), + + pub fn init(allocator: std.mem.Allocator, config: LoadTestConfig, book: *orderbook.ShardedOrderbook) !LoadTester { + if (!config.validate()) { + return error.InvalidConfig; + } + + return LoadTester{ + .allocator = allocator, + .config = config, + .orderbook = book, + .total_operations = std.atomic.Value(usize).init(0), + .successful_operations = std.atomic.Value(usize).init(0), + .failed_operations = std.atomic.Value(usize).init(0), + .latencies = std.ArrayList(u64).init(allocator), + .latencies_mutex = std.Thread.Mutex{}, + .place_order_stats = OperationStats{}, + .cancel_order_stats = OperationStats{}, + .market_order_stats = OperationStats{}, + .query_stats = OperationStats{}, + .stats_mutex = std.Thread.Mutex{}, + .start_time = 0, + .should_stop = std.atomic.Value(bool).init(false), + .active_orders = std.ArrayList(u64).init(allocator), + .active_orders_mutex = std.Thread.Mutex{}, + .next_order_id = std.atomic.Value(u64).init(1), + }; + } + + pub fn deinit(self: *LoadTester) void { + self.latencies.deinit(); + self.active_orders.deinit(); + } + + pub fn run(self: *LoadTester) !LoadTestResult { + std.debug.print("Starting load test...\n"); + std.debug.print("Duration: {d}s\n", .{self.config.duration_seconds}); + std.debug.print("Target: {d} ops/sec\n", .{self.config.target_ops_per_second}); + std.debug.print("Workers: {d} threads\n", .{self.config.worker_threads}); + std.debug.print("Operation mix: {}% place, {}% cancel, {}% market, {}% query\n", .{ + self.config.place_order_pct, + self.config.cancel_order_pct, + self.config.market_order_pct, + self.config.query_pct, + }); + + self.start_time = std.time.nanoTimestamp(); + + // Start worker threads + var threads = try self.allocator.alloc(std.Thread, self.config.worker_threads); + defer self.allocator.free(threads); + + for (threads, 0..) |*thread, i| { + thread.* = try std.Thread.spawn(.{}, workerThread, .{ self, i }); + } + + // Start monitoring thread + const monitor_thread = try std.Thread.spawn(.{}, monitoringThread, .{self}); + + // Wait for test duration + std.time.sleep(self.config.duration_seconds * std.time.ns_per_s); + + // Signal stop + self.should_stop.store(true, .seq_cst); + + // Wait for threads to finish + for (threads) |thread| { + thread.join(); + } + monitor_thread.join(); + + return self.generateResults(); + } + + fn workerThread(self: *LoadTester, worker_id: usize) void { + var prng = std.rand.DefaultPrng.init(@as(u64, @intCast(worker_id)) + @as(u64, @intCast(std.time.timestamp()))); + const rng = prng.random(); + + const ops_per_worker = self.config.target_ops_per_second / self.config.worker_threads; + const ns_per_op = std.time.ns_per_s / ops_per_worker; + + var last_op_time = std.time.nanoTimestamp(); + + while (!self.should_stop.load(.seq_cst)) { + const current_time = std.time.nanoTimestamp(); + const elapsed_since_start = current_time - self.start_time; + + // Check if we're in a burst period + const burst_cycle_ns = (self.config.burst_interval_ms + self.config.burst_duration_ms) * std.time.ns_per_ms; + const cycle_position = @mod(@as(u64, @intCast(elapsed_since_start)), burst_cycle_ns); + const is_burst = cycle_position < self.config.burst_duration_ms * std.time.ns_per_ms; + + const target_ns_per_op = if (is_burst) + @as(u64, @intFromFloat(@as(f64, @floatFromInt(ns_per_op)) / self.config.burst_intensity)) + else + ns_per_op; + + // Rate limiting + const time_since_last_op = current_time - last_op_time; + if (time_since_last_op < target_ns_per_op) { + const sleep_time = target_ns_per_op - time_since_last_op; + std.time.sleep(sleep_time); + } + + // Execute operation + self.executeRandomOperation(rng); + last_op_time = std.time.nanoTimestamp(); + } + } + + fn executeRandomOperation(self: *LoadTester, rng: std.rand.Random) void { + const op_choice = rng.uintAtMost(u8, 99); + var timer = std.time.Timer.start() catch return; + + const result = if (op_choice < self.config.place_order_pct) blk: { + const success = self.executePlaceOrder(rng); + const latency = timer.read(); + + self.stats_mutex.lock(); + defer self.stats_mutex.unlock(); + + if (success) { + self.place_order_stats.recordLatency(latency); + } else { + self.place_order_stats.recordError(); + } + + break :blk success; + } else if (op_choice < self.config.place_order_pct + self.config.cancel_order_pct) blk: { + const success = self.executeCancelOrder(rng); + const latency = timer.read(); + + self.stats_mutex.lock(); + defer self.stats_mutex.unlock(); + + if (success) { + self.cancel_order_stats.recordLatency(latency); + } else { + self.cancel_order_stats.recordError(); + } + + break :blk success; + } else if (op_choice < self.config.place_order_pct + self.config.cancel_order_pct + self.config.market_order_pct) blk: { + const success = self.executeMarketOrder(rng); + const latency = timer.read(); + + self.stats_mutex.lock(); + defer self.stats_mutex.unlock(); + + if (success) { + self.market_order_stats.recordLatency(latency); + } else { + self.market_order_stats.recordError(); + } + + break :blk success; + } else blk: { + const success = self.executeQuery(rng); + const latency = timer.read(); + + self.stats_mutex.lock(); + defer self.stats_mutex.unlock(); + + if (success) { + self.query_stats.recordLatency(latency); + } else { + self.query_stats.recordError(); + } + + break :blk success; + }; + + const latency = timer.read(); + + // Record overall statistics + _ = self.total_operations.fetchAdd(1, .seq_cst); + if (result) { + _ = self.successful_operations.fetchAdd(1, .seq_cst); + } else { + _ = self.failed_operations.fetchAdd(1, .seq_cst); + } + + // Sample latency (to avoid memory growth) + if (self.total_operations.load(.seq_cst) % 100 == 0) { + self.latencies_mutex.lock(); + defer self.latencies_mutex.unlock(); + + if (self.latencies.items.len < 100_000) { + self.latencies.append(latency) catch {}; + } + } + } + + fn executePlaceOrder(self: *LoadTester, rng: std.rand.Random) bool { + const price = rng.intRangeAtMost(u64, self.config.price_min, self.config.price_max); + const amount = rng.intRangeAtMost(u64, self.config.amount_min, self.config.amount_max); + const side: orderbook.OrderSide = if (rng.boolean()) .Buy else .Sell; + const id = self.next_order_id.fetchAdd(1, .seq_cst); + + self.orderbook.placeOrder(side, price, amount, id) catch return false; + + // Track order for potential cancellation + self.active_orders_mutex.lock(); + defer self.active_orders_mutex.unlock(); + self.active_orders.append(id) catch {}; + + return true; + } + + fn executeCancelOrder(self: *LoadTester, rng: std.rand.Random) bool { + self.active_orders_mutex.lock(); + defer self.active_orders_mutex.unlock(); + + if (self.active_orders.items.len == 0) return false; + + const index = rng.uintAtMost(usize, self.active_orders.items.len - 1); + const id = self.active_orders.swapRemove(index); + + self.orderbook.cancelOrder(id) catch return false; + return true; + } + + fn executeMarketOrder(self: *LoadTester, rng: std.rand.Random) bool { + const amount = rng.intRangeAtMost(u64, self.config.amount_min, self.config.amount_max / 10); + const side: orderbook.OrderSide = if (rng.boolean()) .Buy else .Sell; + + _ = self.orderbook.executeMarketOrder(side, amount) catch return false; + return true; + } + + fn executeQuery(self: *LoadTester, rng: std.rand.Random) bool { + _ = rng; + // Execute different types of queries + _ = self.orderbook.getBestBid(); + _ = self.orderbook.getBestAsk(); + return true; + } + + fn monitoringThread(self: *LoadTester) void { + const print_interval = 5 * std.time.ns_per_s; // Print stats every 5 seconds + var last_print = self.start_time; + var last_ops = self.total_operations.load(.seq_cst); + + while (!self.should_stop.load(.seq_cst)) { + std.time.sleep(std.time.ns_per_s); // Check every second + + const current_time = std.time.nanoTimestamp(); + if (current_time - last_print >= print_interval) { + const current_ops = self.total_operations.load(.seq_cst); + const ops_in_period = current_ops - last_ops; + const time_period_s = @as(f64, @floatFromInt(current_time - last_print)) / std.time.ns_per_s; + const current_rate = @as(f64, @floatFromInt(ops_in_period)) / time_period_s; + + const elapsed_s = @as(f64, @floatFromInt(current_time - self.start_time)) / std.time.ns_per_s; + + std.debug.print("[{d:.0}s] Ops: {d}, Rate: {d:.0}/s, Target: {d}/s\n", .{ + elapsed_s, + current_ops, + current_rate, + self.config.target_ops_per_second, + }); + + last_print = current_time; + last_ops = current_ops; + } + } + } + + fn generateResults(self: *LoadTester) LoadTestResult { + const end_time = std.time.nanoTimestamp(); + const total_duration_ns = @as(u64, @intCast(end_time - self.start_time)); + + const total_ops = self.total_operations.load(.seq_cst); + const successful_ops = self.successful_operations.load(.seq_cst); + const failed_ops = self.failed_operations.load(.seq_cst); + + const actual_ops_per_second = @as(f64, @floatFromInt(total_ops)) / (@as(f64, @floatFromInt(total_duration_ns)) / std.time.ns_per_s); + + // Calculate latency percentiles + self.latencies_mutex.lock(); + defer self.latencies_mutex.unlock(); + + std.sort.heap(u64, self.latencies.items, {}, std.sort.asc(u64)); + + const len = self.latencies.items.len; + const latency_p50 = if (len > 0) self.latencies.items[len * 50 / 100] else 0; + const latency_p95 = if (len > 0) self.latencies.items[len * 95 / 100] else 0; + const latency_p99 = if (len > 0) self.latencies.items[len * 99 / 100] else 0; + const latency_p999 = if (len > 0) self.latencies.items[@min(len * 999 / 1000, len - 1)] else 0; + + var total_latency: u64 = 0; + var min_latency: u64 = std.math.maxInt(u64); + var max_latency: u64 = 0; + + for (self.latencies.items) |latency| { + total_latency += latency; + min_latency = @min(min_latency, latency); + max_latency = @max(max_latency, latency); + } + + const avg_latency = if (len > 0) total_latency / len else 0; + + return LoadTestResult{ + .total_operations = total_ops, + .successful_operations = successful_ops, + .failed_operations = failed_ops, + .total_duration_ns = total_duration_ns, + .avg_latency_ns = avg_latency, + .min_latency_ns = if (min_latency == std.math.maxInt(u64)) 0 else min_latency, + .max_latency_ns = max_latency, + .latency_p50 = latency_p50, + .latency_p95 = latency_p95, + .latency_p99 = latency_p99, + .latency_p999 = latency_p999, + .actual_ops_per_second = actual_ops_per_second, + .target_ops_per_second = @as(f64, @floatFromInt(self.config.target_ops_per_second)), + .place_order_results = self.place_order_stats, + .cancel_order_results = self.cancel_order_stats, + .market_order_results = self.market_order_stats, + .query_results = self.query_stats, + .peak_memory_mb = 0.0, // TODO: Implement memory monitoring + .avg_cpu_percent = 0.0, // TODO: Implement CPU monitoring + }; + } +}; + +// Run comprehensive load test +pub fn runLoadTest() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Initialize orderbook + var book = try orderbook.ShardedOrderbook.init(allocator, 16); + defer book.deinit(); + + // Configure load test + const config = LoadTestConfig{ + .duration_seconds = 30, + .target_ops_per_second = 50_000, + .worker_threads = 4, + .burst_intensity = 3.0, + .burst_duration_ms = 2000, + .burst_interval_ms = 10000, + }; + + var load_tester = try LoadTester.init(allocator, config, &book); + defer load_tester.deinit(); + + const result = try load_tester.run(); + result.printSummary(); + + // Validate performance targets + const success_rate = @as(f64, @floatFromInt(result.successful_operations)) / @as(f64, @floatFromInt(result.total_operations)) * 100.0; + const throughput_achievement = result.actual_ops_per_second / result.target_ops_per_second * 100.0; + + std.debug.print("\nPerformance Assessment:\n"); + std.debug.print(" Success Rate: {d:.1}% (Target: >99%)\n", .{success_rate}); + std.debug.print(" Throughput Achievement: {d:.1}% (Target: >90%)\n", .{throughput_achievement}); + std.debug.print(" P99 Latency: {d:.2} µs (Target: <10µs)\n", .{@as(f64, @floatFromInt(result.latency_p99)) / 1000.0}); + + const overall_pass = success_rate > 99.0 and throughput_achievement > 90.0 and result.latency_p99 < 10_000; + std.debug.print(" Overall Result: {s}\n", .{if (overall_pass) "PASS" else "FAIL"}); +} + +pub fn main() !void { + try runLoadTest(); +} diff --git a/src/metrics_reporter.zig b/src/metrics_reporter.zig new file mode 100644 index 0000000..a24f253 --- /dev/null +++ b/src/metrics_reporter.zig @@ -0,0 +1,276 @@ +const std = @import("std"); + +/// Flexible metrics reporting interface supporting multiple output formats +pub const MetricsReporter = struct { + allocator: std.mem.Allocator, + + pub const ReportFormat = enum { + console, + json, + csv, + prometheus, + }; + + pub const MetricEntry = struct { + operation: []const u8, + timestamp: i64, + iterations: usize, + avg_time_ns: u64, + latency_p50: u64, + latency_p95: u64, + latency_p99: u64, + latency_p999: u64, + min_latency: u64, + max_latency: u64, + std_deviation: f64, + throughput: f64, + metadata: std.StringHashMap([]const u8), + + pub fn init(allocator: std.mem.Allocator) MetricEntry { + return .{ + .operation = "", + .timestamp = 0, + .iterations = 0, + .avg_time_ns = 0, + .latency_p50 = 0, + .latency_p95 = 0, + .latency_p99 = 0, + .latency_p999 = 0, + .min_latency = 0, + .max_latency = 0, + .std_deviation = 0, + .throughput = 0, + .metadata = std.StringHashMap([]const u8).init(allocator), + }; + } + + pub fn deinit(self: *MetricEntry) void { + self.metadata.deinit(); + } + }; + + pub fn init(allocator: std.mem.Allocator) MetricsReporter { + return .{ + .allocator = allocator, + }; + } + + pub fn report(self: *MetricsReporter, entries: []const MetricEntry, format: ReportFormat, writer: anytype) !void { + switch (format) { + .console => try self.reportConsole(entries, writer), + .json => try self.reportJSON(entries, writer), + .csv => try self.reportCSV(entries, writer), + .prometheus => try self.reportPrometheus(entries, writer), + } + } + + fn reportConsole(self: *MetricsReporter, entries: []const MetricEntry, writer: anytype) !void { + _ = self; + + try writer.print("{s:<25} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12} {s:>12}\n", .{ + "Operation", "Avg (µs)", "P50 (µs)", "P95 (µs)", "P99 (µs)", "Ops/sec", "StdDev (µs)" + }); + try writer.writeAll("-" ** 105 ++ "\n"); + + for (entries) |entry| { + try writer.print("{s:<25} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.2} {d:>12.0} {d:>12.2}\n", .{ + entry.operation, + @as(f64, @floatFromInt(entry.avg_time_ns)) / 1000.0, + @as(f64, @floatFromInt(entry.latency_p50)) / 1000.0, + @as(f64, @floatFromInt(entry.latency_p95)) / 1000.0, + @as(f64, @floatFromInt(entry.latency_p99)) / 1000.0, + entry.throughput, + entry.std_deviation / 1000.0, + }); + } + } + + fn reportJSON(self: *MetricsReporter, entries: []const MetricEntry, writer: anytype) !void { + _ = self; + + try writer.writeAll("{\n"); + try writer.print(" \"timestamp\": {d},\n", .{std.time.timestamp()}); + try writer.writeAll(" \"format_version\": \"1.0\",\n"); + try writer.writeAll(" \"metrics\": [\n"); + + for (entries, 0..) |entry, i| { + try writer.writeAll(" {\n"); + try writer.print(" \"operation\": \"{s}\",\n", .{entry.operation}); + try writer.print(" \"timestamp\": {d},\n", .{entry.timestamp}); + try writer.print(" \"iterations\": {d},\n", .{entry.iterations}); + try writer.print(" \"avg_time_ns\": {d},\n", .{entry.avg_time_ns}); + try writer.print(" \"latency_p50\": {d},\n", .{entry.latency_p50}); + try writer.print(" \"latency_p95\": {d},\n", .{entry.latency_p95}); + try writer.print(" \"latency_p99\": {d},\n", .{entry.latency_p99}); + try writer.print(" \"latency_p999\": {d},\n", .{entry.latency_p999}); + try writer.print(" \"min_latency\": {d},\n", .{entry.min_latency}); + try writer.print(" \"max_latency\": {d},\n", .{entry.max_latency}); + try writer.print(" \"std_deviation\": {d:.2},\n", .{entry.std_deviation}); + try writer.print(" \"throughput\": {d:.2}\n", .{entry.throughput}); + + if (i < entries.len - 1) { + try writer.writeAll(" },\n"); + } else { + try writer.writeAll(" }\n"); + } + } + + try writer.writeAll(" ]\n"); + try writer.writeAll("}\n"); + } + + fn reportCSV(self: *MetricsReporter, entries: []const MetricEntry, writer: anytype) !void { + _ = self; + + // CSV header + try writer.writeAll("operation,timestamp,iterations,avg_time_ns,latency_p50,latency_p95,latency_p99,latency_p999,min_latency,max_latency,std_deviation,throughput\n"); + + // CSV data + for (entries) |entry| { + try writer.print("\"{s}\",{d},{d},{d},{d},{d},{d},{d},{d},{d},{d:.2},{d:.2}\n", .{ + entry.operation, + entry.timestamp, + entry.iterations, + entry.avg_time_ns, + entry.latency_p50, + entry.latency_p95, + entry.latency_p99, + entry.latency_p999, + entry.min_latency, + entry.max_latency, + entry.std_deviation, + entry.throughput, + }); + } + } + + fn reportPrometheus(self: *MetricsReporter, entries: []const MetricEntry, writer: anytype) !void { + _ = self; + + try writer.writeAll("# HELP orderbook_operation_duration_nanoseconds Latency of orderbook operations in nanoseconds\n"); + try writer.writeAll("# TYPE orderbook_operation_duration_nanoseconds histogram\n"); + + try writer.writeAll("# HELP orderbook_operation_throughput_ops_per_second Throughput of orderbook operations per second\n"); + try writer.writeAll("# TYPE orderbook_operation_throughput_ops_per_second gauge\n"); + + for (entries) |entry| { + const timestamp_ms = entry.timestamp * 1000; + const operation_label = entry.operation; + + // Latency histogram buckets + try writer.print("orderbook_operation_duration_nanoseconds{{operation=\"{s}\",quantile=\"0.50\"}} {d} {d}\n", + .{ operation_label, entry.latency_p50, timestamp_ms }); + try writer.print("orderbook_operation_duration_nanoseconds{{operation=\"{s}\",quantile=\"0.95\"}} {d} {d}\n", + .{ operation_label, entry.latency_p95, timestamp_ms }); + try writer.print("orderbook_operation_duration_nanoseconds{{operation=\"{s}\",quantile=\"0.99\"}} {d} {d}\n", + .{ operation_label, entry.latency_p99, timestamp_ms }); + try writer.print("orderbook_operation_duration_nanoseconds{{operation=\"{s}\",quantile=\"0.999\"}} {d} {d}\n", + .{ operation_label, entry.latency_p999, timestamp_ms }); + + // Throughput gauge + try writer.print("orderbook_operation_throughput_ops_per_second{{operation=\"{s}\"}} {d:.2} {d}\n", + .{ operation_label, entry.throughput, timestamp_ms }); + + // Additional metrics + try writer.print("orderbook_operation_iterations_total{{operation=\"{s}\"}} {d} {d}\n", + .{ operation_label, entry.iterations, timestamp_ms }); + try writer.print("orderbook_operation_stddev_nanoseconds{{operation=\"{s}\"}} {d:.2} {d}\n", + .{ operation_label, entry.std_deviation, timestamp_ms }); + } + } + + /// Export metrics to file in specified format + pub fn exportToFile(self: *MetricsReporter, entries: []const MetricEntry, format: ReportFormat, filename: []const u8) !void { + const file = try std.fs.cwd().createFile(filename, .{}); + defer file.close(); + + var writer = file.writer(); + try self.report(entries, format, &writer); + + std.log.info("Metrics exported to {s} in {} format", .{ filename, format }); + } +}; + +/// Convert BenchmarkResult to MetricEntry for reporting +pub fn benchmarkResultToMetricEntry(allocator: std.mem.Allocator, result: anytype) !MetricsReporter.MetricEntry { + var entry = MetricsReporter.MetricEntry.init(allocator); + + entry.operation = try allocator.dupe(u8, result.operation); + entry.timestamp = std.time.timestamp(); + entry.iterations = result.iterations; + entry.avg_time_ns = result.avg_time_ns; + entry.latency_p50 = result.latency_p50; + entry.latency_p95 = result.latency_p95; + entry.latency_p99 = result.latency_p99; + entry.latency_p999 = if (@hasField(@TypeOf(result), "latency_p999")) result.latency_p999 else 0; + entry.min_latency = if (@hasField(@TypeOf(result), "min_latency")) result.min_latency else 0; + entry.max_latency = if (@hasField(@TypeOf(result), "max_latency")) result.max_latency else 0; + entry.std_deviation = if (@hasField(@TypeOf(result), "std_deviation")) result.std_deviation else 0; + entry.throughput = result.throughput; + + return entry; +} + +// Test the metrics reporter +pub fn testMetricsReporter() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var reporter = MetricsReporter.init(allocator); + + // Create sample metrics + var entries = [_]MetricsReporter.MetricEntry{ + .{ + .operation = "Place Orders", + .timestamp = std.time.timestamp(), + .iterations = 10000, + .avg_time_ns = 1500, + .latency_p50 = 1200, + .latency_p95 = 2500, + .latency_p99 = 4000, + .latency_p999 = 8000, + .min_latency = 500, + .max_latency = 15000, + .std_deviation = 800, + .throughput = 666666.7, + .metadata = std.StringHashMap([]const u8).init(allocator), + }, + .{ + .operation = "Cancel Orders", + .timestamp = std.time.timestamp(), + .iterations = 8000, + .avg_time_ns = 1100, + .latency_p50 = 900, + .latency_p95 = 2000, + .latency_p99 = 3200, + .latency_p999 = 6000, + .min_latency = 400, + .max_latency = 12000, + .std_deviation = 600, + .throughput = 909090.9, + .metadata = std.StringHashMap([]const u8).init(allocator), + }, + }; + defer for (&entries) |*entry| entry.deinit(); + + std.debug.print("\n=== Console Format ===\n"); + try reporter.report(&entries, .console, std.debug.print); + + std.debug.print("\n=== CSV Format ===\n"); + try reporter.report(&entries, .csv, std.debug.print); + + std.debug.print("\n=== JSON Format ===\n"); + try reporter.report(&entries, .json, std.debug.print); + + std.debug.print("\n=== Prometheus Format ===\n"); + try reporter.report(&entries, .prometheus, std.debug.print); + + // Test file export + try reporter.exportToFile(&entries, .json, "test_metrics.json"); + try reporter.exportToFile(&entries, .csv, "test_metrics.csv"); +} + +pub fn main() !void { + try testMetricsReporter(); +} \ No newline at end of file diff --git a/src/orderbook/optimized_storage.zig b/src/orderbook/optimized_storage.zig new file mode 100644 index 0000000..c1c6f7c --- /dev/null +++ b/src/orderbook/optimized_storage.zig @@ -0,0 +1,477 @@ +const std = @import("std"); + +/// Cache-optimized order storage with structure-of-arrays layout +pub const OptimizedOrderStorage = struct { + allocator: std.mem.Allocator, + capacity: usize, + count: usize, + + // Structure of Arrays for better cache locality + prices: []u64, // Aligned for SIMD operations + amounts: []u64, // Aligned for SIMD operations + ids: []u64, // Order IDs + sides: []u8, // Buy=0, Sell=1 (packed efficiently) + flags: []u32, // Order flags packed into single u32 + timestamps: []i64, // For time-based operations + + // Index structures for fast lookup + id_to_index: std.HashMap(u64, u32, std.hash_map.DefaultContext(u64), std.hash_map.default_max_load_percentage), + price_indices: std.AutoArrayHashMap(u64, std.ArrayList(u32)), // Price -> list of indices + + const SIMD_WIDTH = switch (@import("builtin").cpu.arch) { + .x86_64 => if (std.Target.x86.featureSetHas(@import("builtin").cpu.features, .avx2)) @as(usize, 8) else @as(usize, 4), + .aarch64 => @as(usize, 4), // NEON 128-bit vectors + else => @as(usize, 2), // Conservative fallback + }; + const CACHE_LINE_SIZE = 64; + const PREFETCH_DISTANCE = 2; + + pub fn init(allocator: std.mem.Allocator, initial_capacity: usize) !OptimizedOrderStorage { + // Align capacity to SIMD width for better vectorization + const aligned_capacity = ((initial_capacity + SIMD_WIDTH - 1) / SIMD_WIDTH) * SIMD_WIDTH; + + const prices = try allocator.alignedAlloc(u64, CACHE_LINE_SIZE, aligned_capacity); + const amounts = try allocator.alignedAlloc(u64, CACHE_LINE_SIZE, aligned_capacity); + const ids = try allocator.alignedAlloc(u64, CACHE_LINE_SIZE, aligned_capacity); + const sides = try allocator.alignedAlloc(u8, CACHE_LINE_SIZE, aligned_capacity); + const flags = try allocator.alignedAlloc(u32, CACHE_LINE_SIZE, aligned_capacity); + const timestamps = try allocator.alignedAlloc(i64, CACHE_LINE_SIZE, aligned_capacity); + + return OptimizedOrderStorage{ + .allocator = allocator, + .capacity = aligned_capacity, + .count = 0, + .prices = prices, + .amounts = amounts, + .ids = ids, + .sides = sides, + .flags = flags, + .timestamps = timestamps, + .id_to_index = std.HashMap(u64, u32, std.hash_map.DefaultContext(u64), std.hash_map.default_max_load_percentage).init(allocator), + .price_indices = std.AutoArrayHashMap(u64, std.ArrayList(u32)).init(allocator), + }; + } + + pub fn deinit(self: *OptimizedOrderStorage) void { + self.allocator.free(self.prices); + self.allocator.free(self.amounts); + self.allocator.free(self.ids); + self.allocator.free(self.sides); + self.allocator.free(self.flags); + self.allocator.free(self.timestamps); + self.id_to_index.deinit(); + + var price_it = self.price_indices.iterator(); + while (price_it.next()) |entry| { + entry.value_ptr.deinit(); + } + self.price_indices.deinit(); + } + + pub fn addOrder(self: *OptimizedOrderStorage, price: u64, amount: u64, id: u64, side: u8, order_flags: u32) !u32 { + if (self.count >= self.capacity) { + try self.resize(self.capacity * 2); + } + + const index = @as(u32, @intCast(self.count)); + + self.prices[self.count] = price; + self.amounts[self.count] = amount; + self.ids[self.count] = id; + self.sides[self.count] = side; + self.flags[self.count] = order_flags; + self.timestamps[self.count] = std.time.nanoTimestamp(); + + // Update indices + try self.id_to_index.put(id, index); + + // Update price index + if (self.price_indices.getPtr(price)) |indices| { + try indices.append(index); + } else { + var new_indices = std.ArrayList(u32).init(self.allocator); + try new_indices.append(index); + try self.price_indices.put(price, new_indices); + } + + self.count += 1; + return index; + } + + pub fn removeOrder(self: *OptimizedOrderStorage, id: u64) bool { + if (self.id_to_index.get(id)) |index| { + return self.removeByIndex(index); + } + return false; + } + + fn removeByIndex(self: *OptimizedOrderStorage, index: u32) bool { + if (index >= self.count) return false; + + const price = self.prices[index]; + const id = self.ids[index]; + + // Remove from price index + if (self.price_indices.getPtr(price)) |indices| { + for (indices.items, 0..) |idx, i| { + if (idx == index) { + _ = indices.swapRemove(i); + break; + } + } + + // If no more orders at this price, remove the price entry + if (indices.items.len == 0) { + indices.deinit(); + _ = self.price_indices.swapRemove(price); + } + } + + // Remove from ID index + _ = self.id_to_index.remove(id); + + // Swap with last element to maintain density + if (index < self.count - 1) { + const last_index = self.count - 1; + const last_id = self.ids[last_index]; + const last_price = self.prices[last_index]; + + // Copy last element to removed position + self.prices[index] = self.prices[last_index]; + self.amounts[index] = self.amounts[last_index]; + self.ids[index] = self.ids[last_index]; + self.sides[index] = self.sides[last_index]; + self.flags[index] = self.flags[last_index]; + self.timestamps[index] = self.timestamps[last_index]; + + // Update indices + try self.id_to_index.put(last_id, index) catch return false; + + if (self.price_indices.getPtr(last_price)) |indices| { + for (indices.items) |*idx| { + if (idx.* == last_index) { + idx.* = index; + break; + } + } + } + } + + self.count -= 1; + return true; + } + + // SIMD-optimized price filtering + pub fn getOrdersAtPrice(self: *const OptimizedOrderStorage, price: u64, output: []u32) usize { + if (self.price_indices.get(price)) |indices| { + const copy_count = @min(indices.items.len, output.len); + @memcpy(output[0..copy_count], indices.items[0..copy_count]); + return copy_count; + } + return 0; + } + + // SIMD-optimized range queries + pub fn getOrdersInPriceRange(self: *const OptimizedOrderStorage, min_price: u64, max_price: u64, output: []u32) usize { + var result_count: usize = 0; + + // Only use SIMD if we have enough width, otherwise fall back to scalar + if (SIMD_WIDTH >= 4) { + const PriceVector = @Vector(SIMD_WIDTH, u64); + const min_vec: PriceVector = @splat(min_price); + const max_vec: PriceVector = @splat(max_price); + + var i: usize = 0; + // Process in SIMD chunks + while (i + SIMD_WIDTH <= self.count and result_count < output.len) { + // Portable prefetch + if (i + PREFETCH_DISTANCE * SIMD_WIDTH < self.count) { + const prefetch_addr = @intFromPtr(&self.prices[i + PREFETCH_DISTANCE * SIMD_WIDTH]); + switch (@import("builtin").cpu.arch) { + .x86_64 => { + asm volatile ("prefetcht0 (%[addr])" + : // no outputs + : [addr] "r" (prefetch_addr), + ); + }, + .aarch64 => { + asm volatile ("prfm pldl1keep, [%[addr]]" + : // no outputs + : [addr] "r" (prefetch_addr), + ); + }, + else => { + _ = prefetch_addr; + }, + } + } + + const price_vec: PriceVector = self.prices[i..i+SIMD_WIDTH][0..SIMD_WIDTH].*; + const in_range = (price_vec >= min_vec) & (price_vec <= max_vec); + + // Extract matching indices + for (0..SIMD_WIDTH) |j| { + if (in_range[j] and result_count < output.len) { + output[result_count] = @as(u32, @intCast(i + j)); + result_count += 1; + } + } + + i += SIMD_WIDTH; + } + + // Handle remaining elements + while (i < self.count and result_count < output.len) { + if (self.prices[i] >= min_price and self.prices[i] <= max_price) { + output[result_count] = @as(u32, @intCast(i)); + result_count += 1; + } + i += 1; + } + } else { + // Scalar fallback for architectures without sufficient SIMD support + for (0..self.count) |i| { + if (result_count >= output.len) break; + if (self.prices[i] >= min_price and self.prices[i] <= max_price) { + output[result_count] = @as(u32, @intCast(i)); + result_count += 1; + } + } + } + + return result_count; + } + + // SIMD-optimized volume calculation + pub fn getTotalVolumeAtPrice(self: *const OptimizedOrderStorage, price: u64) u64 { + if (self.price_indices.get(price)) |indices| { + var total: u64 = 0; + + // Only use SIMD if we have sufficient width and indices + if (SIMD_WIDTH >= 4 and indices.items.len >= SIMD_WIDTH) { + const AmountVector = @Vector(SIMD_WIDTH, u64); + + var i: usize = 0; + // Process in SIMD chunks + while (i + SIMD_WIDTH <= indices.items.len) { + const indices_chunk = indices.items[i..i+SIMD_WIDTH]; + var amounts: [SIMD_WIDTH]u64 = undefined; + + // Gather amounts with bounds checking + for (indices_chunk, 0..) |idx, j| { + if (idx < self.count) { + amounts[j] = self.amounts[idx]; + } else { + amounts[j] = 0; // Safety fallback + } + } + + const amount_vec: AmountVector = amounts; + total += @reduce(.Add, amount_vec); + i += SIMD_WIDTH; + } + + // Handle remaining elements + while (i < indices.items.len) : (i += 1) { + if (indices.items[i] < self.count) { + total += self.amounts[indices.items[i]]; + } + } + } else { + // Scalar fallback + for (indices.items) |idx| { + if (idx < self.count) { + total += self.amounts[idx]; + } + } + } + + return total; + } + return 0; + } + + // Batch operations for better cache utilization + pub fn updateAmounts(self: *OptimizedOrderStorage, updates: []const AmountUpdate) void { + if (SIMD_WIDTH >= 4 and updates.len >= SIMD_WIDTH) { + const AmountVector = @Vector(SIMD_WIDTH, u64); + + var i: usize = 0; + while (i + SIMD_WIDTH <= updates.len) { + // Portable prefetch for next batch + if (i + PREFETCH_DISTANCE * SIMD_WIDTH < updates.len) { + const prefetch_addr = @intFromPtr(&updates[i + PREFETCH_DISTANCE * SIMD_WIDTH]); + switch (@import("builtin").cpu.arch) { + .x86_64 => { + asm volatile ("prefetcht0 (%[addr])" + : // no outputs + : [addr] "r" (prefetch_addr), + ); + }, + .aarch64 => { + asm volatile ("prfm pldl1keep, [%[addr]]" + : // no outputs + : [addr] "r" (prefetch_addr), + ); + }, + else => { + _ = prefetch_addr; + }, + } + } + + // Gather current amounts with bounds checking + var current_amounts: [SIMD_WIDTH]u64 = undefined; + var new_amounts: [SIMD_WIDTH]u64 = undefined; + + for (0..SIMD_WIDTH) |j| { + const update = updates[i + j]; + if (update.index < self.count) { + current_amounts[j] = self.amounts[update.index]; + new_amounts[j] = update.new_amount; + } else { + // Skip invalid indices + current_amounts[j] = 0; + new_amounts[j] = 0; + } + } + + // Vectorized update + const new_vec: AmountVector = new_amounts; + + // Scatter back with bounds checking + for (0..SIMD_WIDTH) |j| { + const update = updates[i + j]; + if (update.index < self.count) { + self.amounts[update.index] = new_vec[j]; + } + } + + i += SIMD_WIDTH; + } + + // Handle remaining updates + while (i < updates.len) : (i += 1) { + const update = updates[i]; + if (update.index < self.count) { + self.amounts[update.index] = update.new_amount; + } + } + } else { + // Scalar fallback + for (updates) |update| { + if (update.index < self.count) { + self.amounts[update.index] = update.new_amount; + } + } + } + } + + pub const AmountUpdate = struct { + index: u32, + new_amount: u64, + }; + + fn resize(self: *OptimizedOrderStorage, new_capacity: usize) !void { + const aligned_capacity = ((new_capacity + SIMD_WIDTH - 1) / SIMD_WIDTH) * SIMD_WIDTH; + + // Reallocate arrays + self.prices = try self.allocator.realloc(self.prices, aligned_capacity); + self.amounts = try self.allocator.realloc(self.amounts, aligned_capacity); + self.ids = try self.allocator.realloc(self.ids, aligned_capacity); + self.sides = try self.allocator.realloc(self.sides, aligned_capacity); + self.flags = try self.allocator.realloc(self.flags, aligned_capacity); + self.timestamps = try self.allocator.realloc(self.timestamps, aligned_capacity); + + self.capacity = aligned_capacity; + } + + pub fn getMemoryUsage(self: *const OptimizedOrderStorage) struct { + arrays_bytes: usize, + indices_bytes: usize, + total_bytes: usize, + } { + const arrays_bytes = self.capacity * ( + @sizeOf(u64) + // prices + @sizeOf(u64) + // amounts + @sizeOf(u64) + // ids + @sizeOf(u8) + // sides + @sizeOf(u32) + // flags + @sizeOf(i64) // timestamps + ); + + // Estimate index overhead (rough approximation) + const indices_bytes = self.count * @sizeOf(u64) * 2; // ID index + price indices + + return .{ + .arrays_bytes = arrays_bytes, + .indices_bytes = indices_bytes, + .total_bytes = arrays_bytes + indices_bytes, + }; + } + + pub fn printStatistics(self: *const OptimizedOrderStorage) void { + const memory = self.getMemoryUsage(); + std.debug.print("\n=== Optimized Order Storage Statistics ===\n"); + std.debug.print("Orders: {d}/{d} ({d:.1}% full)\n", .{ + self.count, + self.capacity, + @as(f64, @floatFromInt(self.count)) / @as(f64, @floatFromInt(self.capacity)) * 100.0 + }); + std.debug.print("Memory Usage:\n"); + std.debug.print(" Arrays: {d:.2} MB\n", .{@as(f64, @floatFromInt(memory.arrays_bytes)) / 1_048_576.0}); + std.debug.print(" Indices: {d:.2} MB\n", .{@as(f64, @floatFromInt(memory.indices_bytes)) / 1_048_576.0}); + std.debug.print(" Total: {d:.2} MB\n", .{@as(f64, @floatFromInt(memory.total_bytes)) / 1_048_576.0}); + std.debug.print(" Bytes per order: {d}\n", .{if (self.count > 0) memory.total_bytes / self.count else 0}); + std.debug.print("Price levels: {d}\n", .{self.price_indices.count()}); + std.debug.print("Cache alignment: {d}-byte aligned\n", .{CACHE_LINE_SIZE}); + std.debug.print("SIMD width: {d} elements\n", .{SIMD_WIDTH}); + std.debug.print("\n"); + } +}; + +// Test the optimized storage +pub fn testOptimizedStorage() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("Testing Optimized Order Storage...\n"); + + var storage = try OptimizedOrderStorage.init(allocator, 1000); + defer storage.deinit(); + + // Add test orders + const orders = [_]struct { price: u64, amount: u64, id: u64, side: u8 }{ + .{ .price = 100, .amount = 50, .id = 1, .side = 0 }, + .{ .price = 101, .amount = 75, .id = 2, .side = 0 }, + .{ .price = 100, .amount = 25, .id = 3, .side = 0 }, + .{ .price = 99, .amount = 100, .id = 4, .side = 1 }, + .{ .price = 98, .amount = 200, .id = 5, .side = 1 }, + }; + + for (orders) |order| { + _ = try storage.addOrder(order.price, order.amount, order.id, order.side, 0); + } + + storage.printStatistics(); + + // Test price range query + var results: [10]u32 = undefined; + const count = storage.getOrdersInPriceRange(99, 101, &results); + std.debug.print("Orders in range 99-101: {d}\n", .{count}); + + // Test volume calculation + const volume_100 = storage.getTotalVolumeAtPrice(100); + std.debug.print("Total volume at price 100: {d}\n", .{volume_100}); + + // Test removal + const removed = storage.removeOrder(2); + std.debug.print("Removed order 2: {}\n", .{removed}); + + storage.printStatistics(); +} + +pub fn main() !void { + try testOptimizedStorage(); +} diff --git a/src/orderbook/sharded_orderbook/perf_monitor.zig b/src/orderbook/sharded_orderbook/perf_monitor.zig index ab538b7..11eb5c4 100644 --- a/src/orderbook/sharded_orderbook/perf_monitor.zig +++ b/src/orderbook/sharded_orderbook/perf_monitor.zig @@ -6,6 +6,8 @@ pub const SIMDMetrics = struct { cache_misses: usize = 0, start_time: i128 = 0, end_time: i128 = 0, + memory_bandwidth_gb_sec: f64 = 0, + vectorization_efficiency: f64 = 0, pub fn startTimer(self: *SIMDMetrics) void { self.start_time = std.time.nanoTimestamp(); @@ -24,6 +26,22 @@ pub const SIMDMetrics = struct { if (total_ops == 0) return 0; return @as(f64, @floatFromInt(self.vector_operations)) / total_ops; } + + pub fn recordVectorOperation(self: *SIMDMetrics, vector_width: usize) void { + self.vector_operations += vector_width; + } + + pub fn recordScalarOperation(self: *SIMDMetrics) void { + self.scalar_operations += 1; + } + + pub fn calculateEfficiency(self: *SIMDMetrics, data_processed_bytes: usize) void { + const elapsed_seconds = @as(f64, @floatFromInt(self.getElapsedNanos())) / 1_000_000_000.0; + if (elapsed_seconds > 0) { + self.memory_bandwidth_gb_sec = @as(f64, @floatFromInt(data_processed_bytes)) / (elapsed_seconds * 1_000_000_000.0); + } + self.vectorization_efficiency = self.getVectorUtilization(); + } }; pub const SortMetrics = struct { @@ -31,7 +49,9 @@ pub const SortMetrics = struct { swaps: usize = 0, start_time: i128 = 0, end_time: i128 = 0, - + algorithm_used: []const u8 = "unknown", + data_size: usize = 0, + pub fn startTimer(self: *SortMetrics) void { self.start_time = std.time.nanoTimestamp(); } @@ -43,6 +63,22 @@ pub const SortMetrics = struct { pub fn getElapsedNanos(self: *const SortMetrics) i128 { return self.end_time - self.start_time; } + + pub fn recordComparison(self: *SortMetrics) void { + self.comparisons += 1; + } + + pub fn recordSwap(self: *SortMetrics) void { + self.swaps += 1; + } + + pub fn getEfficiencyRatio(self: *const SortMetrics) f64 { + // Compare against theoretical minimum comparisons for the data size + if (self.data_size <= 1) return 1.0; + const theoretical_min = @as(f64, @floatFromInt(self.data_size)) * @log(@as(f64, @floatFromInt(self.data_size))); + const actual = @as(f64, @floatFromInt(self.comparisons)); + return theoretical_min / actual; + } }; pub const BatchMetrics = struct { @@ -50,6 +86,7 @@ pub const BatchMetrics = struct { full_batches: usize = 0, partial_batches: usize = 0, total_orders: usize = 0, + total_items: usize = 0, start_time: i128 = 0, end_time: i128 = 0, @@ -69,6 +106,16 @@ pub const BatchMetrics = struct { if (self.total_batches == 0) return 0; return @as(f64, @floatFromInt(self.full_batches)) / @as(f64, @floatFromInt(self.total_batches)); } + + pub fn recordBatch(self: *BatchMetrics, is_full: bool, item_count: usize) void { + self.total_batches += 1; + self.total_items += item_count; + if (is_full) { + self.full_batches += 1; + } else { + self.partial_batches += 1; + } + } }; const MetricSample = struct { diff --git a/src/orderbook/sharded_orderbook/simd_sort.zig b/src/orderbook/sharded_orderbook/simd_sort.zig index e28e431..c2ae9dc 100644 --- a/src/orderbook/sharded_orderbook/simd_sort.zig +++ b/src/orderbook/sharded_orderbook/simd_sort.zig @@ -2,8 +2,12 @@ const std = @import("std"); const builtin = @import("builtin"); const perf = @import("perf_monitor.zig"); -// Enhanced SIMD configuration -const VECTOR_WIDTH = if (builtin.cpu.arch == .x86_64) @as(usize, 8) else @as(usize, 4); +// Enhanced SIMD configuration with fallbacks +const VECTOR_WIDTH = switch (builtin.cpu.arch) { + .x86_64 => if (std.Target.x86.featureSetHas(builtin.cpu.features, .avx2)) @as(usize, 8) else @as(usize, 4), + .aarch64 => @as(usize, 4), // NEON 128-bit + else => @as(usize, 2), // Conservative fallback +}; const BITONIC_SORT_SIZE = VECTOR_WIDTH * 8; // Increased for better vectorization const CACHE_LINE_SIZE = 64; const PREFETCH_DISTANCE = 8; @@ -26,14 +30,28 @@ pub fn SortContext(comptime T: type) type { }; } - // Prefetch next cache lines + // Portable prefetch implementation inline fn prefetchNext(self: *Self, idx: usize) void { if (idx + PREFETCH_DISTANCE < self.items.len) { const addr = @intFromPtr(&self.items[idx + PREFETCH_DISTANCE]); - asm volatile ("prefetcht0 (%[addr])" - : // no outputs - : [addr] "r" (addr), - ); + switch (@import("builtin").cpu.arch) { + .x86_64 => { + asm volatile ("prefetcht0 (%[addr])" + : // no outputs + : [addr] "r" (addr), + ); + }, + .aarch64 => { + asm volatile ("prfm pldl1keep, [%[addr]]" + : // no outputs + : [addr] "r" (addr), + ); + }, + else => { + // No prefetch support for other architectures + _ = addr; + }, + } } } }; diff --git a/src/profiler.zig b/src/profiler.zig new file mode 100644 index 0000000..8c3967d --- /dev/null +++ b/src/profiler.zig @@ -0,0 +1,262 @@ +const std = @import("std"); +const orderbook = @import("orderbook.zig"); + +pub const ProfilerResult = struct { + function_name: []const u8, + total_time_ns: u64, + call_count: usize, + avg_time_ns: u64, + percentage: f64, +}; + +pub const Profiler = struct { + allocator: std.mem.Allocator, + profiles: std.StringHashMap(ProfileData), + start_time: i128, + total_duration: i128, + + const ProfileData = struct { + total_time: u64, + call_count: usize, + start_time: i128, + }; + + pub fn init(allocator: std.mem.Allocator) Profiler { + return .{ + .allocator = allocator, + .profiles = std.StringHashMap(ProfileData).init(allocator), + .start_time = std.time.nanoTimestamp(), + .total_duration = 0, + }; + } + + pub fn deinit(self: *Profiler) void { + self.profiles.deinit(); + } + + pub fn startFunction(self: *Profiler, function_name: []const u8) !void { + const current_time = std.time.nanoTimestamp(); + + if (self.profiles.getPtr(function_name)) |data| { + data.start_time = current_time; + } else { + try self.profiles.put(function_name, ProfileData{ + .total_time = 0, + .call_count = 0, + .start_time = current_time, + }); + } + } + + pub fn endFunction(self: *Profiler, function_name: []const u8) !void { + const current_time = std.time.nanoTimestamp(); + + if (self.profiles.getPtr(function_name)) |data| { + const elapsed = @as(u64, @intCast(current_time - data.start_time)); + data.total_time += elapsed; + data.call_count += 1; + } else { + std.log.warn("endFunction called for unknown function: {s}", .{function_name}); + } + } + + pub fn generateReport(self: *Profiler) ![]ProfilerResult { + self.total_duration = std.time.nanoTimestamp() - self.start_time; + + var results = std.ArrayList(ProfilerResult).init(self.allocator); + var it = self.profiles.iterator(); + + while (it.next()) |entry| { + const data = entry.value_ptr.*; + const avg_time = if (data.call_count > 0) data.total_time / data.call_count else 0; + const percentage = @as(f64, @floatFromInt(data.total_time)) / @as(f64, @floatFromInt(@as(u64, @intCast(self.total_duration)))) * 100.0; + + try results.append(ProfilerResult{ + .function_name = entry.key_ptr.*, + .total_time_ns = data.total_time, + .call_count = data.call_count, + .avg_time_ns = avg_time, + .percentage = percentage, + }); + } + + // Sort by total time descending + std.sort.heap(ProfilerResult, results.items, {}, struct { + fn lessThan(_: void, a: ProfilerResult, b: ProfilerResult) bool { + return a.total_time_ns > b.total_time_ns; + } + }.lessThan); + + return results.toOwnedSlice(); + } + + pub fn printReport(self: *Profiler) !void { + const results = try self.generateReport(); + defer self.allocator.free(results); + + std.debug.print("\n=== Performance Profile Report ===\n"); + std.debug.print("Total Duration: {d:.2} ms\n", .{@as(f64, @floatFromInt(@as(u64, @intCast(self.total_duration)))) / 1_000_000.0}); + std.debug.print("\n{s:<30} {s:>12} {s:>12} {s:>12} {s:>8}\n", .{ + "Function", "Total (ms)", "Calls", "Avg (µs)", "% Time" + }); + std.debug.print("-" ** 76 ++ "\n"); + + for (results) |result| { + std.debug.print("{s:<30} {d:>12.2} {d:>12} {d:>12.2} {d:>7.1}%\n", .{ + result.function_name, + @as(f64, @floatFromInt(result.total_time_ns)) / 1_000_000.0, + result.call_count, + @as(f64, @floatFromInt(result.avg_time_ns)) / 1000.0, + result.percentage, + }); + } + std.debug.print("\n"); + } +}; + +// Macro for easy profiling with proper error handling +pub fn ProfiledCall(profiler: *Profiler, comptime function_name: []const u8, function: anytype, args: anytype) !@TypeOf(@call(.auto, function, args)) { + try profiler.startFunction(function_name); + defer profiler.endFunction(function_name) catch |err| { + std.log.warn("Failed to end profiling for {s}: {}", .{function_name, err}); + }; + return try @call(.auto, function, args); +} + +// Comprehensive profiling benchmark +pub fn runProfilingBenchmark() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var profiler = Profiler.init(allocator); + defer profiler.deinit(); + + // Initialize orderbook + var book = try orderbook.ShardedOrderbook.init(allocator, 8); + defer book.deinit(); + + std.debug.print("Running comprehensive profiling benchmark...\n"); + + // Test various operations with profiling + const iterations = 10_000; + var prng = std.rand.DefaultPrng.init(42); + const rng = prng.random(); + + // Profile order placement + var i: usize = 0; + while (i < iterations) : (i += 1) { + const price = rng.uintAtMost(u64, 1000) + 1; + const amount = rng.uintAtMost(u64, 100) + 1; + const id = i + 1; + + _ = try ProfiledCall(&profiler, "placeOrder", orderbook.ShardedOrderbook.placeOrder, .{ &book, .Buy, price, amount, id }); + } + + // Profile order cancellation + i = 1; + while (i <= iterations / 2) : (i += 1) { + _ = try ProfiledCall(&profiler, "cancelOrder", orderbook.ShardedOrderbook.cancelOrder, .{ &book, i }); + } + + // Profile market orders + i = 0; + while (i < 100) : (i += 1) { + const amount = rng.uintAtMost(u64, 50) + 1; + _ = try ProfiledCall(&profiler, "executeMarketOrder", orderbook.ShardedOrderbook.executeMarketOrder, .{ &book, .Sell, amount }); + } + + // Profile best bid/ask queries + i = 0; + while (i < 1000) : (i += 1) { + _ = ProfiledCall(&profiler, "getBestBid", orderbook.ShardedOrderbook.getBestBid, .{&book}) catch null; + _ = ProfiledCall(&profiler, "getBestAsk", orderbook.ShardedOrderbook.getBestAsk, .{&book}) catch null; + } + + try profiler.printReport(); +} + +// Memory profiling utilities +pub const MemoryProfiler = struct { + allocator: std.mem.Allocator, + initial_memory: usize, + peak_memory: usize, + current_memory: usize, + allocations: usize, + + pub fn init(allocator: std.mem.Allocator) MemoryProfiler { + return .{ + .allocator = allocator, + .initial_memory = 0, + .peak_memory = 0, + .current_memory = 0, + .allocations = 0, + }; + } + + pub fn startProfiling(self: *MemoryProfiler) void { + // This would ideally hook into the allocator to track memory usage + // For now, we'll use a simple estimation + self.initial_memory = 0; + self.current_memory = 0; + self.peak_memory = 0; + self.allocations = 0; + } + + pub fn recordAllocation(self: *MemoryProfiler, size: usize) void { + self.current_memory += size; + self.peak_memory = @max(self.peak_memory, self.current_memory); + self.allocations += 1; + } + + pub fn recordDeallocation(self: *MemoryProfiler, size: usize) void { + self.current_memory = if (size > self.current_memory) 0 else self.current_memory - size; + } + + pub fn printReport(self: *const MemoryProfiler) void { + std.debug.print("\n=== Memory Profile Report ===\n"); + std.debug.print("Initial Memory: {d} bytes\n", .{self.initial_memory}); + std.debug.print("Peak Memory: {d} bytes ({d:.2} MB)\n", .{ self.peak_memory, @as(f64, @floatFromInt(self.peak_memory)) / 1_048_576.0 }); + std.debug.print("Current Memory: {d} bytes\n", .{self.current_memory}); + std.debug.print("Total Allocations: {d}\n", .{self.allocations}); + std.debug.print("Memory Efficiency: {d:.1}%\n", .{@as(f64, @floatFromInt(self.current_memory)) / @as(f64, @floatFromInt(self.peak_memory)) * 100.0}); + std.debug.print("\n"); + } +}; + +// Cache analysis utilities +pub const CacheProfiler = struct { + l1_hits: usize = 0, + l1_misses: usize = 0, + l2_hits: usize = 0, + l2_misses: usize = 0, + l3_hits: usize = 0, + l3_misses: usize = 0, + + pub fn getL1HitRatio(self: *const CacheProfiler) f64 { + const total = self.l1_hits + self.l1_misses; + return if (total > 0) @as(f64, @floatFromInt(self.l1_hits)) / @as(f64, @floatFromInt(total)) * 100.0 else 0.0; + } + + pub fn getL2HitRatio(self: *const CacheProfiler) f64 { + const total = self.l2_hits + self.l2_misses; + return if (total > 0) @as(f64, @floatFromInt(self.l2_hits)) / @as(f64, @floatFromInt(total)) * 100.0 else 0.0; + } + + pub fn getL3HitRatio(self: *const CacheProfiler) f64 { + const total = self.l3_hits + self.l3_misses; + return if (total > 0) @as(f64, @floatFromInt(self.l3_hits)) / @as(f64, @floatFromInt(total)) * 100.0 else 0.0; + } + + pub fn printReport(self: *const CacheProfiler) void { + std.debug.print("\n=== Cache Profile Report ===\n"); + std.debug.print("L1 Cache: {d} hits, {d} misses ({d:.1}% hit ratio)\n", .{ self.l1_hits, self.l1_misses, self.getL1HitRatio() }); + std.debug.print("L2 Cache: {d} hits, {d} misses ({d:.1}% hit ratio)\n", .{ self.l2_hits, self.l2_misses, self.getL2HitRatio() }); + std.debug.print("L3 Cache: {d} hits, {d} misses ({d:.1}% hit ratio)\n", .{ self.l3_hits, self.l3_misses, self.getL3HitRatio() }); + std.debug.print("\n"); + } +}; + +pub fn main() !void { + try runProfilingBenchmark(); +} diff --git a/src/regression_test.zig b/src/regression_test.zig new file mode 100644 index 0000000..7b2d97e --- /dev/null +++ b/src/regression_test.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const orderbook = @import("orderbook.zig"); + +/// Performance regression test that validates against historical baselines +pub const RegressionTester = struct { + allocator: std.mem.Allocator, + baseline_file: []const u8, + tolerance_pct: f64, + + pub const RegressionResult = struct { + operation: []const u8, + baseline_latency_p99: u64, + current_latency_p99: u64, + baseline_throughput: f64, + current_throughput: f64, + latency_regression_pct: f64, + throughput_regression_pct: f64, + passed: bool, + }; + + pub fn init(allocator: std.mem.Allocator, baseline_file: []const u8, tolerance_pct: f64) RegressionTester { + return .{ + .allocator = allocator, + .baseline_file = baseline_file, + .tolerance_pct = tolerance_pct, + }; + } + + pub fn runRegressionTest(self: *RegressionTester) ![]RegressionResult { + // Load baseline results + const baseline = try self.loadBaseline(); + defer self.allocator.free(baseline); + + // Run current benchmarks (simplified version) + const current = try self.runCurrentBenchmarks(); + defer self.allocator.free(current); + + // Compare results + var results = std.ArrayList(RegressionResult).init(self.allocator); + + for (baseline) |base| { + for (current) |curr| { + if (std.mem.eql(u8, base.operation, curr.operation)) { + const latency_regression = if (base.latency_p99 > 0) + (@as(f64, @floatFromInt(curr.latency_p99)) - @as(f64, @floatFromInt(base.latency_p99))) / @as(f64, @floatFromInt(base.latency_p99)) * 100.0 + else + 0.0; + + const throughput_regression = if (base.throughput > 0) + (curr.throughput - base.throughput) / base.throughput * 100.0 + else + 0.0; + + const passed = latency_regression <= self.tolerance_pct and throughput_regression >= -self.tolerance_pct; + + try results.append(RegressionResult{ + .operation = try self.allocator.dupe(u8, base.operation), + .baseline_latency_p99 = base.latency_p99, + .current_latency_p99 = curr.latency_p99, + .baseline_throughput = base.throughput, + .current_throughput = curr.throughput, + .latency_regression_pct = latency_regression, + .throughput_regression_pct = throughput_regression, + .passed = passed, + }); + break; + } + } + } + + return results.toOwnedSlice(); + } + + const BaselineResult = struct { + operation: []const u8, + latency_p99: u64, + throughput: f64, + }; + + fn loadBaseline(self: *RegressionTester) ![]BaselineResult { + // For now, return hardcoded baseline values + // In a real implementation, this would load from JSON file + const baseline_data = [_]BaselineResult{ + .{ .operation = "Place Orders", .latency_p99 = 5000, .throughput = 200000 }, + .{ .operation = "Cancel Orders", .latency_p99 = 4000, .throughput = 250000 }, + .{ .operation = "Market Orders", .latency_p99 = 10000, .throughput = 100000 }, + .{ .operation = "Burst Orders", .latency_p99 = 3000, .throughput = 500000 }, + }; + + var results = std.ArrayList(BaselineResult).init(self.allocator); + for (baseline_data) |item| { + try results.append(.{ + .operation = try self.allocator.dupe(u8, item.operation), + .latency_p99 = item.latency_p99, + .throughput = item.throughput, + }); + } + + return results.toOwnedSlice(); + } + + fn runCurrentBenchmarks(self: *RegressionTester) ![]BaselineResult { + // Run a simplified benchmark suite + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var book = try orderbook.ShardedOrderbook.init(allocator, 8); + defer book.deinit(); + + var results = std.ArrayList(BaselineResult).init(self.allocator); + + // Simple place order benchmark + const place_result = try self.benchmarkPlaceOrders(&book); + try results.append(.{ + .operation = try self.allocator.dupe(u8, "Place Orders"), + .latency_p99 = place_result.latency_p99, + .throughput = place_result.throughput, + }); + + // Add more benchmarks as needed... + + return results.toOwnedSlice(); + } + + const SimpleBenchResult = struct { + latency_p99: u64, + throughput: f64, + }; + + fn benchmarkPlaceOrders(self: *RegressionTester, book: *orderbook.ShardedOrderbook) !SimpleBenchResult { + _ = self; + const iterations = 10000; + var latencies = std.ArrayList(u64).init(self.allocator); + defer latencies.deinit(); + + var timer = try std.time.Timer.start(); + const start_time = timer.read(); + + var prng = std.rand.DefaultPrng.init(42); + const rng = prng.random(); + + for (0..iterations) |i| { + timer.reset(); + const price = rng.uintAtMost(u64, 1000) + 1; + const amount = rng.uintAtMost(u64, 100) + 1; + book.placeOrder(.Buy, price, amount, i + 1) catch continue; + + const latency = timer.read(); + try latencies.append(latency); + } + + const total_time = timer.read() - start_time; + const throughput = @as(f64, @floatFromInt(iterations)) / (@as(f64, @floatFromInt(total_time)) / 1_000_000_000.0); + + std.sort.heap(u64, latencies.items, {}, std.sort.asc(u64)); + const latency_p99 = latencies.items[latencies.items.len * 99 / 100]; + + return SimpleBenchResult{ + .latency_p99 = latency_p99, + .throughput = throughput, + }; + } + + pub fn printResults(results: []const RegressionResult) void { + std.debug.print("\n" ++ "=" ** 80 ++ "\n"); + std.debug.print("PERFORMANCE REGRESSION TEST RESULTS\n"); + std.debug.print("=" ** 80 ++ "\n"); + + var passed_count: usize = 0; + var failed_count: usize = 0; + + for (results) |result| { + if (result.passed) { + passed_count += 1; + } else { + failed_count += 1; + } + + const status = if (result.passed) "PASS" else "FAIL"; + std.debug.print("\n{s}: {s}\n", .{ result.operation, status }); + std.debug.print(" Latency P99:\n"); + std.debug.print(" Baseline: {d:.2} µs\n", .{@as(f64, @floatFromInt(result.baseline_latency_p99)) / 1000.0}); + std.debug.print(" Current: {d:.2} µs\n", .{@as(f64, @floatFromInt(result.current_latency_p99)) / 1000.0}); + std.debug.print(" Change: {d:+.1}%\n", .{result.latency_regression_pct}); + + std.debug.print(" Throughput:\n"); + std.debug.print(" Baseline: {d:.0} ops/sec\n", .{result.baseline_throughput}); + std.debug.print(" Current: {d:.0} ops/sec\n", .{result.current_throughput}); + std.debug.print(" Change: {d:+.1}%\n", .{result.throughput_regression_pct}); + } + + std.debug.print("\n" ++ "-" ** 80 ++ "\n"); + std.debug.print("Summary: {d} passed, {d} failed\n", .{ passed_count, failed_count }); + + if (failed_count == 0) { + std.debug.print("✅ All performance regression tests PASSED\n"); + } else { + std.debug.print("❌ Performance regression detected!\n"); + } + std.debug.print("=" ** 80 ++ "\n"); + } +}; + +// CI-friendly test that exits with appropriate codes +pub fn runCIRegressionTest() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var tester = RegressionTester.init(allocator, "baseline.json", 10.0); // 10% tolerance + const results = try tester.runRegressionTest(); + defer { + for (results) |result| { + allocator.free(result.operation); + } + allocator.free(results); + } + + RegressionTester.printResults(results); + + // Check if any tests failed + for (results) |result| { + if (!result.passed) { + std.process.exit(1); // Exit with error code for CI + } + } + + std.process.exit(0); // Success +} + +pub fn main() !void { + try runCIRegressionTest(); +}