Skip to main content

dynamo_mocker/loadgen/
trace.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // Clamp input_length to the synthesizable capacity: in the mooncake
317        // trace format, input_length is the full prompt token count which may
318        // exceed hash_ids.len() * block_size (cached portion only).
319        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}