1use {
24 crate::ids::{MessageCount, TimeOffsetMs, WallclockMs},
25 krabka_units::{prelude::*, serde_units},
26 serde::{Deserialize, Serialize},
27};
28
29pub(crate) mod bounded {
44
45 krabka_macros::bounded_unit_modules! {
46 pub mod positive_size: ByteSize [codec = krabka_units::serde_units::human::byte_size, bound = "positive", requirement = "a positive size"];
48
49 pub mod nonnegative_size: ByteSize [codec = krabka_units::serde_units::human::byte_size, bound = "nonnegative", requirement = "a size of zero or more"];
51
52 pub mod positive_time: Time [codec = krabka_units::serde_units::human::time, bound = "positive", requirement = "a positive extent"];
54
55 pub mod nonnegative_time: Time [codec = krabka_units::serde_units::human::time, bound = "nonnegative", requirement = "an extent of zero or more"];
58
59 pub mod positive_rate: Frequency [codec = krabka_units::serde_units::human::frequency, bound = "positive", requirement = "a positive rate"];
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
69#[serde(rename_all = "lowercase")]
70pub enum Stack {
71 Krabka,
72 Kafka,
73}
74
75impl Stack {
76 #[must_use]
79 pub fn as_str(self) -> &'static str {
80 match self {
81 Self::Krabka => "krabka",
82 Self::Kafka => "kafka",
83 }
84 }
85
86 #[must_use]
90 pub fn broker_pod_regex(self) -> &'static str {
91 match self {
92 Self::Krabka => "^demo-broker",
98 Self::Kafka => "^demo-kafka-",
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum ModeTag {
108 Ci,
110 Cluster,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "lowercase")]
116pub enum Acks {
117 None,
118 Leader,
119 All,
120}
121
122impl Acks {
123 pub(crate) fn into_producer(self) -> krabka_client_producer::Acks {
124 match self {
125 Self::None => krabka_client_producer::Acks::Zero,
126 Self::Leader => krabka_client_producer::Acks::One,
127 Self::All => krabka_client_producer::Acks::All,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "lowercase")]
134#[derive(Default)]
135pub enum Compression {
136 #[default]
137 None,
138 Gzip,
139 Snappy,
140 Lz4,
141 Zstd,
142}
143
144impl Compression {
145 pub(crate) fn into_producer(self) -> krabka_client_producer::Compression {
146 match self {
147 Self::None => krabka_client_producer::Compression::None,
148 Self::Gzip => krabka_client_producer::Compression::Gzip,
149 Self::Snappy => krabka_client_producer::Compression::Snappy,
150 Self::Lz4 => krabka_client_producer::Compression::Lz4,
151 Self::Zstd => krabka_client_producer::Compression::Zstd,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157#[serde(tag = "kind", rename_all = "snake_case")]
158pub enum LoadMode {
159 Saturate,
161 FixedRate {
164 #[serde(with = "bounded::positive_rate")]
165 rate: Frequency,
166 },
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct FailoverSpec {
173 #[serde(with = "bounded::nonnegative_time")]
176 pub kill_after: Time,
177 #[serde(default = "default_failover_target")]
181 pub target: String,
182}
183
184fn default_failover_target() -> String { "partition0_leader".to_owned() }
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct Scenario {
199 pub name: String,
200 #[serde(default = "default_mode_tag")]
201 pub mode_tag: ModeTag,
202 #[serde(default = "default_msg_size", with = "bounded::positive_size")]
206 pub msg_size: ByteSize,
207 #[serde(default, with = "bounded::nonnegative_size")]
209 pub key_size: ByteSize,
210 #[serde(default = "default_partitions")]
211 pub partitions: i32,
212 #[serde(default = "default_replicas")]
213 pub replication_factor: i16,
214 #[serde(default = "default_producers")]
215 pub producers: usize,
216 #[serde(default = "default_consumers")]
217 pub consumers: usize,
218 pub mode: LoadMode,
219 #[serde(default = "default_acks")]
220 pub acks: Acks,
221 #[serde(default)]
222 pub compression: Compression,
223 #[serde(default = "default_linger", with = "bounded::nonnegative_time")]
226 pub linger: Time,
227 #[serde(default = "default_batch_size", with = "bounded::positive_size")]
229 pub batch_size: ByteSize,
230 #[serde(default = "default_duration", with = "bounded::positive_time")]
233 pub duration: Time,
234 #[serde(default = "default_warmup", with = "bounded::nonnegative_time")]
237 pub warmup: Time,
238 #[serde(default)]
239 pub failover: Option<FailoverSpec>,
240}
241
242fn default_mode_tag() -> ModeTag { ModeTag::Ci }
243fn default_msg_size() -> ByteSize { kibibytes(1) }
244fn default_partitions() -> i32 { 6 }
245fn default_replicas() -> i16 { 1 }
246fn default_producers() -> usize { 1 }
247fn default_consumers() -> usize { 1 }
248fn default_acks() -> Acks { Acks::Leader }
249fn default_linger() -> Time { millis(5) }
250fn default_batch_size() -> ByteSize { kibibytes(16) }
251fn default_duration() -> Time { secs(60) }
252fn default_warmup() -> Time { secs(10) }
253
254#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
262pub struct LatencyPercentiles {
263 #[serde(with = "serde_units::numeric::nanos_i64")]
264 pub p50: Time,
265 #[serde(with = "serde_units::numeric::nanos_i64")]
266 pub p95: Time,
267 #[serde(with = "serde_units::numeric::nanos_i64")]
268 pub p99: Time,
269 #[serde(with = "serde_units::numeric::nanos_i64")]
270 pub p999: Time,
271 #[serde(with = "serde_units::numeric::nanos_i64")]
272 pub max: Time,
273 #[serde(with = "serde_units::numeric::nanos_i64")]
274 pub mean: Time,
275 pub count: u64,
276}
277
278#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
279pub struct Throughput {
280 pub msgs_produced: MessageCount,
281 pub msgs_consumed: MessageCount,
282 #[serde(with = "serde_units::numeric::bytes_u64")]
284 pub bytes_in: ByteSize,
285 #[serde(with = "serde_units::numeric::bytes_u64")]
287 pub bytes_out: ByteSize,
288 #[serde(with = "serde_units::human::frequency")]
289 pub producer_rate: Frequency,
290 #[serde(with = "serde_units::human::frequency")]
291 pub consumer_rate: Frequency,
292}
293
294#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
295pub struct Resource {
296 #[serde(with = "serde_units::numeric::nanos_i64")]
298 pub broker_cpu: Time,
299 #[serde(with = "serde_units::numeric::bytes_u64")]
300 pub mem_cgroup_working_set: ByteSize,
301 #[serde(
303 default,
304 skip_serializing_if = "Option::is_none",
305 with = "serde_units::numeric::option_bytes_u64"
306 )]
307 pub jvm_heap_used: Option<ByteSize>,
308 #[serde(
310 default,
311 skip_serializing_if = "Option::is_none",
312 with = "serde_units::numeric::option_bytes_u64"
313 )]
314 pub jvm_nonheap_used: Option<ByteSize>,
315 #[serde(
319 default,
320 skip_serializing_if = "Option::is_none",
321 with = "serde_units::numeric::option_bytes_i64"
322 )]
323 pub kafka_page_cache_approx: Option<ByteSize>,
324 #[serde(with = "serde_units::human::frequency")]
326 pub msgs_per_cpu_second: Frequency,
327}
328
329#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
330pub struct Disturbance {
331 pub kill_at_ms: TimeOffsetMs,
334 pub recovery_at_ms: TimeOffsetMs,
336 pub dropped: MessageCount,
337 #[serde(with = "serde_units::numeric::nanos_i64")]
338 pub latency_spike_max: Time,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct Topology {
343 pub partitions: i32,
344 pub replication_factor: i16,
345 pub broker_count: u32,
346}
347
348#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
356pub struct Sample {
357 pub t_offset_ms: TimeOffsetMs,
359 #[serde(with = "serde_units::human::frequency")]
360 pub producer_rate: Frequency,
361 #[serde(with = "serde_units::human::frequency")]
362 pub consumer_rate: Frequency,
363 #[serde(with = "serde_units::numeric::nanos_i64")]
365 pub producer_p50: Time,
366 #[serde(with = "serde_units::numeric::nanos_i64")]
367 pub producer_p99: Time,
368 #[serde(with = "serde_units::numeric::nanos_i64")]
370 pub consumer_e2e_p99: Time,
371}
372
373#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
379pub struct BrokerSample {
380 pub t_offset_ms: TimeOffsetMs,
382 pub cpu_cores: f64,
385 #[serde(with = "serde_units::numeric::bytes_u64")]
387 pub mem_working_set: ByteSize,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
393pub struct RunOutput {
394 pub scenario: Scenario,
395 pub stack: Stack,
396 pub topology: Topology,
397 pub wallclock_start_unix_ms: WallclockMs,
398 pub wallclock_end_unix_ms: WallclockMs,
399 pub throughput: Throughput,
400 pub producer_latency: LatencyPercentiles,
401 pub consumer_e2e_latency: LatencyPercentiles,
402 pub resource: Resource,
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub disturbance: Option<Disturbance>,
405 #[serde(
409 default,
410 skip_serializing_if = "Option::is_none",
411 with = "serde_units::numeric::option_millis_i64"
412 )]
413 pub startup: Option<Time>,
414 #[serde(with = "serde_units::numeric::millis_i64")]
417 pub first_ack: Time,
418 #[serde(default)]
419 pub errors: Vec<String>,
420 #[serde(default)]
421 pub notes: Vec<String>,
422 #[serde(default)]
425 pub samples: Vec<Sample>,
426 #[serde(default)]
429 pub broker_samples: Vec<BrokerSample>,
430}
431
432#[cfg(test)]
433mod tests {
434 use {super::*, assert2::check};
435
436 fn scenario() -> Scenario {
439 Scenario {
440 name: "round-trip".into(),
441 mode_tag: ModeTag::Cluster,
442 msg_size: kibibytes(4),
443 key_size: bytes(16),
444 partitions: 12,
445 replication_factor: 3,
446 producers: 2,
447 consumers: 4,
448 mode: LoadMode::FixedRate {
449 rate: per_sec(20_000),
450 },
451 acks: Acks::All,
452 compression: Compression::Zstd,
453 linger: millis(5),
454 batch_size: kibibytes(16),
455 duration: secs(60),
456 warmup: secs(10),
457 failover: Some(FailoverSpec {
458 kill_after: secs(4),
459 target: "partition0_leader".into(),
460 }),
461 }
462 }
463
464 krabka_macros::benchmark_latency_fixture!(sample_latency);
465
466 fn run_output() -> RunOutput {
467 RunOutput {
468 scenario: scenario(),
469 stack: Stack::Krabka,
470 topology: Topology {
471 partitions: 12,
472 replication_factor: 3,
473 broker_count: 3,
474 },
475 wallclock_start_unix_ms: WallclockMs(1_700_000_000_000),
476 wallclock_end_unix_ms: WallclockMs(1_700_000_060_000),
477 throughput: Throughput {
478 msgs_produced: MessageCount(600_000),
479 msgs_consumed: MessageCount(599_000),
480 bytes_in: mebibytes(2400),
481 bytes_out: mebibytes(2396),
482 producer_rate: per_sec(10_000),
483 consumer_rate: per_sec(9_983),
484 },
485 producer_latency: sample_latency(),
486 consumer_e2e_latency: LatencyPercentiles::default(),
487 resource: Resource {
488 broker_cpu: millis(123_456),
489 mem_cgroup_working_set: mebibytes(300),
490 jvm_heap_used: Some(mebibytes(1024)),
491 jvm_nonheap_used: Some(mebibytes(96)),
492 kafka_page_cache_approx: Some(mebibytes(200)),
493 msgs_per_cpu_second: per_sec(4_860),
494 },
495 disturbance: Some(Disturbance {
496 kill_at_ms: TimeOffsetMs(1_700_000_004_000),
497 recovery_at_ms: TimeOffsetMs(1_700_000_006_000),
498 dropped: MessageCount(7),
499 latency_spike_max: micros(42_500),
500 }),
501 startup: Some(millis(1234)),
502 first_ack: millis(42),
503 errors: vec!["an-error".into()],
504 notes: vec!["a-note".into()],
505 samples: vec![Sample {
506 t_offset_ms: TimeOffsetMs(2000),
507 producer_rate: per_sec(10_100),
508 consumer_rate: per_sec(9_900),
509 producer_p50: micros(1500),
510 producer_p99: micros(4200),
511 consumer_e2e_p99: millis(7),
512 }],
513 broker_samples: vec![BrokerSample {
514 t_offset_ms: TimeOffsetMs(0),
515 cpu_cores: 2.5,
516 mem_working_set: mebibytes(300),
517 }],
518 }
519 }
520
521 #[test]
522 fn stack_broker_pod_regex_distinguishes_stacks() {
523 assert2::assert!(Stack::Krabka.broker_pod_regex() == "^demo-broker");
524 assert2::assert!(Stack::Kafka.broker_pod_regex() == "^demo-kafka-");
525 let p = Stack::Krabka.broker_pod_regex().trim_start_matches('^');
528 assert2::assert!("demo-brokers-0".starts_with(p));
529 assert2::assert!("demo-broker-0-0".starts_with(p));
530 }
531
532 #[yare::parameterized(
533 none = { Acks::None, krabka_client_producer::Acks::Zero },
534 leader = { Acks::Leader, krabka_client_producer::Acks::One },
535 all = { Acks::All, krabka_client_producer::Acks::All },
536 )]
537 fn acks_map_to_producer_enum(acks: Acks, want: krabka_client_producer::Acks) {
538 assert2::assert!(acks.into_producer() == want);
539 }
540
541 #[yare::parameterized(
542 none = { Compression::None, krabka_client_producer::Compression::None },
543 gzip = { Compression::Gzip, krabka_client_producer::Compression::Gzip },
544 snappy = { Compression::Snappy, krabka_client_producer::Compression::Snappy },
545 lz4 = { Compression::Lz4, krabka_client_producer::Compression::Lz4 },
546 zstd = { Compression::Zstd, krabka_client_producer::Compression::Zstd },
547 )]
548 fn compression_maps_to_producer_enum(
549 compression: Compression,
550 want: krabka_client_producer::Compression,
551 ) {
552 assert2::assert!(compression.into_producer() == want);
553 }
554
555 #[test]
556 fn compression_default_is_none() {
557 assert2::assert!(Compression::default() == Compression::None);
558 }
559
560 #[test]
561 fn scenario_yaml_reads_the_operator_form() {
562 let y = "
563name: small-msg-saturate
564mode_tag: ci
565msg_size: 100B
566partitions: 6
567replication_factor: 1
568producers: 1
569consumers: 1
570mode:
571 kind: saturate
572acks: leader
573compression: none
574linger: 5ms
575batch_size: 16KiB
576duration: 60s
577warmup: 10s
578";
579 let s: Scenario = serde_saphyr::from_str(y).expect("parse");
580 check!(s.name.as_str() == "small-msg-saturate");
581 check!(s.partitions == 6);
582 check!(s.msg_size == bytes(100));
583 check_default_batch_timing(&s);
584 check!(matches!(s.mode, LoadMode::Saturate));
585 }
586
587 fn check_default_batch_timing(s: &Scenario) {
588 check!(s.key_size == ByteSize::ZERO);
589 check!(s.linger == millis(5));
590 check!(s.batch_size == kibibytes(16));
591 check!(s.duration == secs(60));
592 check!(s.warmup == secs(10));
593 }
594
595 #[test]
596 fn scenario_yaml_defaults_every_omitted_field() {
597 let y = "
598name: bare
599mode:
600 kind: saturate
601";
602 let s: Scenario = serde_saphyr::from_str(y).expect("parse");
603 check!(s.msg_size == kibibytes(1));
604 check_default_batch_timing(&s);
605 check!(s.failover == None);
606 }
607
608 #[test]
609 fn scenario_yaml_rejects_a_size_without_a_unit() {
610 let y = "
611name: unitless
612msg_size: 100
613mode:
614 kind: saturate
615";
616 let error = serde_saphyr::from_str::<Scenario>(y).expect_err("a bare number is not a size");
617 check!(error.to_string().contains("missing unit"));
618 }
619
620 fn parse_saturate_with(extra: &str) -> Result<Scenario, serde_saphyr::Error> {
623 serde_saphyr::from_str(&format!("name: bounds\nmode:\n kind: saturate\n{extra}\n"))
624 }
625
626 #[yare::parameterized(
627 negative_message_size = { "msg_size: -1B", "a positive size" },
628 zero_message_size = { "msg_size: 0", "a positive size" },
629 negative_key_size = { "key_size: -1B", "a size of zero or more" },
630 negative_batch_size = { "batch_size: -16KiB", "a positive size" },
631 zero_batch_size = { "batch_size: 0", "a positive size" },
632 negative_linger = { "linger: -5ms", "an extent of zero or more" },
633 negative_duration = { "duration: -60s", "a positive extent" },
634 zero_duration = { "duration: 0", "a positive extent" },
635 negative_warmup = { "warmup: -10s", "an extent of zero or more" },
636 negative_failover_kill_after = {
637 "failover:\n kill_after: -4s",
638 "an extent of zero or more",
639 },
640 )]
641 fn scenario_yaml_rejects_unrunnable_magnitudes(field: &str, requirement: &str) {
642 let error = parse_saturate_with(field)
645 .expect_err("an unrunnable magnitude must fail at load, not at run");
646 check!(error.to_string().contains(requirement), "{field:?}");
647 }
648
649 #[yare::parameterized(
650 negative = { "\"-1/s\"" },
651 zero = { "\"0\"" },
652 )]
653 fn scenario_yaml_rejects_an_unrunnable_fixed_rate(rate: &str) {
654 let yaml = format!("name: bounds\nmode:\n kind: fixed_rate\n rate: {rate}\n");
658 let error = serde_saphyr::from_str::<Scenario>(&yaml)
659 .expect_err("a paced producer needs a positive rate");
660 check!(error.to_string().contains("a positive rate"), "{rate}");
661 }
662
663 #[test]
664 fn scenario_yaml_admits_zero_where_zero_is_runnable() {
665 let s =
666 parse_saturate_with("key_size: 0\nlinger: 0\nwarmup: 0\nfailover:\n kill_after: 0")
667 .expect("keyless records, no linger, no warmup, and an immediate kill all run");
668 check!(s.key_size == ByteSize::ZERO);
669 check!(s.linger == Time::ZERO);
670 check!(s.warmup == Time::ZERO);
671 check!(s.failover.map(|failover| failover.kill_after) == Some(Time::ZERO));
672 }
673
674 #[test]
675 fn fixed_rate_yaml_parses_an_event_rate() {
676 let y = "
677name: fixed-rate
678mode:
679 kind: fixed_rate
680 rate: 20000/s
681";
682 let s: Scenario = serde_saphyr::from_str(y).unwrap();
683 check!(
684 s.mode
685 == LoadMode::FixedRate {
686 rate: per_sec(20_000)
687 }
688 );
689 }
690
691 #[test]
692 fn failover_yaml_parses_a_kill_offset() {
693 let y = "
694name: failover
695mode:
696 kind: saturate
697failover:
698 kill_after: 4s
699";
700 let s: Scenario = serde_saphyr::from_str(y).unwrap();
701 check!(
702 s.failover
703 == Some(FailoverSpec {
704 kill_after: secs(4),
705 target: "partition0_leader".into(),
706 })
707 );
708 }
709
710 #[test]
711 fn scenario_yaml_round_trips_through_the_human_form() {
712 let yaml = serde_saphyr::to_string(&scenario()).expect("encode");
713 for needle in ["msg_size: 4KiB", "linger: 5ms", "rate: 20000/s"] {
715 check!(yaml.contains(needle));
716 }
717 let back: Scenario = serde_saphyr::from_str(&yaml).expect("decode");
718 check!(back == scenario());
719 }
720
721 #[test]
722 fn run_output_json_round_trips() {
723 let out = run_output();
724 let json = serde_json::to_string(&out).expect("encode");
725 let back: RunOutput = serde_json::from_str(&json).expect("decode");
726 check!(back == out);
727 }
728
729 #[test]
730 fn run_output_json_encodes_measurements_as_exact_integers() {
731 let json = serde_json::to_value(run_output()).expect("encode");
732 check!(json["producer_latency"]["p99"] == serde_json::json!(4_250_000));
733 check!(json["throughput"]["bytes_in"] == serde_json::json!(2_516_582_400_u64));
734 check!(json["throughput"]["producer_rate"] == serde_json::json!("10000/s"));
735 check!(json["resource"]["mem_cgroup_working_set"] == serde_json::json!(314_572_800));
736 check!(json["first_ack"] == serde_json::json!(42));
737 check!(json["startup"] == serde_json::json!(1234));
738 }
739
740 #[test]
741 fn absent_optional_resource_fields_decode_as_none() {
742 let mut out = run_output();
743 out.resource.jvm_heap_used = None;
744 out.resource.jvm_nonheap_used = None;
745 out.resource.kafka_page_cache_approx = None;
746 out.startup = None;
747 let json = serde_json::to_string(&out).expect("encode");
748 check!(!json.contains("jvm_heap_used"));
749 check!(!json.contains("startup"));
750 let back: RunOutput = serde_json::from_str(&json).expect("decode");
751 check!(back == out);
752 }
753}
754
755#[cfg(test)]
756pub(crate) mod fixture {
757 use super::*;
758 krabka_macros::benchmark_scenario_fixture!(default_scenario);
759
760 pub fn scenario(name: &str) -> Scenario { default_scenario(name, krabka_units::bytes(100)) }
761
762 pub struct RunSetup<'a> {
763 pub scenario: &'a str,
764 pub stack: Stack,
765 pub topology: Topology,
766 pub producer_rate: Frequency,
767 pub samples: Vec<Sample>,
768 pub broker_samples: Vec<BrokerSample>,
769 pub p99: Time,
770 }
771
772 impl Default for RunSetup<'_> {
773 fn default() -> Self {
774 Self {
775 scenario: "small-msg",
776 stack: Stack::Krabka,
777 topology: Topology {
778 partitions: 100,
779 replication_factor: 3,
780 broker_count: 6,
781 },
782 producer_rate: krabka_units::per_sec(1000),
783 samples: vec![],
784 broker_samples: vec![],
785 p99: Time::ZERO,
786 }
787 }
788 }
789
790 #[bon::builder]
791 pub fn empty_run(
792 #[builder(default = RunSetup::default().scenario)] scenario: &'_ str,
793 #[builder(default = RunSetup::default().stack)] stack: Stack,
794 #[builder(default = RunSetup::default().topology)] topology: Topology,
795 #[builder(default = RunSetup::default().producer_rate)] producer_rate: Frequency,
796 #[builder(default = RunSetup::default().samples)] samples: Vec<Sample>,
797 #[builder(default = RunSetup::default().broker_samples)] broker_samples: Vec<BrokerSample>,
798 #[builder(default = RunSetup::default().p99)] p99: Time,
799 ) -> RunOutput {
800 let setup = RunSetup {
801 scenario,
802 stack,
803 topology,
804 producer_rate,
805 samples,
806 broker_samples,
807 p99,
808 };
809 let RunSetup {
810 scenario: name,
811 stack,
812 topology,
813 producer_rate,
814 samples,
815 broker_samples,
816 p99,
817 } = setup;
818 let scenario = Scenario {
819 mode_tag: ModeTag::Cluster,
820 partitions: topology.partitions,
821 replication_factor: topology.replication_factor,
822 ..self::scenario(name)
823 };
824 RunOutput {
825 scenario,
826 stack,
827 topology,
828 wallclock_start_unix_ms: WallclockMs(0),
829 wallclock_end_unix_ms: WallclockMs(60_000),
830 throughput: Throughput {
831 producer_rate,
832 ..Throughput::default()
833 },
834 producer_latency: LatencyPercentiles {
835 p99,
836 ..LatencyPercentiles::default()
837 },
838 consumer_e2e_latency: LatencyPercentiles::default(),
839 resource: Resource::default(),
840 disturbance: None,
841 startup: None,
842 first_ack: Time::ZERO,
843 errors: vec![],
844 notes: vec![],
845 samples,
846 broker_samples,
847 }
848 }
849
850 pub fn short_scenario(replication_factor: i16) -> Scenario {
851 Scenario {
852 partitions: 1,
853 replication_factor,
854 linger: krabka_units::Time::ZERO,
855 duration: krabka_units::secs(1),
856 warmup: krabka_units::Time::ZERO,
857 ..scenario("x")
858 }
859 }
860}