1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{
7 MAX_PLUGIN_DIAGNOSTICS, MAX_PLUGIN_EVENT_ID_BYTES, MAX_PLUGIN_EVENT_NAME_BYTES,
8 MAX_PLUGIN_MEASUREMENTS, MAX_PLUGIN_PLATFORM_BYTES, MAX_PLUGIN_PROTOCOL_BYTES,
9 MAX_PLUGIN_THREAD_BYTES, PluginDiagnostic, PluginMeasurement, PluginProtocolViolation,
10 protocol::{validate_attributes, validate_optional_text, validate_text},
11};
12
13pub const MAX_BENCHMARK_BATCH_EVENTS: usize = 512;
14pub const MAX_BENCHMARK_THRESHOLD_VIOLATIONS: usize = 128;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct BenchmarkEvent {
20 pub run_id: String,
21 pub session_id: String,
22 pub platform: String,
23 pub source_protocol: Option<String>,
24 pub event_name: String,
25 pub timestamp_ns: u64,
26 pub elapsed_ns: u64,
27 pub thread: Option<String>,
28 #[serde(default)]
29 pub attributes: BTreeMap<String, String>,
30}
31
32impl BenchmarkEvent {
33 pub fn validate(&self) -> Result<(), BenchmarkSinkError> {
34 validate_text("benchmark.run_id", &self.run_id, MAX_PLUGIN_EVENT_ID_BYTES)?;
35 validate_text(
36 "benchmark.session_id",
37 &self.session_id,
38 MAX_PLUGIN_EVENT_ID_BYTES,
39 )?;
40 validate_text(
41 "benchmark.platform",
42 &self.platform,
43 MAX_PLUGIN_PLATFORM_BYTES,
44 )?;
45 validate_optional_text(
46 "benchmark.source_protocol",
47 self.source_protocol.as_deref(),
48 MAX_PLUGIN_PROTOCOL_BYTES,
49 )?;
50 validate_text(
51 "benchmark.event_name",
52 &self.event_name,
53 MAX_PLUGIN_EVENT_NAME_BYTES,
54 )?;
55 validate_optional_text(
56 "benchmark.thread",
57 self.thread.as_deref(),
58 MAX_PLUGIN_THREAD_BYTES,
59 )?;
60 validate_attributes(&self.attributes)?;
61 Ok(())
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct BenchmarkEventBatch {
69 pub events: Vec<BenchmarkEvent>,
70}
71
72impl BenchmarkEventBatch {
73 pub fn validate(&self) -> Result<(), BenchmarkSinkError> {
74 if self.events.len() > MAX_BENCHMARK_BATCH_EVENTS {
75 return Err(BenchmarkSinkError::ProtocolViolation(format!(
76 "benchmark batch exceeds the {MAX_BENCHMARK_BATCH_EVENTS}-event protocol limit"
77 )));
78 }
79 for event in &self.events {
80 event.validate()?;
81 }
82 Ok(())
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct BenchmarkSinkStatus {
90 pub accepted_events: u64,
91}
92
93impl BenchmarkSinkStatus {
94 pub fn validate_for_batch(&self, batch_event_count: usize) -> Result<(), BenchmarkSinkError> {
95 let batch_event_count = u64::try_from(batch_event_count).map_err(|_| {
96 BenchmarkSinkError::ProtocolViolation(
97 "benchmark batch size cannot be represented by the protocol".to_owned(),
98 )
99 })?;
100 if self.accepted_events > batch_event_count {
101 return Err(BenchmarkSinkError::ProtocolViolation(format!(
102 "benchmark sink accepted {} events from a {batch_event_count}-event batch",
103 self.accepted_events
104 )));
105 }
106 Ok(())
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct BenchmarkThresholdViolation {
113 pub measurement: String,
114 pub actual: f64,
115 pub threshold: f64,
116 pub comparison: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct BenchmarkSinkReport {
123 pub accepted_events: u64,
124 pub dropped_events: u64,
125 #[serde(default)]
126 pub measurements: Vec<PluginMeasurement>,
127 #[serde(default)]
128 pub threshold_violations: Vec<BenchmarkThresholdViolation>,
129 #[serde(default)]
130 pub diagnostics: Vec<PluginDiagnostic>,
131}
132
133impl BenchmarkSinkReport {
134 pub fn validate(&self) -> Result<(), BenchmarkSinkError> {
135 if self.measurements.len() > MAX_PLUGIN_MEASUREMENTS {
136 return Err(BenchmarkSinkError::ProtocolViolation(format!(
137 "benchmark report exceeds the {MAX_PLUGIN_MEASUREMENTS}-measurement protocol limit"
138 )));
139 }
140 if self.diagnostics.len() > MAX_PLUGIN_DIAGNOSTICS {
141 return Err(BenchmarkSinkError::ProtocolViolation(format!(
142 "benchmark report exceeds the {MAX_PLUGIN_DIAGNOSTICS}-diagnostic protocol limit"
143 )));
144 }
145 if self.threshold_violations.len() > MAX_BENCHMARK_THRESHOLD_VIOLATIONS {
146 return Err(BenchmarkSinkError::ProtocolViolation(format!(
147 "benchmark report exceeds the {MAX_BENCHMARK_THRESHOLD_VIOLATIONS}-threshold-violation protocol limit"
148 )));
149 }
150 for measurement in &self.measurements {
151 measurement.validate()?;
152 }
153 for diagnostic in &self.diagnostics {
154 diagnostic.validate()?;
155 }
156 for violation in &self.threshold_violations {
157 validate_text(
158 "threshold_violation.measurement",
159 &violation.measurement,
160 MAX_PLUGIN_EVENT_NAME_BYTES,
161 )?;
162 validate_text(
163 "threshold_violation.comparison",
164 &violation.comparison,
165 MAX_PLUGIN_EVENT_NAME_BYTES,
166 )?;
167 if !violation.actual.is_finite() || !violation.threshold.is_finite() {
168 return Err(BenchmarkSinkError::ProtocolViolation(format!(
169 "threshold violation `{}` contains a non-finite value",
170 violation.measurement
171 )));
172 }
173 }
174 Ok(())
175 }
176}
177
178#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase", tag = "code", content = "message")]
181pub enum BenchmarkSinkError {
182 #[error("payload codec error: {0}")]
183 PayloadCodec(String),
184 #[error("plugin ABI violation: {0}")]
185 AbiViolation(String),
186 #[error("sink failed: {0}")]
187 SinkFailed(String),
188 #[error("sink protocol violation: {0}")]
189 ProtocolViolation(String),
190}
191
192impl From<PluginProtocolViolation> for BenchmarkSinkError {
193 fn from(value: PluginProtocolViolation) -> Self {
194 Self::ProtocolViolation(value.to_string())
195 }
196}
197
198pub trait BenchmarkSink: Send + Sync {
199 fn name(&self) -> &str;
200
201 fn on_event_batch(
202 &self,
203 batch: &BenchmarkEventBatch,
204 ) -> Result<BenchmarkSinkStatus, BenchmarkSinkError>;
205
206 fn flush(&self) -> Result<BenchmarkSinkReport, BenchmarkSinkError> {
207 Ok(BenchmarkSinkReport::default())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn batch_limit_is_part_of_the_protocol() {
217 let event = BenchmarkEvent {
218 run_id: "run".to_owned(),
219 session_id: "session".to_owned(),
220 platform: "test".to_owned(),
221 source_protocol: None,
222 event_name: "tick".to_owned(),
223 timestamp_ns: 0,
224 elapsed_ns: 0,
225 thread: None,
226 attributes: BTreeMap::new(),
227 };
228 let batch = BenchmarkEventBatch {
229 events: vec![event; MAX_BENCHMARK_BATCH_EVENTS + 1],
230 };
231 assert!(matches!(
232 batch.validate(),
233 Err(BenchmarkSinkError::ProtocolViolation(_))
234 ));
235 }
236
237 #[test]
238 fn sink_status_cannot_accept_more_events_than_the_input_batch() {
239 let status = BenchmarkSinkStatus { accepted_events: 2 };
240 assert!(matches!(
241 status.validate_for_batch(1),
242 Err(BenchmarkSinkError::ProtocolViolation(_))
243 ));
244 }
245
246 #[test]
247 fn benchmark_events_validate_all_transport_text_fields() {
248 let event = BenchmarkEvent {
249 run_id: String::new(),
250 session_id: "session".to_owned(),
251 platform: "test".to_owned(),
252 source_protocol: None,
253 event_name: "tick".to_owned(),
254 timestamp_ns: 0,
255 elapsed_ns: 0,
256 thread: None,
257 attributes: BTreeMap::new(),
258 };
259
260 assert!(matches!(
261 event.validate(),
262 Err(BenchmarkSinkError::ProtocolViolation(message))
263 if message.contains("benchmark.run_id")
264 ));
265 }
266
267 #[test]
268 fn benchmark_reports_bound_threshold_violations() {
269 let violation = BenchmarkThresholdViolation {
270 measurement: "latency".to_owned(),
271 actual: 2.0,
272 threshold: 1.0,
273 comparison: "greater-than".to_owned(),
274 };
275 let report = BenchmarkSinkReport {
276 threshold_violations: vec![violation; MAX_BENCHMARK_THRESHOLD_VIOLATIONS + 1],
277 ..BenchmarkSinkReport::default()
278 };
279
280 assert!(matches!(
281 report.validate(),
282 Err(BenchmarkSinkError::ProtocolViolation(message))
283 if message.contains("threshold-violation")
284 ));
285 }
286}