1use std::collections::HashMap;
5use std::fs::File;
6use std::io::{BufRead, BufReader};
7use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result, anyhow, bail};
10use dynamo_data_gen::request_trace::{
11 agentic::lower_agentic_mooncake_rows,
12 load::{RequestTraceMode, load_request_trace_records},
13 mooncake::lower_mooncake_rows,
14};
15use dynamo_data_gen::{AgenticMooncakeRow, MooncakeRow};
16use dynamo_kv_router::LocalBlockHash;
17use dynamo_kv_router::protocols::{
18 ExternalSequenceBlockHash, WorkerId, XXH3_SEED, compute_seq_hash_for_block,
19};
20use dynamo_tokens::compute_hash_v2;
21use rand::rngs::StdRng;
22use rand::{Rng, SeedableRng};
23use rustc_hash::FxHashMap;
24use serde::Deserialize;
25use uuid::Uuid;
26
27use super::driver::WorkloadDriver;
28use super::types::{
29 AgenticTrace, AgenticTurnTrace, DelaySpec, DynamoRequestTrace, LengthSpec, ReplayRequestHashes,
30 RouterSequence, SequenceHashMode, SessionPartitionSpec, SessionTrace, SyntheticTraceSpec,
31 Trace, TraceFileFormat, TurnTrace, effective_replay_key,
32};
33use super::{SYNTHETIC_OUTPUT_SEED, planned_output_token_ids};
34use crate::common::protocols::DirectRequest;
35
36#[derive(Debug, Deserialize)]
37struct RawAppliedComputeAgenticRecord {
38 num_turns: usize,
39 input_prompt_length: usize,
40 assistant_response_length: Vec<usize>,
41 tool_call_output_length: Vec<usize>,
42 tool_call_latency: Vec<f64>,
43 final_assistant_response_length: usize,
44}
45
46#[derive(Debug, Default)]
47struct HashIdInterner {
48 canonical_ids: FxHashMap<u64, u32>,
49}
50
51impl HashIdInterner {
52 fn intern_all(&mut self, hash_ids: Vec<u64>) -> Result<Vec<u32>> {
53 hash_ids
54 .into_iter()
55 .map(|hash_id| self.intern(hash_id))
56 .collect()
57 }
58
59 fn intern(&mut self, hash_id: u64) -> Result<u32> {
60 let next_id = self.canonical_ids.len();
61 match self.canonical_ids.entry(hash_id) {
62 std::collections::hash_map::Entry::Occupied(entry) => Ok(*entry.get()),
63 std::collections::hash_map::Entry::Vacant(entry) => {
64 let canonical_id = u32::try_from(next_id)
65 .context("trace contains more unique hash IDs than u32 can represent")?;
66 entry.insert(canonical_id);
67 Ok(canonical_id)
68 }
69 }
70 }
71}
72
73impl DynamoRequestTrace {
74 pub fn from_request_trace_files(
75 paths: &[PathBuf],
76 expected_block_size: Option<usize>,
77 ) -> Result<Self> {
78 validate_trace_files(TraceFileFormat::Dynamo, paths)?;
79
80 let loaded = load_request_trace_records(paths)?;
81 match loaded.mode()? {
82 RequestTraceMode::Standard => {
83 let mut builder = None;
84 let mut row_index = 0;
85 let block_size = lower_mooncake_rows(loaded.requests, |block_size, row| {
86 let builder =
87 builder.get_or_insert_with(|| MooncakeTraceBuilder::new(block_size));
88 builder.push(row_index, row)?;
89 row_index += 1;
90 Ok(())
91 })?;
92 validate_dynamo_trace_block_size(expected_block_size, block_size)?;
93 let builder = builder.expect("request trace lowering must emit at least one row");
94 Ok(Self::Standard(builder.finish()))
95 }
96 RequestTraceMode::Agentic => {
97 let mut builder = None;
98 let mut row_index = 0;
99 let block_size = lower_agentic_mooncake_rows(loaded, |block_size, row| {
100 let builder =
101 builder.get_or_insert_with(|| AgenticTraceBuilder::new(block_size));
102 builder.push(row_index, row)?;
103 row_index += 1;
104 Ok(())
105 })?;
106 validate_dynamo_trace_block_size(expected_block_size, block_size)?;
107 let builder = builder.expect("request trace lowering must emit at least one row");
108 Ok(Self::Agentic(builder.finish()?))
109 }
110 }
111 }
112}
113
114pub fn validate_trace_files(format: TraceFileFormat, paths: &[PathBuf]) -> Result<()> {
115 if paths.is_empty() {
116 bail!("trace replay requires at least one trace file");
117 }
118 if format != TraceFileFormat::Dynamo && paths.len() != 1 {
119 bail!(
120 "trace_format='{}' requires exactly one trace file, got {}",
121 format.as_str(),
122 paths.len()
123 );
124 }
125 Ok(())
126}
127
128fn validate_dynamo_trace_block_size(expected: Option<usize>, embedded: usize) -> Result<()> {
129 let Some(expected) = expected else {
130 return Ok(());
131 };
132 if expected != embedded {
133 bail!(
134 "trace_block_size {expected} does not match embedded Dynamo request trace block size {embedded}"
135 );
136 }
137 Ok(())
138}
139
140fn single_turn_request_uuid(_request_ordinal: usize) -> Uuid {
141 #[cfg(feature = "replay-bench")]
142 {
143 Uuid::from_u128(_request_ordinal as u128 + 1)
144 }
145 #[cfg(not(feature = "replay-bench"))]
146 {
147 Uuid::new_v4()
148 }
149}
150
151pub(super) fn validate_synthesizable_prompt(
152 input_length: usize,
153 hash_ids: &[u32],
154 trace_block_size: usize,
155) -> Result<()> {
156 if trace_block_size == 0 {
157 bail!("trace_block_size must be greater than 0");
158 }
159 let synthesizable_capacity = hash_ids
160 .len()
161 .checked_mul(trace_block_size)
162 .context("synthesized prompt capacity overflow")?;
163 let required_hash_ids = input_length.div_ceil(trace_block_size);
164 if hash_ids.len() < required_hash_ids {
165 bail!(
166 "input_length {} exceeds synthesized capacity {}",
167 input_length,
168 synthesizable_capacity
169 );
170 }
171
172 Ok(())
173}
174
175pub(super) fn synthesize_trace_tokens(
176 input_length: usize,
177 hash_ids: &[u32],
178 trace_block_size: usize,
179) -> Result<Vec<u32>> {
180 validate_synthesizable_prompt(input_length, hash_ids, trace_block_size)?;
181
182 Ok(synthesize_validated_trace_tokens(
183 input_length,
184 hash_ids,
185 trace_block_size,
186 ))
187}
188
189pub(super) fn synthesize_validated_trace_tokens(
190 input_length: usize,
191 hash_ids: &[u32],
192 trace_block_size: usize,
193) -> Vec<u32> {
194 let mut tokens = Vec::with_capacity(input_length);
195 for &hash_id in hash_ids {
196 let remaining = input_length - tokens.len();
197 tokens.extend(std::iter::repeat_n(
198 hash_id,
199 remaining.min(trace_block_size),
200 ));
201 if tokens.len() == input_length {
202 break;
203 }
204 }
205
206 debug_assert_eq!(tokens.len(), input_length);
207 tokens
208}
209
210fn trace_to_replay_hashes(
211 input_length: usize,
212 hash_ids: &[u32],
213 trace_block_size: usize,
214 engine_block_size: usize,
215) -> Result<ReplayRequestHashes> {
216 if engine_block_size == 0 {
217 bail!("engine_block_size must be greater than 0");
218 }
219
220 let tokens = synthesize_trace_tokens(input_length, hash_ids, trace_block_size)?;
221 let engine_block_size =
222 u32::try_from(engine_block_size).context("engine_block_size does not fit in u32")?;
223 Ok(ReplayRequestHashes::from_tokens(&tokens, engine_block_size))
224}
225
226impl TurnTrace {
227 pub fn synthesize_tokens(&self, trace_block_size: usize) -> Result<Vec<u32>> {
228 synthesize_trace_tokens(self.input_length, &self.hash_ids, trace_block_size)
229 }
230
231 pub fn to_direct_request(
232 &self,
233 trace_block_size: usize,
234 request_uuid: Uuid,
235 arrival_timestamp_ms: Option<f64>,
236 ) -> Result<DirectRequest> {
237 let tokens = self.synthesize_tokens(trace_block_size)?;
238 Ok(DirectRequest {
239 tokens,
240 max_output_tokens: self.max_output_tokens,
241 output_token_ids: self.output_token_ids.clone(),
242 uuid: Some(request_uuid),
243 dp_rank: 0,
244 arrival_timestamp_ms,
245 priority: self.priority,
246 strict_priority: self.strict_priority,
247 policy_class: self.policy_class.clone(),
248 })
249 }
250
251 pub fn to_replay_hashes(
252 &self,
253 trace_block_size: usize,
254 engine_block_size: usize,
255 ) -> Result<ReplayRequestHashes> {
256 trace_to_replay_hashes(
257 self.input_length,
258 &self.hash_ids,
259 trace_block_size,
260 engine_block_size,
261 )
262 }
263}
264
265impl AgenticTurnTrace {
266 pub fn synthesize_tokens(&self, trace_block_size: usize) -> Result<Vec<u32>> {
267 synthesize_trace_tokens(self.input_length, &self.hash_ids, trace_block_size)
268 }
269
270 pub fn to_replay_hashes(
271 &self,
272 trace_block_size: usize,
273 engine_block_size: usize,
274 ) -> Result<ReplayRequestHashes> {
275 trace_to_replay_hashes(
276 self.input_length,
277 &self.hash_ids,
278 trace_block_size,
279 engine_block_size,
280 )
281 }
282}
283
284struct MooncakeTraceBuilder {
285 trace_block_size: usize,
286 hash_id_interner: HashIdInterner,
287 sessions: Vec<SessionTrace>,
288 session_indices: HashMap<String, usize>,
289 last_timestamps: Vec<Option<f64>>,
290}
291
292impl MooncakeTraceBuilder {
293 fn new(trace_block_size: usize) -> Self {
294 Self {
295 trace_block_size,
296 hash_id_interner: HashIdInterner::default(),
297 sessions: Vec::new(),
298 session_indices: HashMap::new(),
299 last_timestamps: Vec::new(),
300 }
301 }
302
303 fn is_empty(&self) -> bool {
304 self.sessions.is_empty()
305 }
306
307 fn push(&mut self, line_idx: usize, raw: MooncakeRow) -> Result<()> {
308 let request_id = raw.request_id;
309 let raw_session_id = raw.session_id;
310 let session_id = raw_session_id
311 .clone()
312 .unwrap_or_else(|| format!("request_{}", line_idx + 1));
313 let hash_ids = raw
314 .hash_ids
315 .ok_or_else(|| anyhow!("trace line {} is missing hash_ids", line_idx + 1))?;
316 let synthesizable_capacity = hash_ids
320 .len()
321 .checked_mul(self.trace_block_size)
322 .ok_or_else(|| anyhow!("trace line {} synthesized capacity overflow", line_idx + 1))?;
323 let input_length = raw
324 .input_length
325 .unwrap_or(synthesizable_capacity)
326 .min(synthesizable_capacity);
327 let output_length = raw
328 .output_length
329 .ok_or_else(|| anyhow!("trace line {} is missing output_length", line_idx + 1))?;
330 let output_token_ids = raw.output_token_ids;
331 if let Some(output_token_ids) = output_token_ids.as_ref()
332 && output_token_ids.len() != output_length
333 {
334 bail!(
335 "trace line {} output_length {} does not match output_token_ids length {}",
336 line_idx + 1,
337 output_length,
338 output_token_ids.len()
339 );
340 }
341 let timestamp_ms = raw.timestamp;
342 let explicit_delay_ms = raw.delay;
343 let priority = raw.priority.unwrap_or(0);
344 let strict_priority = raw.strict_priority.unwrap_or(0);
345 let policy_class = raw.policy_class.clone();
346
347 let session_index = *self
348 .session_indices
349 .entry(session_id.clone())
350 .or_insert_with(|| {
351 let idx = self.sessions.len();
352 self.sessions.push(SessionTrace {
353 session_id: session_id.clone(),
354 first_arrival_timestamp_ms: timestamp_ms,
355 turns: Vec::new(),
356 });
357 self.last_timestamps.push(timestamp_ms);
358 idx
359 });
360
361 let session = self
362 .sessions
363 .get_mut(session_index)
364 .expect("newly inserted session must exist");
365 let turn_idx = session.turns.len();
366 let replay_key = output_token_ids.as_ref().map(|_| {
367 effective_replay_key(
368 request_id.as_deref(),
369 raw_session_id.as_deref(),
370 turn_idx,
371 line_idx,
372 )
373 });
374 let delay_after_previous_ms = if turn_idx == 0 {
375 let delay = explicit_delay_ms.unwrap_or(0.0);
376 if delay != 0.0 {
377 bail!(
378 "trace line {} sets delay on the first turn of session {}",
379 line_idx + 1,
380 session.session_id
381 );
382 }
383 0.0
384 } else if let Some(delay_ms) = explicit_delay_ms {
385 delay_ms
386 } else if let Some(timestamp_ms) = timestamp_ms {
387 let previous_timestamp_ms = self.last_timestamps[session_index].ok_or_else(|| {
388 anyhow!(
389 "trace line {} for session {} cannot infer delay without a previous timestamp",
390 line_idx + 1,
391 session.session_id
392 )
393 })?;
394 timestamp_ms - previous_timestamp_ms
395 } else {
396 0.0
397 };
398
399 if !delay_after_previous_ms.is_finite() || delay_after_previous_ms < 0.0 {
400 bail!(
401 "trace line {} has invalid delay {}",
402 line_idx + 1,
403 delay_after_previous_ms
404 );
405 }
406
407 if hash_ids.len() * self.trace_block_size < input_length {
408 bail!(
409 "trace line {} input_length {} exceeds synthesized capacity {}",
410 line_idx + 1,
411 input_length,
412 hash_ids.len() * self.trace_block_size
413 );
414 }
415
416 let hash_ids = self.hash_id_interner.intern_all(hash_ids)?;
417
418 session.turns.push(TurnTrace {
419 input_length,
420 max_output_tokens: output_length,
421 output_token_ids,
422 replay_key,
423 hash_ids,
424 delay_after_previous_ms,
425 priority,
426 strict_priority,
427 policy_class,
428 });
429 if let Some(timestamp_ms) = timestamp_ms {
430 self.last_timestamps[session_index] = Some(timestamp_ms);
431 }
432 Ok(())
433 }
434
435 fn finish(self) -> Trace {
436 Trace {
437 block_size: self.trace_block_size,
438 sessions: self.sessions,
439 }
440 }
441}
442
443impl Trace {
444 pub fn from_mooncake(path: &Path, trace_block_size: usize) -> Result<Self> {
445 if trace_block_size == 0 {
446 bail!("trace_block_size must be greater than 0");
447 }
448
449 let file = File::open(path)
450 .with_context(|| format!("failed to open trace file {}", path.display()))?;
451 let reader = BufReader::new(file);
452 let mut builder = MooncakeTraceBuilder::new(trace_block_size);
453
454 for (line_idx, line) in reader.lines().enumerate() {
455 let line = line.with_context(|| {
456 format!(
457 "failed to read line {} from {}",
458 line_idx + 1,
459 path.display()
460 )
461 })?;
462 if line.trim().is_empty() {
463 continue;
464 }
465
466 let row = serde_json::from_str(&line).with_context(|| {
467 format!(
468 "failed to parse line {} from {} as JSON",
469 line_idx + 1,
470 path.display()
471 )
472 })?;
473 builder.push(line_idx, row)?;
474 }
475
476 if builder.is_empty() {
477 bail!("trace file {} did not contain any requests", path.display());
478 }
479
480 Ok(builder.finish())
481 }
482
483 pub fn from_mooncake_rows(rows: Vec<MooncakeRow>, trace_block_size: usize) -> Result<Self> {
484 if trace_block_size == 0 {
485 bail!("trace_block_size must be greater than 0");
486 }
487 let mut builder = MooncakeTraceBuilder::new(trace_block_size);
488 for (line_idx, row) in rows.into_iter().enumerate() {
489 builder.push(line_idx, row)?;
490 }
491 if builder.is_empty() {
492 bail!("Mooncake rows did not contain any requests");
493 }
494 Ok(builder.finish())
495 }
496
497 pub fn from_applied_compute_agentic(
498 path: &Path,
499 trace_block_size: usize,
500 shared_prefix_ratio: f64,
501 num_prefix_groups: usize,
502 ) -> Result<Self> {
503 if trace_block_size == 0 {
504 bail!("trace_block_size must be greater than 0");
505 }
506 if !(0.0..=1.0).contains(&shared_prefix_ratio) {
507 bail!(
508 "shared_prefix_ratio must be between 0.0 and 1.0, got {}",
509 shared_prefix_ratio
510 );
511 }
512
513 let file = File::open(path)
514 .with_context(|| format!("failed to open trace file {}", path.display()))?;
515 let reader = BufReader::new(file);
516 let mut sessions = Vec::new();
517 let mut hash_id_interner = HashIdInterner::default();
518 let mut next_unique_hash = 1_u64;
519
520 for (line_idx, line) in reader.lines().enumerate() {
521 let line = line.with_context(|| {
522 format!(
523 "failed to read line {} from {}",
524 line_idx + 1,
525 path.display()
526 )
527 })?;
528 if line.trim().is_empty() {
529 continue;
530 }
531
532 let raw: RawAppliedComputeAgenticRecord =
533 serde_json::from_str(&line).with_context(|| {
534 format!(
535 "failed to parse line {} from {} as JSON",
536 line_idx + 1,
537 path.display()
538 )
539 })?;
540
541 for (name, values) in [
542 (
543 "assistant_response_length",
544 raw.assistant_response_length.len(),
545 ),
546 ("tool_call_output_length", raw.tool_call_output_length.len()),
547 ("tool_call_latency", raw.tool_call_latency.len()),
548 ] {
549 if values != raw.num_turns {
550 bail!(
551 "trace line {} field {} length {} does not match num_turns {}",
552 line_idx + 1,
553 name,
554 values,
555 raw.num_turns
556 );
557 }
558 }
559
560 if raw.input_prompt_length == 0 {
561 bail!(
562 "trace line {} input_prompt_length must be positive",
563 line_idx + 1
564 );
565 }
566
567 let group_id = if shared_prefix_ratio > 0.0 && num_prefix_groups > 0 {
568 Some(line_idx % num_prefix_groups)
569 } else {
570 None
571 };
572 let mut current_input_length = raw.input_prompt_length;
573 let mut hash_ids = Vec::new();
574 let shared_initial_blocks = ((current_input_length.div_ceil(trace_block_size) as f64)
575 * shared_prefix_ratio)
576 .round() as usize;
577 extend_applied_compute_agentic_hash_ids(
578 &mut hash_ids,
579 current_input_length,
580 trace_block_size,
581 shared_initial_blocks,
582 group_id,
583 &mut next_unique_hash,
584 )?;
585
586 let mut turns = Vec::with_capacity(raw.num_turns + 1);
587 let mut next_turn_delay_ms = 0.0;
588 for turn_idx in 0..raw.num_turns {
589 let tool_call_latency = raw.tool_call_latency[turn_idx];
590 if !tool_call_latency.is_finite() || tool_call_latency < 0.0 {
591 bail!(
592 "trace line {} tool_call_latency[{}] must be a finite non-negative number",
593 line_idx + 1,
594 turn_idx
595 );
596 }
597
598 turns.push(TurnTrace {
599 input_length: current_input_length,
600 max_output_tokens: raw.assistant_response_length[turn_idx],
601 hash_ids: hash_id_interner.intern_all(hash_ids.clone())?,
602 delay_after_previous_ms: next_turn_delay_ms,
603 ..Default::default()
604 });
605
606 current_input_length = current_input_length
607 .checked_add(raw.assistant_response_length[turn_idx])
608 .and_then(|value| value.checked_add(raw.tool_call_output_length[turn_idx]))
609 .ok_or_else(|| {
610 anyhow!(
611 "trace line {} cumulative input length overflow",
612 line_idx + 1
613 )
614 })?;
615 extend_applied_compute_agentic_hash_ids(
616 &mut hash_ids,
617 current_input_length,
618 trace_block_size,
619 shared_initial_blocks,
620 group_id,
621 &mut next_unique_hash,
622 )?;
623 next_turn_delay_ms = tool_call_latency * 1000.0;
624 }
625
626 turns.push(TurnTrace {
627 input_length: current_input_length,
628 max_output_tokens: raw.final_assistant_response_length,
629 hash_ids: hash_id_interner.intern_all(hash_ids)?,
630 delay_after_previous_ms: next_turn_delay_ms,
631 ..Default::default()
632 });
633
634 sessions.push(SessionTrace {
635 session_id: format!("applied_compute_agentic_session_{}", line_idx + 1),
636 first_arrival_timestamp_ms: None,
637 turns,
638 });
639 }
640
641 if sessions.is_empty() {
642 bail!("trace file {} did not contain any requests", path.display());
643 }
644
645 Ok(Self {
646 block_size: trace_block_size,
647 sessions,
648 })
649 }
650
651 pub fn synthetic(spec: SyntheticTraceSpec) -> Result<Self> {
652 if spec.block_size == 0 {
653 bail!("block_size must be greater than 0");
654 }
655 if spec.num_sessions == 0 {
656 bail!("num_sessions must be greater than 0");
657 }
658 if spec.turns_per_session == 0 {
659 bail!("turns_per_session must be greater than 0");
660 }
661 if !(0.0..=1.0).contains(&spec.shared_prefix_ratio) {
662 bail!(
663 "shared_prefix_ratio must be between 0.0 and 1.0, got {}",
664 spec.shared_prefix_ratio
665 );
666 }
667
668 let mut rng = StdRng::seed_from_u64(spec.seed);
669 let mut sessions = Vec::with_capacity(spec.num_sessions);
670 let first_arrivals = spec
671 .first_turn_arrivals
672 .timestamps(spec.num_sessions, spec.arrival_seed)?;
673
674 let mut next_unique_hash = 1_u64;
675 let mut hash_id_interner = HashIdInterner::default();
676 for (session_idx, first_arrival_timestamp_ms) in first_arrivals.into_iter().enumerate() {
677 let group_id = if spec.num_prefix_groups > 0 && spec.shared_prefix_ratio > 0.0 {
678 Some(rng.random_range(0..spec.num_prefix_groups) as u64)
679 } else {
680 None
681 };
682 let mut turns = Vec::with_capacity(spec.turns_per_session);
683 for turn_idx in 0..spec.turns_per_session {
684 let input_length = sample_length(&spec.input_tokens, 1, &mut rng);
685 let max_output_tokens = sample_length(&spec.output_tokens, 1, &mut rng);
686 let num_blocks = input_length.div_ceil(spec.block_size);
687 let prefix_blocks =
688 ((num_blocks as f64) * spec.shared_prefix_ratio).round() as usize;
689 let prefix_blocks = prefix_blocks.min(num_blocks);
690 let mut hash_ids = Vec::with_capacity(num_blocks);
691
692 for block_idx in 0..prefix_blocks {
693 if let Some(group_id) = group_id {
694 hash_ids.push(0xD00D_0000_0000_0000 | (group_id << 32) | block_idx as u64);
695 }
696 }
697
698 while hash_ids.len() < num_blocks {
699 hash_ids.push(next_unique_hash);
700 next_unique_hash = next_unique_hash
701 .checked_add(1)
702 .expect("synthetic hash id overflow");
703 }
704
705 turns.push(TurnTrace {
706 input_length,
707 max_output_tokens,
708 hash_ids: hash_id_interner.intern_all(hash_ids)?,
709 delay_after_previous_ms: if turn_idx == 0 {
710 0.0
711 } else {
712 sample_delay_ms(&spec.inter_turn_delays, &mut rng)?
713 },
714 ..Default::default()
715 });
716 }
717
718 sessions.push(SessionTrace {
719 session_id: format!("session_{session_idx}"),
720 first_arrival_timestamp_ms: Some(first_arrival_timestamp_ms),
721 turns,
722 });
723 }
724
725 Ok(Self {
726 block_size: spec.block_size,
727 sessions,
728 })
729 }
730
731 pub fn validate_for_trace_mode(&self) -> Result<()> {
732 self.validate(false)
733 }
734
735 pub fn validate_for_concurrency_mode(&self) -> Result<()> {
736 self.validate(true)
737 }
738
739 pub fn normalize_session_starts(mut self) -> Result<Self> {
740 let Some(min_timestamp_ms) = self
741 .sessions
742 .iter()
743 .filter_map(|session| session.first_arrival_timestamp_ms)
744 .min_by(|left, right| left.total_cmp(right))
745 else {
746 return Ok(self);
747 };
748
749 for session in &mut self.sessions {
750 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
751 *timestamp_ms -= min_timestamp_ms;
752 }
753 }
754 Ok(self)
755 }
756
757 pub fn speed_up_timing(mut self, ratio: f64) -> Result<Self> {
758 if !ratio.is_finite() || ratio <= 0.0 {
759 bail!("ratio must be a finite positive number, got {ratio}");
760 }
761
762 for session in &mut self.sessions {
763 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
764 *timestamp_ms /= ratio;
765 }
766 for turn in &mut session.turns {
767 turn.delay_after_previous_ms /= ratio;
768 }
769 }
770 Ok(self)
771 }
772
773 pub fn rescale_session_start_span(mut self, duration_ms: u64) -> Result<Self> {
774 let Some(min_timestamp_ms) = self
775 .sessions
776 .iter()
777 .filter_map(|session| session.first_arrival_timestamp_ms)
778 .min_by(|left, right| left.total_cmp(right))
779 else {
780 return Ok(self);
781 };
782 let Some(max_timestamp_ms) = self
783 .sessions
784 .iter()
785 .filter_map(|session| session.first_arrival_timestamp_ms)
786 .max_by(|left, right| left.total_cmp(right))
787 else {
788 return Ok(self);
789 };
790
791 let target_span_ms = duration_ms as f64;
792 let source_span_ms = max_timestamp_ms - min_timestamp_ms;
793 for session in &mut self.sessions {
794 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
795 *timestamp_ms = if source_span_ms == 0.0 {
796 0.0
797 } else {
798 (*timestamp_ms - min_timestamp_ms) * target_span_ms / source_span_ms
799 };
800 }
801 }
802 Ok(self)
803 }
804
805 pub fn rescale_ready_span(mut self, duration_ms: u64) -> Result<Self> {
806 let Some(min_start_ms) = self
807 .sessions
808 .iter()
809 .map(|session| session.first_arrival_timestamp_ms.unwrap_or(0.0))
810 .min_by(|left, right| left.total_cmp(right))
811 else {
812 return Ok(self);
813 };
814
815 let Some(max_ready_ms) = self
816 .sessions
817 .iter()
818 .map(|session| {
819 session.first_arrival_timestamp_ms.unwrap_or(0.0)
820 + session
821 .turns
822 .iter()
823 .enumerate()
824 .filter(|(turn_idx, _)| *turn_idx > 0)
825 .map(|(_, turn)| turn.delay_after_previous_ms)
826 .sum::<f64>()
827 })
828 .max_by(|left, right| left.total_cmp(right))
829 else {
830 return Ok(self);
831 };
832
833 let ratio = duration_ms as f64 / (max_ready_ms - min_start_ms).max(1.0);
834 for session in &mut self.sessions {
835 if let Some(start_ms) = session.first_arrival_timestamp_ms.as_mut() {
836 *start_ms = (*start_ms - min_start_ms) * ratio;
837 }
838 for (turn_idx, turn) in session.turns.iter_mut().enumerate() {
839 if turn_idx > 0 {
840 turn.delay_after_previous_ms *= ratio;
841 }
842 }
843 }
844 Ok(self)
845 }
846
847 pub fn expand_hash_prefix_depth(mut self, factor: usize) -> Self {
848 if factor <= 1 {
849 return self;
850 }
851 let factor = u32::try_from(factor).expect("hash prefix expansion factor exceeds u32");
852 for session in &mut self.sessions {
853 for turn in &mut session.turns {
854 turn.input_length = turn
855 .input_length
856 .checked_mul(factor as usize)
857 .expect("input_length expansion overflow");
858 turn.hash_ids = turn
859 .hash_ids
860 .iter()
861 .flat_map(|&hash_id| {
862 let base = hash_id
863 .checked_mul(factor)
864 .expect("hash prefix expansion overflow");
865 (0..factor).map(move |offset| {
866 base.checked_add(offset)
867 .expect("hash prefix expansion overflow")
868 })
869 })
870 .collect();
871 }
872 }
873 self
874 }
875
876 pub fn duplicate_hash_space(mut self, copies: usize) -> Self {
877 if copies <= 1 {
878 return self;
879 }
880
881 let max_hash_id = self
882 .sessions
883 .iter()
884 .flat_map(|session| session.turns.iter())
885 .flat_map(|turn| turn.hash_ids.iter().copied())
886 .max()
887 .unwrap_or(0);
888 let offset_base = max_hash_id
889 .checked_add(1)
890 .expect("hash duplication offset overflow");
891 let original_sessions = self.sessions.clone();
892 self.sessions.clear();
893
894 for copy_idx in 0..copies {
895 let copy_idx = u32::try_from(copy_idx).expect("hash copy index exceeds u32");
896 let offset = offset_base
897 .checked_mul(copy_idx)
898 .expect("hash duplication offset overflow");
899 for session in &original_sessions {
900 let mut duplicated = session.clone();
901 duplicated.session_id = format!("{}:copy_{copy_idx}", session.session_id);
902 for turn in &mut duplicated.turns {
903 turn.hash_ids = turn
904 .hash_ids
905 .iter()
906 .map(|&hash_id| {
907 hash_id
908 .checked_add(offset)
909 .expect("hash duplication overflow")
910 })
911 .collect();
912 }
913 self.sessions.push(duplicated);
914 }
915 }
916 self
917 }
918
919 pub fn partition_by_session(&self, spec: SessionPartitionSpec) -> Vec<Self> {
920 let num_partitions = match spec {
921 SessionPartitionSpec::Random { num_partitions, .. } => num_partitions,
922 SessionPartitionSpec::RoundRobin { num_partitions } => num_partitions,
923 }
924 .max(1);
925 let mut partitions = vec![
926 Self {
927 block_size: self.block_size,
928 sessions: Vec::new(),
929 };
930 num_partitions
931 ];
932
933 let mut rng = match spec {
934 SessionPartitionSpec::Random { seed, .. } => Some(StdRng::seed_from_u64(seed)),
935 SessionPartitionSpec::RoundRobin { .. } => None,
936 };
937
938 for (session_idx, session) in self.sessions.iter().cloned().enumerate() {
939 let partition_idx = match spec {
940 SessionPartitionSpec::Random { .. } => rng
941 .as_mut()
942 .expect("random partitioner must exist")
943 .random_range(0..num_partitions),
944 SessionPartitionSpec::RoundRobin { .. } => session_idx % num_partitions,
945 };
946 partitions[partition_idx].sessions.push(session);
947 }
948
949 partitions
950 }
951
952 pub fn to_single_turn_requests(&self) -> Result<Vec<DirectRequest>> {
953 let mut requests = Vec::with_capacity(self.sessions.len());
954 let mut output_rng = StdRng::seed_from_u64(SYNTHETIC_OUTPUT_SEED);
955 for (request_ordinal, session) in self.sessions.iter().enumerate() {
956 if session.turns.len() != 1 {
957 bail!(
958 "to_single_turn_requests requires exactly one turn per session, but session {} has {} turns",
959 session.session_id,
960 session.turns.len()
961 );
962 }
963 let request_uuid = single_turn_request_uuid(request_ordinal);
964 let mut request = session.turns[0].to_direct_request(
965 self.block_size,
966 request_uuid,
967 session.first_arrival_timestamp_ms,
968 )?;
969 request.output_token_ids = Some(planned_output_token_ids(
970 request.output_token_ids,
971 request.max_output_tokens,
972 &mut output_rng,
973 ));
974 requests.push(request);
975 }
976 Ok(requests)
977 }
978
979 pub fn is_single_turn(&self) -> bool {
980 self.sessions.iter().all(|session| session.turns.len() == 1)
981 }
982
983 pub fn to_router_sequences(
984 &self,
985 worker_id: WorkerId,
986 hash_mode: SequenceHashMode,
987 ) -> Result<Vec<RouterSequence>> {
988 let mut sequences = Vec::new();
989 for session in &self.sessions {
990 for turn in &session.turns {
991 let local_hashes = turn
992 .hash_ids
993 .iter()
994 .map(|&hash_id| local_block_hash_from_id(hash_id, self.block_size))
995 .collect::<Vec<_>>();
996 let external_hashes = match hash_mode {
997 SequenceHashMode::Raw => local_hashes
998 .iter()
999 .map(|hash| ExternalSequenceBlockHash(hash.0))
1000 .collect(),
1001 SequenceHashMode::Cumulative => compute_seq_hash_for_block(&local_hashes)
1002 .into_iter()
1003 .map(ExternalSequenceBlockHash)
1004 .collect(),
1005 };
1006 sequences.push(RouterSequence {
1007 worker_id,
1008 local_hashes,
1009 external_hashes,
1010 });
1011 }
1012 }
1013 Ok(sequences)
1014 }
1015
1016 pub fn into_trace_driver(self) -> Result<WorkloadDriver> {
1017 self.validate_for_trace_mode()?;
1018 let engine_block_size = self.block_size;
1019 WorkloadDriver::new_trace(self, engine_block_size)
1020 }
1021
1022 pub fn into_concurrency_driver(self, max_in_flight: usize) -> Result<WorkloadDriver> {
1023 self.validate_for_concurrency_mode()?;
1024 let engine_block_size = self.block_size;
1025 WorkloadDriver::new_concurrency(self, engine_block_size, max_in_flight)
1026 }
1027
1028 pub fn into_trace_driver_with_block_size(
1029 self,
1030 engine_block_size: usize,
1031 ) -> Result<WorkloadDriver> {
1032 self.validate_for_trace_mode()?;
1033 WorkloadDriver::new_trace(self, engine_block_size)
1034 }
1035
1036 pub fn into_delta_accumulating_trace_driver_with_block_size(
1037 self,
1038 engine_block_size: usize,
1039 ) -> Result<WorkloadDriver> {
1040 self.validate_for_trace_mode()?;
1041 WorkloadDriver::new_trace_accumulating_deltas(self, engine_block_size)
1042 }
1043
1044 pub fn into_concurrency_driver_with_block_size(
1045 self,
1046 engine_block_size: usize,
1047 max_in_flight: usize,
1048 ) -> Result<WorkloadDriver> {
1049 self.validate_for_concurrency_mode()?;
1050 WorkloadDriver::new_concurrency(self, engine_block_size, max_in_flight)
1051 }
1052
1053 pub fn into_delta_accumulating_concurrency_driver_with_block_size(
1054 self,
1055 engine_block_size: usize,
1056 max_in_flight: usize,
1057 ) -> Result<WorkloadDriver> {
1058 self.validate_for_concurrency_mode()?;
1059 WorkloadDriver::new_concurrency_accumulating_deltas(self, engine_block_size, max_in_flight)
1060 }
1061
1062 fn validate(&self, allow_missing_first_timestamp: bool) -> Result<()> {
1063 if self.block_size == 0 {
1064 bail!("block_size must be greater than 0");
1065 }
1066 if self.sessions.is_empty() {
1067 bail!("trace must contain at least one session");
1068 }
1069
1070 for session in &self.sessions {
1071 if session.turns.is_empty() {
1072 bail!(
1073 "session {} must contain at least one turn",
1074 session.session_id
1075 );
1076 }
1077 if !allow_missing_first_timestamp {
1078 let timestamp_ms = session.first_arrival_timestamp_ms.ok_or_else(|| {
1079 anyhow!(
1080 "trace mode requires first_arrival_timestamp_ms for session {}",
1081 session.session_id
1082 )
1083 })?;
1084 if !timestamp_ms.is_finite() || timestamp_ms < 0.0 {
1085 bail!(
1086 "session {} has invalid first_arrival_timestamp_ms {}",
1087 session.session_id,
1088 timestamp_ms
1089 );
1090 }
1091 } else if let Some(timestamp_ms) = session.first_arrival_timestamp_ms
1092 && (!timestamp_ms.is_finite() || timestamp_ms < 0.0)
1093 {
1094 bail!(
1095 "session {} has invalid first_arrival_timestamp_ms {}",
1096 session.session_id,
1097 timestamp_ms
1098 );
1099 }
1100
1101 for (turn_idx, turn) in session.turns.iter().enumerate() {
1102 if let Some(output_token_ids) = turn.output_token_ids.as_ref()
1103 && output_token_ids.len() != turn.max_output_tokens
1104 {
1105 bail!(
1106 "session {} turn {} max_output_tokens {} does not match output_token_ids length {}",
1107 session.session_id,
1108 turn_idx,
1109 turn.max_output_tokens,
1110 output_token_ids.len()
1111 );
1112 }
1113 if turn.input_length == 0 {
1114 bail!(
1115 "session {} turn {} must have a positive input_length",
1116 session.session_id,
1117 turn_idx
1118 );
1119 }
1120 if turn.hash_ids.is_empty() {
1121 bail!(
1122 "session {} turn {} must contain at least one hash id",
1123 session.session_id,
1124 turn_idx
1125 );
1126 }
1127 validate_synthesizable_prompt(turn.input_length, &turn.hash_ids, self.block_size)
1128 .with_context(|| {
1129 format!(
1130 "session {} turn {} has invalid prompt",
1131 session.session_id, turn_idx
1132 )
1133 })?;
1134 if !turn.delay_after_previous_ms.is_finite() || turn.delay_after_previous_ms < 0.0 {
1135 bail!(
1136 "session {} turn {} has invalid delay {}",
1137 session.session_id,
1138 turn_idx,
1139 turn.delay_after_previous_ms
1140 );
1141 }
1142 if turn_idx == 0 && turn.delay_after_previous_ms != 0.0 {
1143 bail!(
1144 "session {} first turn must have delay_after_previous_ms == 0.0",
1145 session.session_id
1146 );
1147 }
1148 }
1149 }
1150
1151 Ok(())
1152 }
1153}
1154
1155struct AgenticTraceBuilder {
1156 trace_block_size: usize,
1157 hash_id_interner: HashIdInterner,
1158 turns: Vec<AgenticTurnTrace>,
1159 request_ids: std::collections::HashSet<String>,
1160}
1161
1162impl AgenticTraceBuilder {
1163 fn new(trace_block_size: usize) -> Self {
1164 Self {
1165 trace_block_size,
1166 hash_id_interner: HashIdInterner::default(),
1167 turns: Vec::new(),
1168 request_ids: std::collections::HashSet::new(),
1169 }
1170 }
1171
1172 fn is_empty(&self) -> bool {
1173 self.turns.is_empty()
1174 }
1175
1176 fn push(&mut self, line_idx: usize, raw: AgenticMooncakeRow) -> Result<()> {
1177 if raw.request_id.trim().is_empty() {
1178 bail!("trace line {} has empty request_id", line_idx + 1);
1179 }
1180 if !self.request_ids.insert(raw.request_id.clone()) {
1181 bail!(
1182 "trace line {} duplicates request_id {}",
1183 line_idx + 1,
1184 raw.request_id
1185 );
1186 }
1187
1188 let delay_after_dependencies_ms = raw.dependency_delay_ms();
1189 let hash_ids = raw
1190 .hash_ids
1191 .ok_or_else(|| anyhow!("trace line {} is missing hash_ids", line_idx + 1))?;
1192 let synthesizable_capacity = hash_ids
1193 .len()
1194 .checked_mul(self.trace_block_size)
1195 .ok_or_else(|| anyhow!("trace line {} synthesized capacity overflow", line_idx + 1))?;
1196 let input_length = match raw.input_length {
1197 Some(input_length) if input_length > synthesizable_capacity => {
1198 bail!(
1199 "trace line {} has input_length {} but only {} tokens can be synthesized from {} hash_ids at trace_block_size {}",
1200 line_idx + 1,
1201 input_length,
1202 synthesizable_capacity,
1203 hash_ids.len(),
1204 self.trace_block_size
1205 );
1206 }
1207 Some(input_length) => input_length,
1208 None => synthesizable_capacity,
1209 };
1210 let output_length = raw
1211 .output_length
1212 .ok_or_else(|| anyhow!("trace line {} is missing output_length", line_idx + 1))?;
1213 let output_token_ids = raw.output_token_ids;
1214 if let Some(output_token_ids) = output_token_ids.as_ref()
1215 && output_token_ids.len() != output_length
1216 {
1217 bail!(
1218 "trace line {} output_length {} does not match output_token_ids length {}",
1219 line_idx + 1,
1220 output_length,
1221 output_token_ids.len()
1222 );
1223 }
1224 if !delay_after_dependencies_ms.is_finite() || delay_after_dependencies_ms < 0.0 {
1225 bail!(
1226 "trace line {} has invalid dependency delay {}",
1227 line_idx + 1,
1228 delay_after_dependencies_ms
1229 );
1230 }
1231 if let Some(timestamp_ms) = raw.timestamp
1232 && (!timestamp_ms.is_finite() || timestamp_ms < 0.0)
1233 {
1234 bail!(
1235 "trace line {} has invalid timestamp {}",
1236 line_idx + 1,
1237 timestamp_ms
1238 );
1239 }
1240
1241 let hash_ids = self.hash_id_interner.intern_all(hash_ids)?;
1242
1243 let replay_key = output_token_ids.as_ref().map(|_| {
1244 effective_replay_key(
1245 Some(raw.request_id.as_str()),
1246 raw.session_id.as_deref(),
1247 0,
1248 line_idx,
1249 )
1250 });
1251 self.turns.push(AgenticTurnTrace {
1252 replay_key,
1253 request_id: raw.request_id,
1254 session_id: raw
1255 .session_id
1256 .unwrap_or_else(|| format!("request_{}", line_idx + 1)),
1257 input_length,
1258 max_output_tokens: output_length,
1259 output_token_ids,
1260 hash_ids,
1261 first_ready_timestamp_ms: raw.timestamp,
1262 delay_after_dependencies_ms,
1263 priority: raw.priority.unwrap_or(0),
1264 strict_priority: raw.strict_priority.unwrap_or(0),
1265 policy_class: raw.policy_class,
1266 wait_for: raw.wait_for,
1267 prefix_reset: raw.prefix_reset.unwrap_or(false),
1268 });
1269 Ok(())
1270 }
1271
1272 fn finish(self) -> Result<AgenticTrace> {
1273 for turn in &self.turns {
1274 for dependency in &turn.wait_for {
1275 if !self.request_ids.contains(dependency) {
1276 bail!(
1277 "request {} waits for unknown request_id {}",
1278 turn.request_id,
1279 dependency
1280 );
1281 }
1282 if dependency == &turn.request_id {
1283 bail!("request {} cannot wait for itself", turn.request_id);
1284 }
1285 }
1286 }
1287 validate_agentic_trace_is_acyclic(&self.turns)?;
1288
1289 Ok(AgenticTrace {
1290 block_size: self.trace_block_size,
1291 turns: self.turns,
1292 })
1293 }
1294}
1295
1296impl AgenticTrace {
1297 pub fn from_agentic_mooncake(path: &Path, trace_block_size: usize) -> Result<Self> {
1298 if trace_block_size == 0 {
1299 bail!("trace_block_size must be greater than 0");
1300 }
1301
1302 let file = File::open(path)
1303 .with_context(|| format!("failed to open trace file {}", path.display()))?;
1304 let reader = BufReader::new(file);
1305 let mut builder = AgenticTraceBuilder::new(trace_block_size);
1306
1307 for (line_idx, line) in reader.lines().enumerate() {
1308 let line = line.with_context(|| {
1309 format!(
1310 "failed to read line {} from {}",
1311 line_idx + 1,
1312 path.display()
1313 )
1314 })?;
1315 if line.trim().is_empty() {
1316 continue;
1317 }
1318
1319 let row = serde_json::from_str(&line).with_context(|| {
1320 format!(
1321 "failed to parse line {} from {} as agentic Mooncake JSON",
1322 line_idx + 1,
1323 path.display()
1324 )
1325 })?;
1326 builder.push(line_idx, row)?;
1327 }
1328
1329 if builder.is_empty() {
1330 bail!(
1331 "agentic trace file {} did not contain any requests",
1332 path.display()
1333 );
1334 }
1335
1336 builder.finish()
1337 }
1338
1339 pub fn from_agentic_mooncake_rows(
1340 rows: Vec<AgenticMooncakeRow>,
1341 trace_block_size: usize,
1342 ) -> Result<Self> {
1343 if trace_block_size == 0 {
1344 bail!("trace_block_size must be greater than 0");
1345 }
1346 let mut builder = AgenticTraceBuilder::new(trace_block_size);
1347 for (line_idx, row) in rows.into_iter().enumerate() {
1348 builder.push(line_idx, row)?;
1349 }
1350 if builder.is_empty() {
1351 bail!("agentic Mooncake rows did not contain any requests");
1352 }
1353 builder.finish()
1354 }
1355
1356 pub fn normalize_starts(mut self) -> Self {
1357 let Some(min_timestamp_ms) = self
1358 .turns
1359 .iter()
1360 .filter_map(|turn| turn.first_ready_timestamp_ms)
1361 .min_by(|left, right| left.total_cmp(right))
1362 else {
1363 return self;
1364 };
1365
1366 for turn in &mut self.turns {
1367 if let Some(timestamp_ms) = turn.first_ready_timestamp_ms.as_mut() {
1368 *timestamp_ms -= min_timestamp_ms;
1369 }
1370 }
1371 self
1372 }
1373
1374 pub fn speed_up_timing(mut self, ratio: f64) -> Result<Self> {
1375 if !ratio.is_finite() || ratio <= 0.0 {
1376 bail!("ratio must be a finite positive number, got {ratio}");
1377 }
1378
1379 for turn in &mut self.turns {
1380 if let Some(timestamp_ms) = turn.first_ready_timestamp_ms.as_mut() {
1381 *timestamp_ms /= ratio;
1382 }
1383 turn.delay_after_dependencies_ms /= ratio;
1384 }
1385 Ok(self)
1386 }
1387
1388 pub fn into_trace_driver_with_block_size(
1389 self,
1390 engine_block_size: usize,
1391 ) -> Result<WorkloadDriver> {
1392 WorkloadDriver::new_agentic_trace(self, engine_block_size)
1393 }
1394}
1395
1396fn validate_agentic_trace_is_acyclic(turns: &[AgenticTurnTrace]) -> Result<()> {
1397 let mut index_by_id = HashMap::new();
1398 for (idx, turn) in turns.iter().enumerate() {
1399 index_by_id.insert(turn.request_id.as_str(), idx);
1400 }
1401
1402 #[derive(Clone, Copy, PartialEq, Eq)]
1403 enum Mark {
1404 Visiting,
1405 Done,
1406 }
1407
1408 fn visit<'a>(
1409 idx: usize,
1410 turns: &'a [AgenticTurnTrace],
1411 index_by_id: &HashMap<&'a str, usize>,
1412 marks: &mut Vec<Option<Mark>>,
1413 ) -> Result<()> {
1414 match marks[idx] {
1415 Some(Mark::Done) => return Ok(()),
1416 Some(Mark::Visiting) => bail!("cycle detected at request {}", turns[idx].request_id),
1417 None => {}
1418 }
1419 marks[idx] = Some(Mark::Visiting);
1420 for dependency in &turns[idx].wait_for {
1421 let dep_idx = *index_by_id
1422 .get(dependency.as_str())
1423 .expect("dependencies were prevalidated");
1424 visit(dep_idx, turns, index_by_id, marks)?;
1425 }
1426 marks[idx] = Some(Mark::Done);
1427 Ok(())
1428 }
1429
1430 let mut marks = vec![None; turns.len()];
1431 for idx in 0..turns.len() {
1432 visit(idx, turns, &index_by_id, &mut marks)?;
1433 }
1434 Ok(())
1435}
1436
1437fn extend_applied_compute_agentic_hash_ids(
1438 hash_ids: &mut Vec<u64>,
1439 input_length: usize,
1440 trace_block_size: usize,
1441 shared_initial_blocks: usize,
1442 group_id: Option<usize>,
1443 next_unique_hash: &mut u64,
1444) -> Result<()> {
1445 let target_blocks = input_length.div_ceil(trace_block_size);
1446 while hash_ids.len() < target_blocks {
1447 let block_idx = hash_ids.len();
1448 if block_idx < shared_initial_blocks
1449 && let Some(group_id) = group_id
1450 {
1451 hash_ids.push(0xA63E_0000_0000_0000 | ((group_id as u64) << 32) | block_idx as u64);
1452 continue;
1453 }
1454 hash_ids.push(*next_unique_hash);
1455 *next_unique_hash = next_unique_hash
1456 .checked_add(1)
1457 .ok_or_else(|| anyhow!("synthetic hash id overflow"))?;
1458 }
1459 Ok(())
1460}
1461
1462fn sample_delay_ms(spec: &DelaySpec, rng: &mut StdRng) -> Result<f64> {
1463 match spec {
1464 DelaySpec::None => Ok(0.0),
1465 DelaySpec::ConstantMs(delay_ms) => {
1466 if !delay_ms.is_finite() || *delay_ms < 0.0 {
1467 bail!("delay must be a finite non-negative number, got {delay_ms}");
1468 }
1469 Ok(*delay_ms)
1470 }
1471 DelaySpec::ExponentialMs { mean_ms } => {
1472 if !mean_ms.is_finite() || *mean_ms < 0.0 {
1473 bail!("mean_ms must be a finite non-negative number, got {mean_ms}");
1474 }
1475 Ok(sample_exponential_delay_ms(*mean_ms, rng))
1476 }
1477 }
1478}
1479
1480fn sample_length(spec: &LengthSpec, min_value: usize, rng: &mut StdRng) -> usize {
1481 if spec.stddev == 0.0 {
1482 return spec.mean.max(min_value);
1483 }
1484
1485 let stddev = spec.stddev.abs();
1486 let u1 = (1.0 - rng.random::<f64>()).clamp(f64::MIN_POSITIVE, 1.0);
1487 let u2 = rng.random::<f64>();
1488 let z0 = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
1489 let sample = spec.mean as f64 + z0 * stddev;
1490 sample.round().max(min_value as f64) as usize
1491}
1492
1493fn sample_exponential_delay_ms(mean_ms: f64, rng: &mut StdRng) -> f64 {
1494 if mean_ms == 0.0 {
1495 return 0.0;
1496 }
1497 let u = (1.0 - rng.random::<f64>()).clamp(f64::MIN_POSITIVE, 1.0);
1498 -mean_ms * u.ln()
1499}
1500
1501fn local_block_hash_from_id(hash_id: u32, block_size: usize) -> LocalBlockHash {
1502 let tokens: Vec<u32> = (0..block_size).map(|_| hash_id).collect();
1503 let bytes = unsafe {
1504 std::slice::from_raw_parts(
1505 tokens.as_ptr() as *const u8,
1506 std::mem::size_of_val(tokens.as_slice()),
1507 )
1508 };
1509 LocalBlockHash(compute_hash_v2(bytes, XXH3_SEED))
1510}