Skip to content

Commit c0ed6fc

Browse files
test: add banchmark tests
1 parent f8e6be8 commit c0ed6fc

File tree

5 files changed

+716
-1
lines changed

5 files changed

+716
-1
lines changed

.github/workflows/ci.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- '**' # Run on push to any branch
7+
pull_request:
8+
branches:
9+
- main # Run on PRs targeting main
10+
11+
env:
12+
CARGO_TERM_COLOR: always
13+
14+
jobs:
15+
test:
16+
name: Test Suite
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Install Rust toolchain
23+
uses: dtolnay/rust-toolchain@stable
24+
25+
- name: Cache cargo registry
26+
uses: actions/cache@v4
27+
with:
28+
path: ~/.cargo/registry
29+
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
30+
31+
- name: Cache cargo index
32+
uses: actions/cache@v4
33+
with:
34+
path: ~/.cargo/git
35+
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
36+
37+
- name: Cache cargo build
38+
uses: actions/cache@v4
39+
with:
40+
path: target
41+
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
42+
43+
- name: Run tests
44+
run: cargo test --all-features --verbose
45+
46+
- name: Check formatting
47+
run: cargo fmt -- --check
48+
49+
- name: Run clippy
50+
run: cargo clippy --all-features -- -D warnings
51+
52+
benchmark:
53+
name: Benchmark Suite
54+
runs-on: ubuntu-latest
55+
# Only run benchmarks on PRs to main
56+
if: github.event_name == 'pull_request' && github.base_ref == 'main'
57+
steps:
58+
- name: Checkout repository
59+
uses: actions/checkout@v4
60+
61+
- name: Install Rust toolchain
62+
uses: dtolnay/rust-toolchain@stable
63+
64+
- name: Cache cargo registry
65+
uses: actions/cache@v4
66+
with:
67+
path: ~/.cargo/registry
68+
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
69+
70+
- name: Cache cargo index
71+
uses: actions/cache@v4
72+
with:
73+
path: ~/.cargo/git
74+
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
75+
76+
- name: Cache cargo build
77+
uses: actions/cache@v4
78+
with:
79+
path: target
80+
key: ${{ runner.os }}-cargo-bench-target-${{ hashFiles('**/Cargo.lock') }}
81+
82+
- name: Run GOOSE benchmarks
83+
run: cargo bench --bench goose_codec -- --output-format bencher | tee goose_output.txt
84+
85+
- name: Comment benchmark results
86+
uses: actions/github-script@v7
87+
with:
88+
script: |
89+
const fs = require('fs');
90+
const gooseOutput = fs.readFileSync('goose_output.txt', 'utf8');
91+
92+
const body = `## Benchmark Results
93+
94+
### GOOSE Codec Benchmarks
95+
\`\`\`
96+
${gooseOutput}
97+
\`\`\`
98+
`;
99+
100+
github.rest.issues.createComment({
101+
issue_number: context.issue.number,
102+
owner: context.repo.owner,
103+
repo: context.repo.repo,
104+
body: body
105+
});

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ rasn = "0.18"
1111
hex = "0.4.3"
1212

1313
[dev-dependencies]
14-
# Add test dependencies here if needed
14+
criterion = { version = "0.5", features = ["html_reports"] }
15+
16+
[[bench]]
17+
name = "goose_codec"
18+
harness = false

benches/goose_codec.rs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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

Comments
 (0)