#![cfg(feature = "std")]
use std::vec::Vec;
use crate::event::TraceEvent;
pub const DEFAULT_SEED: u64 = 0xD5FB_D5FB_D5FB_D5FB;
pub const N_EVENTS: usize = 10_000;
pub const N_ENTITIES: u32 = 16;
pub const N_ROUTES_PER_ENTITY: u32 = 4;
pub const N_WINDOWS: u32 = 128;
pub const WINDOW_SIZE_NS: u64 = 1_000_000_000;
pub const BASELINE_LATENCY_US: u32 = 1_000;
pub const LATENCY_JITTER_US: u32 = 200;
mod episode {
pub const RAMP_ENTITY: u32 = 3;
pub const RAMP_WINDOWS: core::ops::Range<u32> = 20..36;
pub const RAMP_STEP_US: u32 = 3_000;
pub const BURST_ENTITY: u32 = 7;
pub const BURST_WINDOWS: core::ops::Range<u32> = 60..66;
pub const BURST_ERROR_CODE: u16 = 500;
pub const BURST_STATUS_CODE: u16 = 500;
pub const SHOCK_ENTITY: u32 = 11;
pub const SHOCK_WINDOW: u32 = 90;
pub const SHOCK_LATENCY_US: u32 = 100_000;
pub const SHOCK_RECOVERY: core::ops::Range<u32> = 91..96;
}
#[derive(Clone, Copy)]
pub struct Lcg {
state: u64,
}
impl Lcg {
#[must_use]
pub const fn new(seed: u64) -> Self {
Self { state: seed }
}
pub fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
pub fn next_in(&mut self, range: u32) -> u32 {
(self.next_u64() >> 32) as u32 % range
}
pub fn next_jitter(&mut self, magnitude: u32) -> i32 {
if magnitude == 0 {
return 0;
}
let span = magnitude.saturating_mul(2).saturating_add(1);
let raw = self.next_in(span) as i32;
raw - magnitude as i32
}
}
#[must_use]
#[allow(clippy::cast_lossless)]
pub fn synthesize(seed: u64) -> Vec<TraceEvent> {
let mut events: Vec<TraceEvent> = Vec::with_capacity(N_EVENTS);
let mut rng = Lcg::new(seed);
let ticks_per_event: u64 = (u64::from(N_WINDOWS) * WINDOW_SIZE_NS) / (N_EVENTS as u64);
for i in 0..N_EVENTS {
let ts_ns = (i as u64) * ticks_per_event;
let window: u32 = (ts_ns / WINDOW_SIZE_NS) as u32;
let entity_id: u32 = (i as u32) % N_ENTITIES;
let route_id: u32 = entity_id * N_ROUTES_PER_ENTITY + rng.next_in(N_ROUTES_PER_ENTITY);
let jitter = rng.next_jitter(LATENCY_JITTER_US);
let mut latency_us: u32 = (BASELINE_LATENCY_US as i32 + jitter).max(1) as u32;
let mut status_code: u16 = 200;
let mut error_code: u16 = 0;
if entity_id == episode::RAMP_ENTITY && episode::RAMP_WINDOWS.contains(&window) {
let steps = window - episode::RAMP_WINDOWS.start;
latency_us = latency_us.saturating_add(steps * episode::RAMP_STEP_US);
}
if entity_id == episode::BURST_ENTITY && episode::BURST_WINDOWS.contains(&window) {
status_code = episode::BURST_STATUS_CODE;
error_code = episode::BURST_ERROR_CODE;
}
if entity_id == episode::SHOCK_ENTITY && window == episode::SHOCK_WINDOW {
latency_us = episode::SHOCK_LATENCY_US;
}
if entity_id == episode::SHOCK_ENTITY && episode::SHOCK_RECOVERY.contains(&window) {
let steps_in = window - episode::SHOCK_RECOVERY.start;
let span = episode::SHOCK_RECOVERY.end - episode::SHOCK_RECOVERY.start;
let taper_high: u32 = 25_000;
let taper_low: u32 = BASELINE_LATENCY_US;
let taper = taper_high - ((taper_high - taper_low) * steps_in) / span.max(1);
latency_us = taper;
}
let span_id = (i as u64) + 1;
let parent_span_id = if i >= N_ENTITIES as usize {
span_id - u64::from(N_ENTITIES)
} else {
0
};
events.push(TraceEvent {
ts_ns,
entity_id,
route_id,
span_id,
parent_span_id,
latency_us,
status_code,
error_code,
event_kind: 1,
flags: 0,
});
}
events
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn synthesize_scaled(
seed: u64,
n_entities: u32,
n_windows: u32,
events_per_cell: u32,
) -> Vec<TraceEvent> {
let total_events: usize =
(n_entities as usize) * (n_windows as usize) * (events_per_cell as usize);
let mut events: Vec<TraceEvent> = Vec::with_capacity(total_events);
let mut rng = Lcg::new(seed);
let ticks_per_event: u64 = if total_events == 0 {
1
} else {
(u64::from(n_windows) * WINDOW_SIZE_NS) / (total_events as u64).max(1)
};
let ramp_entity = if n_entities > 3 { 3 } else { 0 };
let burst_entity = if n_entities > 7 {
7
} else {
n_entities.saturating_sub(1)
};
let shock_entity = if n_entities > 11 {
11
} else {
n_entities.saturating_sub(1)
};
let scale_w = |w: u32| -> u32 { (u64::from(w) * u64::from(n_windows) / 128) as u32 };
let ramp_start = scale_w(20);
let ramp_end = scale_w(36);
let ramp_step_us: u32 = 3_000;
let burst_start = scale_w(60);
let burst_end = scale_w(66);
let shock_window = scale_w(90);
let shock_recovery_start = scale_w(91);
let shock_recovery_end = scale_w(96);
for i in 0..total_events {
let ts_ns = (i as u64) * ticks_per_event;
let window: u32 = ((ts_ns / WINDOW_SIZE_NS) as u32).min(n_windows.saturating_sub(1));
let entity_id: u32 = (i as u32) % n_entities;
let route_id: u32 = entity_id * N_ROUTES_PER_ENTITY + rng.next_in(N_ROUTES_PER_ENTITY);
let jitter = rng.next_jitter(LATENCY_JITTER_US);
let mut latency_us: u32 = (BASELINE_LATENCY_US as i32 + jitter).max(1) as u32;
let mut status_code: u16 = 200;
let mut error_code: u16 = 0;
if entity_id == ramp_entity && (ramp_start..ramp_end).contains(&window) {
let steps = window - ramp_start;
latency_us = latency_us.saturating_add(steps * ramp_step_us);
}
if entity_id == burst_entity && (burst_start..burst_end).contains(&window) {
status_code = 500;
error_code = 500;
}
if entity_id == shock_entity && window == shock_window {
latency_us = 100_000;
}
if entity_id == shock_entity && (shock_recovery_start..shock_recovery_end).contains(&window)
{
let span = (shock_recovery_end - shock_recovery_start).max(1);
let steps_in = window - shock_recovery_start;
let taper_high: u32 = 25_000;
let taper_low: u32 = BASELINE_LATENCY_US;
latency_us = taper_high - ((taper_high - taper_low) * steps_in) / span;
}
let span_id = (i as u64) + 1;
let parent_span_id = if i >= n_entities as usize {
span_id - u64::from(n_entities)
} else {
0
};
events.push(TraceEvent {
ts_ns,
entity_id,
route_id,
span_id,
parent_span_id,
latency_us,
status_code,
error_code,
event_kind: 1,
flags: 0,
});
}
events
}
#[must_use]
pub fn synthesize_courthouse_factory(
seed: u64,
n_catalogs: u32,
n_entities: u32,
n_windows: u32,
events_per_cell: u32,
) -> Vec<Vec<TraceEvent>> {
const KNUTH_U64: u64 = 0x9E37_79B9_7F4A_7C15;
let mut catalogs: Vec<Vec<TraceEvent>> = Vec::with_capacity(n_catalogs as usize);
for c in 0..u64::from(n_catalogs) {
let derived_seed = seed ^ c.wrapping_mul(KNUTH_U64);
catalogs.push(synthesize_scaled(
derived_seed,
n_entities,
n_windows,
events_per_cell,
));
}
catalogs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lcg_is_deterministic_from_seed() {
let mut a = Lcg::new(0xD5FB);
let mut b = Lcg::new(0xD5FB);
for _ in 0..256 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn lcg_different_seeds_produce_different_streams() {
let mut a = Lcg::new(1);
let mut b = Lcg::new(2);
let stream_a: Vec<u64> = (0..16).map(|_| a.next_u64()).collect();
let stream_b: Vec<u64> = (0..16).map(|_| b.next_u64()).collect();
assert_ne!(stream_a, stream_b);
}
#[test]
fn synthesize_produces_exactly_n_events() {
let events = synthesize(DEFAULT_SEED);
assert_eq!(events.len(), N_EVENTS);
}
#[test]
fn synthesize_is_deterministic_from_seed() {
let a = synthesize(DEFAULT_SEED);
let b = synthesize(DEFAULT_SEED);
assert_eq!(a, b, "same seed must yield same events");
}
#[test]
fn synthesize_distinct_seeds_distinct_fixtures() {
let a = synthesize(DEFAULT_SEED);
let b = synthesize(DEFAULT_SEED.wrapping_add(1));
assert_ne!(a, b);
}
#[test]
fn synthesize_spans_the_expected_window_range() {
let events = synthesize(DEFAULT_SEED);
let min_window = events
.iter()
.map(|e| e.window_index(WINDOW_SIZE_NS))
.min()
.unwrap();
let max_window = events
.iter()
.map(|e| e.window_index(WINDOW_SIZE_NS))
.max()
.unwrap();
assert_eq!(min_window, 0);
assert!(
max_window == N_WINDOWS - 1 || max_window == N_WINDOWS,
"max window observed was {max_window}, expected {} or {}",
N_WINDOWS - 1,
N_WINDOWS
);
}
#[test]
fn synthesize_uses_every_entity() {
let events = synthesize(DEFAULT_SEED);
let mut seen = [false; N_ENTITIES as usize];
for event in &events {
seen[event.entity_id as usize] = true;
}
assert!(
seen.iter().all(|s| *s),
"every entity_id 0..N_ENTITIES must appear"
);
}
#[test]
fn synthesize_injects_latency_ramp_episode() {
let events = synthesize(DEFAULT_SEED);
let near_end_window = episode::RAMP_WINDOWS.end - 1;
let ramp_events: Vec<_> = events
.iter()
.filter(|e| {
e.entity_id == episode::RAMP_ENTITY
&& e.window_index(WINDOW_SIZE_NS) == near_end_window
})
.collect();
assert!(
!ramp_events.is_empty(),
"expected at least one ramp event near the end of the range"
);
for event in ramp_events {
assert!(
event.latency_us > BASELINE_LATENCY_US + LATENCY_JITTER_US + 30_000,
"ramp event latency {} is not elevated beyond baseline+jitter+ramp",
event.latency_us
);
}
}
#[test]
fn synthesize_injects_error_burst_episode() {
let events = synthesize(DEFAULT_SEED);
let burst_events: Vec<_> = events
.iter()
.filter(|e| {
e.entity_id == episode::BURST_ENTITY
&& episode::BURST_WINDOWS.contains(&e.window_index(WINDOW_SIZE_NS))
})
.collect();
assert!(
!burst_events.is_empty(),
"expected events in the burst window range"
);
for event in burst_events {
assert_eq!(event.error_code, episode::BURST_ERROR_CODE);
assert_eq!(event.status_code, episode::BURST_STATUS_CODE);
}
}
#[test]
fn synthesize_injects_slew_shock_episode() {
let events = synthesize(DEFAULT_SEED);
let shock_events: Vec<_> = events
.iter()
.filter(|e| {
e.entity_id == episode::SHOCK_ENTITY
&& e.window_index(WINDOW_SIZE_NS) == episode::SHOCK_WINDOW
})
.collect();
assert!(
!shock_events.is_empty(),
"expected events in the shock window"
);
for event in shock_events {
assert_eq!(event.latency_us, episode::SHOCK_LATENCY_US);
}
}
#[test]
fn synthesize_round_trips_through_canonical_bytes() {
use crate::serialize::{read_fixture, write_fixture};
let events = synthesize(DEFAULT_SEED);
let bytes = write_fixture(&events);
let parsed = read_fixture(&bytes).expect("synthesized fixture parses");
assert_eq!(parsed, events);
}
#[test]
fn synthesize_canonical_bytes_are_stable_across_calls() {
use crate::hash::sha256;
use crate::serialize::write_fixture;
let a = write_fixture(&synthesize(DEFAULT_SEED));
let b = write_fixture(&synthesize(DEFAULT_SEED));
assert_eq!(a, b);
assert_eq!(sha256(&a), sha256(&b));
}
#[test]
fn courthouse_factory_is_deterministic_from_seed() {
let a = synthesize_courthouse_factory(DEFAULT_SEED, 4, 16, 32, 2);
let b = synthesize_courthouse_factory(DEFAULT_SEED, 4, 16, 32, 2);
assert_eq!(a, b, "courthouse-factory must replay byte-identically");
}
#[test]
fn courthouse_factory_per_catalog_independence() {
let factory = synthesize_courthouse_factory(DEFAULT_SEED, 4, 16, 32, 2);
assert_eq!(factory.len(), 4);
assert_ne!(
factory[0], factory[1],
"catalog 0 and 1 must have different event streams (R.2 per-catalog independence)"
);
assert_ne!(factory[1], factory[2]);
}
#[test]
fn courthouse_factory_catalog0_matches_synthesize_scaled() {
let factory = synthesize_courthouse_factory(DEFAULT_SEED, 1, 16, 32, 2);
let scaled = synthesize_scaled(DEFAULT_SEED, 16, 32, 2);
assert_eq!(factory[0], scaled);
}
}