Skip to main content

dcp/bench/
suite.rs

1//! Comprehensive benchmark suite for DCP performance testing.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8use super::json_rpc::compare_sizes_auto;
9
10/// Configuration for benchmark runs.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct BenchmarkConfig {
13    /// Number of iterations for throughput tests
14    pub iterations: usize,
15    /// Number of warmup iterations
16    pub warmup_iterations: usize,
17    /// Message sizes to test (in bytes)
18    pub message_sizes: Vec<usize>,
19    /// Number of concurrent connections for throughput tests
20    pub concurrency_levels: Vec<usize>,
21    /// Whether to include DCP vs MCP comparison
22    pub include_comparison: bool,
23}
24
25impl Default for BenchmarkConfig {
26    fn default() -> Self {
27        Self {
28            iterations: 10000,
29            warmup_iterations: 1000,
30            message_sizes: vec![64, 256, 1024, 4096, 16384],
31            concurrency_levels: vec![1, 10, 50, 100],
32            include_comparison: true,
33        }
34    }
35}
36
37/// Latency statistics with percentiles.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct LatencyStats {
40    /// Minimum latency
41    pub min: Duration,
42    /// Maximum latency
43    pub max: Duration,
44    /// Mean latency
45    pub mean: Duration,
46    /// Median (p50) latency
47    pub p50: Duration,
48    /// 95th percentile latency
49    pub p95: Duration,
50    /// 99th percentile latency
51    pub p99: Duration,
52    /// Standard deviation
53    pub std_dev: Duration,
54}
55
56impl LatencyStats {
57    /// Calculate latency statistics from a list of durations.
58    pub fn from_durations(mut durations: Vec<Duration>) -> Self {
59        if durations.is_empty() {
60            return Self {
61                min: Duration::ZERO,
62                max: Duration::ZERO,
63                mean: Duration::ZERO,
64                p50: Duration::ZERO,
65                p95: Duration::ZERO,
66                p99: Duration::ZERO,
67                std_dev: Duration::ZERO,
68            };
69        }
70
71        durations.sort();
72        let n = durations.len();
73
74        let min = durations[0];
75        let max = durations[n - 1];
76
77        let total: Duration = durations.iter().sum();
78        let mean = total / n as u32;
79
80        let p50 = durations[n / 2];
81        let p95 = durations[(n as f64 * 0.95) as usize];
82        let p99 = durations[(n as f64 * 0.99).min((n - 1) as f64) as usize];
83
84        // Calculate standard deviation
85        let mean_nanos = mean.as_nanos() as f64;
86        let variance: f64 = durations
87            .iter()
88            .map(|d| {
89                let diff = d.as_nanos() as f64 - mean_nanos;
90                diff * diff
91            })
92            .sum::<f64>()
93            / n as f64;
94        let std_dev = Duration::from_nanos(variance.sqrt() as u64);
95
96        Self {
97            min,
98            max,
99            mean,
100            p50,
101            p95,
102            p99,
103            std_dev,
104        }
105    }
106}
107
108/// Throughput measurement result.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ThroughputResult {
111    /// Messages processed per second
112    pub messages_per_second: f64,
113    /// Bytes processed per second
114    pub bytes_per_second: f64,
115    /// Total messages processed
116    pub total_messages: usize,
117    /// Total bytes processed
118    pub total_bytes: usize,
119    /// Total duration
120    pub duration: Duration,
121}
122
123/// Memory usage measurement.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct MemoryUsage {
126    /// Heap allocated bytes
127    pub heap_bytes: usize,
128    /// Peak memory usage
129    pub peak_bytes: usize,
130    /// Number of allocations
131    pub allocation_count: usize,
132}
133
134impl Default for MemoryUsage {
135    fn default() -> Self {
136        Self {
137            heap_bytes: 0,
138            peak_bytes: 0,
139            allocation_count: 0,
140        }
141    }
142}
143
144/// DCP vs MCP comparison result.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ProtocolComparison {
147    /// Payload description
148    pub payload_name: String,
149    /// JSON-RPC (MCP) message size
150    pub mcp_size: usize,
151    /// DCP binary message size
152    pub dcp_size: usize,
153    /// Size ratio (MCP / DCP)
154    pub size_ratio: f64,
155    /// MCP encoding latency
156    pub mcp_encode_latency: Duration,
157    /// DCP encoding latency
158    pub dcp_encode_latency: Duration,
159    /// Latency ratio (MCP / DCP)
160    pub latency_ratio: f64,
161}
162
163/// Individual benchmark result.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct BenchmarkResult {
166    /// Benchmark name
167    pub name: String,
168    /// Latency statistics
169    pub latency: LatencyStats,
170    /// Throughput result
171    pub throughput: ThroughputResult,
172    /// Memory usage (if measured)
173    pub memory: Option<MemoryUsage>,
174    /// Additional metadata
175    pub metadata: HashMap<String, String>,
176}
177
178/// Complete benchmark suite results.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct BenchmarkResults {
181    /// Configuration used
182    pub config: BenchmarkConfig,
183    /// Individual benchmark results
184    pub results: Vec<BenchmarkResult>,
185    /// Protocol comparison results
186    pub comparisons: Vec<ProtocolComparison>,
187    /// Timestamp when benchmarks were run
188    pub timestamp: String,
189    /// Total benchmark duration
190    pub total_duration: Duration,
191}
192
193impl BenchmarkResults {
194    /// Generate a JSON report.
195    pub fn to_json(&self) -> String {
196        serde_json::to_string_pretty(self).unwrap_or_default()
197    }
198
199    /// Generate a Markdown report.
200    pub fn to_markdown(&self) -> String {
201        let mut md = String::new();
202
203        md.push_str("# DCP Benchmark Results\n\n");
204        md.push_str(&format!("Generated: {}\n\n", self.timestamp));
205        md.push_str(&format!(
206            "Total Duration: {:.2}s\n\n",
207            self.total_duration.as_secs_f64()
208        ));
209
210        // Configuration
211        md.push_str("## Configuration\n\n");
212        md.push_str(&format!("- Iterations: {}\n", self.config.iterations));
213        md.push_str(&format!(
214            "- Warmup Iterations: {}\n",
215            self.config.warmup_iterations
216        ));
217        md.push_str(&format!(
218            "- Message Sizes: {:?}\n",
219            self.config.message_sizes
220        ));
221        md.push_str(&format!(
222            "- Concurrency Levels: {:?}\n\n",
223            self.config.concurrency_levels
224        ));
225
226        // Latency Results
227        md.push_str("## Latency Results\n\n");
228        md.push_str("| Benchmark | Min | Mean | P50 | P95 | P99 | Max |\n");
229        md.push_str("|-----------|-----|------|-----|-----|-----|-----|\n");
230
231        for result in &self.results {
232            md.push_str(&format!(
233                "| {} | {:?} | {:?} | {:?} | {:?} | {:?} | {:?} |\n",
234                result.name,
235                result.latency.min,
236                result.latency.mean,
237                result.latency.p50,
238                result.latency.p95,
239                result.latency.p99,
240                result.latency.max
241            ));
242        }
243        md.push('\n');
244
245        // Throughput Results
246        md.push_str("## Throughput Results\n\n");
247        md.push_str("| Benchmark | Messages/sec | MB/sec | Total Messages |\n");
248        md.push_str("|-----------|--------------|--------|----------------|\n");
249
250        for result in &self.results {
251            md.push_str(&format!(
252                "| {} | {:.0} | {:.2} | {} |\n",
253                result.name,
254                result.throughput.messages_per_second,
255                result.throughput.bytes_per_second / 1_000_000.0,
256                result.throughput.total_messages
257            ));
258        }
259        md.push('\n');
260
261        // Protocol Comparison
262        if !self.comparisons.is_empty() {
263            md.push_str("## DCP vs MCP Comparison\n\n");
264            md.push_str("| Payload | MCP Size | DCP Size | Size Ratio | MCP Latency | DCP Latency | Latency Ratio |\n");
265            md.push_str("|---------|----------|----------|------------|-------------|-------------|---------------|\n");
266
267            for comp in &self.comparisons {
268                md.push_str(&format!(
269                    "| {} | {} B | {} B | {:.2}x | {:?} | {:?} | {:.2}x |\n",
270                    comp.payload_name,
271                    comp.mcp_size,
272                    comp.dcp_size,
273                    comp.size_ratio,
274                    comp.mcp_encode_latency,
275                    comp.dcp_encode_latency,
276                    comp.latency_ratio
277                ));
278            }
279            md.push('\n');
280        }
281
282        md
283    }
284}
285
286/// Benchmark suite runner.
287pub struct BenchmarkSuite {
288    config: BenchmarkConfig,
289    results: Vec<BenchmarkResult>,
290    comparisons: Vec<ProtocolComparison>,
291}
292
293impl BenchmarkSuite {
294    /// Create a new benchmark suite with the given configuration.
295    pub fn new(config: BenchmarkConfig) -> Self {
296        Self {
297            config,
298            results: Vec::new(),
299            comparisons: Vec::new(),
300        }
301    }
302
303    /// Create a benchmark suite with default configuration.
304    pub fn with_defaults() -> Self {
305        Self::new(BenchmarkConfig::default())
306    }
307
308    /// Run all benchmarks and return results.
309    pub fn run(&mut self) -> BenchmarkResults {
310        let start = Instant::now();
311
312        // Run throughput benchmarks
313        self.run_throughput_benchmarks();
314
315        // Run latency benchmarks
316        self.run_latency_benchmarks();
317
318        // Run protocol comparison
319        if self.config.include_comparison {
320            self.run_protocol_comparison();
321        }
322
323        let total_duration = start.elapsed();
324
325        BenchmarkResults {
326            config: self.config.clone(),
327            results: self.results.clone(),
328            comparisons: self.comparisons.clone(),
329            timestamp: chrono_lite_timestamp(),
330            total_duration,
331        }
332    }
333
334    fn run_throughput_benchmarks(&mut self) {
335        for &size in &self.config.message_sizes.clone() {
336            let result = self.benchmark_throughput(size);
337            self.results.push(result);
338        }
339    }
340
341    fn run_latency_benchmarks(&mut self) {
342        for &size in &self.config.message_sizes.clone() {
343            let result = self.benchmark_latency(size);
344            self.results.push(result);
345        }
346    }
347
348    fn benchmark_throughput(&self, message_size: usize) -> BenchmarkResult {
349        let payload = generate_payload(message_size);
350        let iterations = self.config.iterations;
351
352        // Warmup
353        for _ in 0..self.config.warmup_iterations {
354            let _ = serde_json::to_string(&payload);
355        }
356
357        let start = Instant::now();
358        let mut total_bytes = 0;
359
360        for _ in 0..iterations {
361            let encoded = serde_json::to_string(&payload).unwrap_or_default();
362            total_bytes += encoded.len();
363        }
364
365        let duration = start.elapsed();
366        let messages_per_second = iterations as f64 / duration.as_secs_f64();
367        let bytes_per_second = total_bytes as f64 / duration.as_secs_f64();
368
369        BenchmarkResult {
370            name: format!("throughput_{}b", message_size),
371            latency: LatencyStats::from_durations(vec![duration / iterations as u32]),
372            throughput: ThroughputResult {
373                messages_per_second,
374                bytes_per_second,
375                total_messages: iterations,
376                total_bytes,
377                duration,
378            },
379            memory: None,
380            metadata: HashMap::new(),
381        }
382    }
383
384    fn benchmark_latency(&self, message_size: usize) -> BenchmarkResult {
385        let payload = generate_payload(message_size);
386        let iterations = self.config.iterations;
387
388        // Warmup
389        for _ in 0..self.config.warmup_iterations {
390            let _ = serde_json::to_string(&payload);
391        }
392
393        let mut latencies = Vec::with_capacity(iterations);
394        let mut total_bytes = 0;
395
396        for _ in 0..iterations {
397            let start = Instant::now();
398            let encoded = serde_json::to_string(&payload).unwrap_or_default();
399            latencies.push(start.elapsed());
400            total_bytes += encoded.len();
401        }
402
403        let total_duration: Duration = latencies.iter().sum();
404        let messages_per_second = iterations as f64 / total_duration.as_secs_f64();
405        let bytes_per_second = total_bytes as f64 / total_duration.as_secs_f64();
406
407        BenchmarkResult {
408            name: format!("latency_{}b", message_size),
409            latency: LatencyStats::from_durations(latencies),
410            throughput: ThroughputResult {
411                messages_per_second,
412                bytes_per_second,
413                total_messages: iterations,
414                total_bytes,
415                duration: total_duration,
416            },
417            memory: None,
418            metadata: HashMap::new(),
419        }
420    }
421
422    fn run_protocol_comparison(&mut self) {
423        let payloads = get_realistic_payloads();
424
425        for (name, params) in payloads {
426            let comparison = self.compare_protocols(&name, &params);
427            self.comparisons.push(comparison);
428        }
429    }
430
431    fn compare_protocols(&self, name: &str, params: &Value) -> ProtocolComparison {
432        let size_comparison = compare_sizes_auto("tools/call", params);
433
434        // Measure MCP encoding latency
435        let mcp_latency = {
436            let mut total = Duration::ZERO;
437            for _ in 0..1000 {
438                let start = Instant::now();
439                let _ = serde_json::to_string(params);
440                total += start.elapsed();
441            }
442            total / 1000
443        };
444
445        // Measure DCP encoding latency (simulated binary encoding)
446        let dcp_latency = {
447            let mut total = Duration::ZERO;
448            for _ in 0..1000 {
449                let start = Instant::now();
450                let _ = simulate_binary_encode(params);
451                total += start.elapsed();
452            }
453            total / 1000
454        };
455
456        let latency_ratio = if dcp_latency.as_nanos() > 0 {
457            mcp_latency.as_nanos() as f64 / dcp_latency.as_nanos() as f64
458        } else {
459            1.0
460        };
461
462        ProtocolComparison {
463            payload_name: name.to_string(),
464            mcp_size: size_comparison.json_rpc_size,
465            dcp_size: size_comparison.dcp_size,
466            size_ratio: size_comparison.ratio,
467            mcp_encode_latency: mcp_latency,
468            dcp_encode_latency: dcp_latency,
469            latency_ratio,
470        }
471    }
472}
473
474/// Generate a payload of approximately the given size.
475fn generate_payload(target_size: usize) -> Value {
476    let base = json!({
477        "jsonrpc": "2.0",
478        "method": "tools/call",
479        "id": 1,
480        "params": {
481            "name": "test_tool"
482        }
483    });
484
485    let base_size = serde_json::to_string(&base).unwrap_or_default().len();
486
487    if target_size <= base_size {
488        return base;
489    }
490
491    let padding_size = target_size - base_size - 20; // Account for JSON overhead
492    let padding = "x".repeat(padding_size.max(0));
493
494    json!({
495        "jsonrpc": "2.0",
496        "method": "tools/call",
497        "id": 1,
498        "params": {
499            "name": "test_tool",
500            "data": padding
501        }
502    })
503}
504
505/// Get realistic tool invocation payloads for comparison.
506fn get_realistic_payloads() -> Vec<(String, Value)> {
507    vec![
508        (
509            "simple_tool_call".to_string(),
510            json!({
511                "name": "read_file",
512                "arguments": {
513                    "path": "/home/user/project/src/main.rs"
514                }
515            }),
516        ),
517        (
518            "tool_with_options".to_string(),
519            json!({
520                "name": "search",
521                "arguments": {
522                    "query": "function definition",
523                    "path": "/home/user/project",
524                    "include": ["*.rs", "*.ts"],
525                    "exclude": ["target", "node_modules"],
526                    "caseSensitive": false,
527                    "maxResults": 100
528                }
529            }),
530        ),
531        (
532            "code_execution".to_string(),
533            json!({
534                "name": "execute_code",
535                "arguments": {
536                    "language": "python",
537                    "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)\n\nprint(fibonacci(10))",
538                    "timeout": 30000
539                }
540            }),
541        ),
542        (
543            "large_content".to_string(),
544            json!({
545                "name": "write_file",
546                "arguments": {
547                    "path": "/home/user/project/output.txt",
548                    "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(100)
549                }
550            }),
551        ),
552        (
553            "structured_data".to_string(),
554            json!({
555                "name": "create_resource",
556                "arguments": {
557                    "type": "database",
558                    "config": {
559                        "host": "localhost",
560                        "port": 5432,
561                        "database": "myapp",
562                        "user": "admin",
563                        "ssl": true,
564                        "poolSize": 10,
565                        "timeout": 30000,
566                        "retries": 3
567                    }
568                }
569            }),
570        ),
571    ]
572}
573
574/// Simulate binary encoding (placeholder for actual DCP encoding).
575fn simulate_binary_encode(value: &Value) -> Vec<u8> {
576    // Simple simulation: convert to msgpack-like format
577    let json_str = serde_json::to_string(value).unwrap_or_default();
578    // Simulate compression ratio of ~0.6
579    let compressed_size = (json_str.len() as f64 * 0.6) as usize;
580    vec![0u8; compressed_size]
581}
582
583/// Simple timestamp without external dependency.
584fn chrono_lite_timestamp() -> String {
585    use std::time::SystemTime;
586    let duration = SystemTime::now()
587        .duration_since(SystemTime::UNIX_EPOCH)
588        .unwrap_or_default();
589    format!("{}s since epoch", duration.as_secs())
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn test_latency_stats() {
598        let durations: Vec<Duration> = (1..=100).map(|i| Duration::from_micros(i * 10)).collect();
599
600        let stats = LatencyStats::from_durations(durations);
601
602        assert_eq!(stats.min, Duration::from_micros(10));
603        assert_eq!(stats.max, Duration::from_micros(1000));
604        // p50 is the median - for 100 elements, index 50 gives us element 51 (510µs)
605        assert_eq!(stats.p50, Duration::from_micros(510));
606    }
607
608    #[test]
609    fn test_benchmark_config_default() {
610        let config = BenchmarkConfig::default();
611        assert_eq!(config.iterations, 10000);
612        assert!(!config.message_sizes.is_empty());
613    }
614
615    #[test]
616    fn test_generate_payload() {
617        let payload = generate_payload(256);
618        let size = serde_json::to_string(&payload).unwrap().len();
619        // Should be approximately the target size
620        assert!(size >= 200 && size <= 300);
621    }
622
623    #[test]
624    fn test_benchmark_suite_runs() {
625        let config = BenchmarkConfig {
626            iterations: 100,
627            warmup_iterations: 10,
628            message_sizes: vec![64],
629            concurrency_levels: vec![1],
630            include_comparison: true,
631        };
632
633        let mut suite = BenchmarkSuite::new(config);
634        let results = suite.run();
635
636        assert!(!results.results.is_empty());
637        assert!(!results.comparisons.is_empty());
638    }
639
640    #[test]
641    fn test_results_to_json() {
642        let results = BenchmarkResults {
643            config: BenchmarkConfig::default(),
644            results: vec![],
645            comparisons: vec![],
646            timestamp: "test".to_string(),
647            total_duration: Duration::from_secs(1),
648        };
649
650        let json = results.to_json();
651        assert!(json.contains("config"));
652        assert!(json.contains("results"));
653    }
654
655    #[test]
656    fn test_results_to_markdown() {
657        let results = BenchmarkResults {
658            config: BenchmarkConfig::default(),
659            results: vec![],
660            comparisons: vec![],
661            timestamp: "test".to_string(),
662            total_duration: Duration::from_secs(1),
663        };
664
665        let md = results.to_markdown();
666        assert!(md.contains("# DCP Benchmark Results"));
667        assert!(md.contains("## Configuration"));
668    }
669}