|
| 1 | +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; |
| 2 | +use iec_61850_lib::decode_goose::{decode_ethernet_header, decode_goose_pdu, is_goose_frame}; |
| 3 | +use iec_61850_lib::encode_goose::{encode_ethernet_header, encode_goose}; |
| 4 | +use iec_61850_lib::types::{EthernetHeader, IECData, IECGoosePdu, TimeQuality, Timestamp}; |
| 5 | + |
| 6 | +/// Create sample GOOSE PDU for encoding with realistic data size |
| 7 | +/// Typical GOOSE frames contain 50-200 data points |
| 8 | +/// This creates a large frame approaching Ethernet MTU limit (~1500 bytes) |
| 9 | +fn create_sample_goose_pdu() -> IECGoosePdu { |
| 10 | + // Create a realistic dataset with ~220 data points |
| 11 | + // This represents a complete substation bay with: |
| 12 | + // - Circuit breaker statuses |
| 13 | + // - Disconnector positions |
| 14 | + // - Analog measurements (scaled integers) |
| 15 | + // - Quality bits |
| 16 | + // - Protection relay outputs |
| 17 | + let mut all_data = vec![]; |
| 18 | + |
| 19 | + // 40 circuit breaker positions (Boolean) |
| 20 | + for i in 0..40 { |
| 21 | + all_data.push(IECData::Boolean(i % 2 == 0)); |
| 22 | + } |
| 23 | + |
| 24 | + // 50 disconnector positions (Boolean) |
| 25 | + for i in 0..50 { |
| 26 | + all_data.push(IECData::Boolean(i % 3 == 0)); |
| 27 | + } |
| 28 | + |
| 29 | + // 50 analog values (current/voltage as Int32) |
| 30 | + for i in 0..50 { |
| 31 | + all_data.push(IECData::Int((10000 + i * 1000) as i64)); |
| 32 | + } |
| 33 | + |
| 34 | + // 50 quality/status values (UInt32 bitstrings) |
| 35 | + for i in 0..50 { |
| 36 | + all_data.push(IECData::UInt(0xC000 + i as u64)); // Quality bits |
| 37 | + } |
| 38 | + |
| 39 | + // Add structured data representing multiple bays (15 strings each with status info) |
| 40 | + for bay in 1..=15 { |
| 41 | + all_data.push(IECData::VisibleString(format!("BAY_{:02}_CB_STATUS", bay))); |
| 42 | + all_data.push(IECData::Int(13800 + bay as i64 * 10)); // Voltage |
| 43 | + all_data.push(IECData::Int(450 + bay as i64)); // Current |
| 44 | + all_data.push(IECData::Boolean(bay % 2 == 0)); // Trip signal |
| 45 | + } |
| 46 | + |
| 47 | + IECGoosePdu { |
| 48 | + go_cb_ref: "SUBSTATION1/BAY_COMPLETE/LLN0$GO$gcb_full_status".to_string(), |
| 49 | + time_allowed_to_live: 2000, |
| 50 | + dat_set: "SUBSTATION1/BAY_COMPLETE/LLN0$DATASET_FULL_STATUS".to_string(), |
| 51 | + go_id: "GOOSE_SUBSTATION_COMPLETE_STATUS".to_string(), |
| 52 | + t: Timestamp { |
| 53 | + seconds: 539035154, |
| 54 | + fraction: 667648, |
| 55 | + quality: TimeQuality::default(), |
| 56 | + }, |
| 57 | + st_num: 1, |
| 58 | + sq_num: 42, |
| 59 | + simulation: false, |
| 60 | + conf_rev: 128, |
| 61 | + nds_com: false, |
| 62 | + num_dat_set_entries: all_data.len() as u32, |
| 63 | + all_data, |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +/// Create a large GOOSE packet dynamically for benchmarking |
| 68 | +/// This approaches the Ethernet MTU limit (~1500 bytes) |
| 69 | +fn create_large_goose_packet() -> Vec<u8> { |
| 70 | + let header = create_sample_ethernet_header(); |
| 71 | + let pdu = create_sample_goose_pdu(); |
| 72 | + encode_goose(&header, &pdu).expect("Failed to encode large GOOSE packet") |
| 73 | +} |
| 74 | + |
| 75 | +/// Create sample Ethernet header for encoding |
| 76 | +fn create_sample_ethernet_header() -> EthernetHeader { |
| 77 | + EthernetHeader { |
| 78 | + dst_addr: [0x01, 0x0c, 0xcd, 0x01, 0x00, 0x01], |
| 79 | + src_addr: [0x00, 0x1a, 0xb6, 0x03, 0x2f, 0x1c], |
| 80 | + tpid: Some([0x81, 0x00]), |
| 81 | + tci: Some([0x00, 0x01]), |
| 82 | + ether_type: [0x88, 0xb8], |
| 83 | + appid: [0x10, 0x01], |
| 84 | + length: [0x00, 0x8c], |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +fn benchmark_goose_frame_detection(c: &mut Criterion) { |
| 89 | + let packet = create_large_goose_packet(); |
| 90 | + |
| 91 | + // Print packet size information |
| 92 | + println!("\n=== GOOSE Benchmark Packet Info ==="); |
| 93 | + println!("Total packet size: {} bytes", packet.len()); |
| 94 | + println!("Ethernet MTU limit: ~1500 bytes"); |
| 95 | + println!( |
| 96 | + "Utilization: {:.1}%", |
| 97 | + (packet.len() as f64 / 1500.0) * 100.0 |
| 98 | + ); |
| 99 | + println!("===================================\n"); |
| 100 | + |
| 101 | + c.bench_function("goose_frame_detection", |b| { |
| 102 | + b.iter(|| is_goose_frame(black_box(&packet))); |
| 103 | + }); |
| 104 | +} |
| 105 | + |
| 106 | +fn benchmark_ethernet_header_decode(c: &mut Criterion) { |
| 107 | + let packet = create_large_goose_packet(); |
| 108 | + |
| 109 | + c.bench_function("ethernet_header_decode", |b| { |
| 110 | + b.iter(|| { |
| 111 | + let mut header = EthernetHeader::default(); |
| 112 | + decode_ethernet_header(black_box(&mut header), black_box(&packet)) |
| 113 | + }); |
| 114 | + }); |
| 115 | +} |
| 116 | + |
| 117 | +fn benchmark_goose_pdu_decode(c: &mut Criterion) { |
| 118 | + let packet = create_large_goose_packet(); |
| 119 | + let mut header = EthernetHeader::default(); |
| 120 | + let pos = decode_ethernet_header(&mut header, &packet); |
| 121 | + |
| 122 | + c.bench_function("goose_pdu_decode", |b| { |
| 123 | + b.iter(|| decode_goose_pdu(black_box(&packet), black_box(pos))); |
| 124 | + }); |
| 125 | +} |
| 126 | + |
| 127 | +fn benchmark_full_goose_decode(c: &mut Criterion) { |
| 128 | + let packet = create_large_goose_packet(); |
| 129 | + |
| 130 | + c.bench_function("full_goose_decode", |b| { |
| 131 | + b.iter(|| { |
| 132 | + let mut header = EthernetHeader::default(); |
| 133 | + let pos = decode_ethernet_header(black_box(&mut header), black_box(&packet)); |
| 134 | + decode_goose_pdu(black_box(&packet), black_box(pos)) |
| 135 | + }); |
| 136 | + }); |
| 137 | +} |
| 138 | + |
| 139 | +fn benchmark_ethernet_header_encode(c: &mut Criterion) { |
| 140 | + let header = create_sample_ethernet_header(); |
| 141 | + |
| 142 | + c.bench_function("ethernet_header_encode", |b| { |
| 143 | + b.iter(|| encode_ethernet_header(black_box(&header), black_box(140))); |
| 144 | + }); |
| 145 | +} |
| 146 | + |
| 147 | +fn benchmark_goose_pdu_encode(c: &mut Criterion) { |
| 148 | + let header = create_sample_ethernet_header(); |
| 149 | + let pdu = create_sample_goose_pdu(); |
| 150 | + |
| 151 | + c.bench_function("goose_pdu_encode", |b| { |
| 152 | + b.iter(|| encode_goose(black_box(&header), black_box(&pdu))); |
| 153 | + }); |
| 154 | +} |
| 155 | + |
| 156 | +fn benchmark_encode_decode_roundtrip(c: &mut Criterion) { |
| 157 | + let header = create_sample_ethernet_header(); |
| 158 | + let pdu = create_sample_goose_pdu(); |
| 159 | + |
| 160 | + c.bench_function("goose_encode_decode_roundtrip", |b| { |
| 161 | + b.iter(|| { |
| 162 | + // Encode |
| 163 | + let encoded = encode_goose(black_box(&header), black_box(&pdu)).unwrap(); |
| 164 | + |
| 165 | + // Decode |
| 166 | + let mut decoded_header = EthernetHeader::default(); |
| 167 | + let pos = decode_ethernet_header(black_box(&mut decoded_header), black_box(&encoded)); |
| 168 | + decode_goose_pdu(black_box(&encoded), black_box(pos)) |
| 169 | + }); |
| 170 | + }); |
| 171 | +} |
| 172 | + |
| 173 | +fn benchmark_goose_with_different_data_sizes(c: &mut Criterion) { |
| 174 | + let mut group = c.benchmark_group("goose_data_size"); |
| 175 | + |
| 176 | + // Test realistic data sizes from small to large (approaching MTU limit) |
| 177 | + // Typical GOOSE: 10-200 data points |
| 178 | + // Ethernet MTU: ~1500 bytes (including headers) |
| 179 | + for num_elements in [10, 50, 100, 150, 200].iter() { |
| 180 | + let header = create_sample_ethernet_header(); |
| 181 | + let mut pdu = create_sample_goose_pdu(); |
| 182 | + |
| 183 | + // Create mixed data with specified number of elements |
| 184 | + // Mix of different data types to be realistic |
| 185 | + pdu.all_data = (0..*num_elements) |
| 186 | + .map(|i| match i % 5 { |
| 187 | + 0 => IECData::Boolean(i % 2 == 0), |
| 188 | + 1 => IECData::Int((i * 1000) as i64), |
| 189 | + 2 => IECData::UInt(0xC000 + i as u64), |
| 190 | + 3 => IECData::Float(i as f64 * 1.5), |
| 191 | + _ => IECData::VisibleString(format!("DATA_{:03}", i)), |
| 192 | + }) |
| 193 | + .collect(); |
| 194 | + pdu.num_dat_set_entries = *num_elements; |
| 195 | + |
| 196 | + group.bench_with_input( |
| 197 | + BenchmarkId::new("encode", num_elements), |
| 198 | + num_elements, |
| 199 | + |b, _| { |
| 200 | + b.iter(|| encode_goose(black_box(&header), black_box(&pdu))); |
| 201 | + }, |
| 202 | + ); |
| 203 | + } |
| 204 | + |
| 205 | + group.finish(); |
| 206 | +} |
| 207 | + |
| 208 | +fn benchmark_goose_rates(c: &mut Criterion) { |
| 209 | + let packet = create_large_goose_packet(); |
| 210 | + let mut group = c.benchmark_group("goose_packet_rates"); |
| 211 | + |
| 212 | + // GOOSE typical rates (much slower than SMV) |
| 213 | + for rate_hz in [50, 100, 1000].iter() { |
| 214 | + group.bench_with_input( |
| 215 | + BenchmarkId::new("decode_rate_Hz", rate_hz), |
| 216 | + rate_hz, |
| 217 | + |b, _| { |
| 218 | + b.iter(|| { |
| 219 | + let mut header = EthernetHeader::default(); |
| 220 | + let pos = decode_ethernet_header(black_box(&mut header), black_box(&packet)); |
| 221 | + decode_goose_pdu(black_box(&packet), black_box(pos)) |
| 222 | + }); |
| 223 | + }, |
| 224 | + ); |
| 225 | + |
| 226 | + group.throughput(criterion::Throughput::Elements(*rate_hz as u64)); |
| 227 | + } |
| 228 | + |
| 229 | + group.finish(); |
| 230 | +} |
| 231 | + |
| 232 | +criterion_group!( |
| 233 | + benches, |
| 234 | + benchmark_goose_frame_detection, |
| 235 | + benchmark_ethernet_header_decode, |
| 236 | + benchmark_goose_pdu_decode, |
| 237 | + benchmark_full_goose_decode, |
| 238 | + benchmark_ethernet_header_encode, |
| 239 | + benchmark_goose_pdu_encode, |
| 240 | + benchmark_encode_decode_roundtrip, |
| 241 | + benchmark_goose_with_different_data_sizes, |
| 242 | + benchmark_goose_rates |
| 243 | +); |
| 244 | +criterion_main!(benches); |
0 commit comments