use nmbrs_workload::model::ParsedOp;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SequencerType {
Bucket,
Interval,
Concat,
}
impl SequencerType {
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"bucket" => Ok(Self::Bucket),
"interval" => Ok(Self::Interval),
"concat" => Ok(Self::Concat),
other => Err(format!(
"unknown sequencer type: '{other}', use bucket|interval|concat"
)),
}
}
}
pub struct OpSequence {
ops: Vec<ParsedOp>,
lut: Vec<usize>,
sequencer_type: SequencerType,
}
impl OpSequence {
pub fn build(ops: Vec<ParsedOp>, ratios: &[u64], strategy: SequencerType) -> Self {
assert_eq!(
ops.len(),
ratios.len(),
"ops and ratios must have equal length"
);
assert!(!ops.is_empty(), "must have at least one op");
let lut = match strategy {
SequencerType::Bucket => build_bucket_lut(ratios),
SequencerType::Interval => build_interval_lut(ratios),
SequencerType::Concat => build_concat_lut(ratios),
};
Self {
ops,
lut,
sequencer_type: strategy,
}
}
pub fn uniform(ops: Vec<ParsedOp>) -> Self {
let ratios = vec![1u64; ops.len()];
Self::build(ops, &ratios, SequencerType::Bucket)
}
pub fn from_ops(ops: Vec<ParsedOp>, strategy: SequencerType) -> Self {
let ratios: Vec<u64> = ops
.iter()
.map(|op| {
op.params
.get("ratio")
.and_then(|v| v.as_u64())
.or_else(|| op.op.get("ratio").and_then(|v| v.as_u64()))
.unwrap_or(1)
})
.collect();
Self::build(ops, &ratios, strategy)
}
#[inline]
pub fn get(&self, cycle: u64) -> &ParsedOp {
let idx = self.lut[(cycle as usize) % self.lut.len()];
&self.ops[idx]
}
#[inline]
pub fn get_with_index(&self, cycle: u64) -> (usize, &ParsedOp) {
let idx = self.lut[(cycle as usize) % self.lut.len()];
(idx, &self.ops[idx])
}
pub fn templates(&self) -> &[ParsedOp] {
&self.ops
}
pub fn op_count(&self) -> usize {
self.ops.len()
}
pub fn stanza_length(&self) -> usize {
self.lut.len()
}
pub fn stanza_ops(&self, base_cycle: u64) -> Vec<(&ParsedOp, u64)> {
(0..self.lut.len())
.map(|offset| {
let cycle = base_cycle + offset as u64;
(self.get(cycle), cycle)
})
.collect()
}
pub fn sequencer_type(&self) -> SequencerType {
self.sequencer_type
}
pub fn lut(&self) -> &[usize] {
&self.lut
}
}
fn build_bucket_lut(ratios: &[u64]) -> Vec<usize> {
let total: u64 = ratios.iter().sum();
let mut lut = Vec::with_capacity(total as usize);
let mut remaining: Vec<u64> = ratios.to_vec();
loop {
let mut any_drawn = false;
for (i, rem) in remaining.iter_mut().enumerate() {
if *rem > 0 {
lut.push(i);
*rem -= 1;
any_drawn = true;
}
}
if !any_drawn {
break;
}
}
lut
}
fn build_interval_lut(ratios: &[u64]) -> Vec<usize> {
let total: u64 = ratios.iter().sum();
if total == 0 {
return Vec::new();
}
let mut entries: Vec<(f64, usize, usize)> = Vec::new(); for (i, &ratio) in ratios.iter().enumerate() {
if ratio == 0 {
continue;
}
let spacing = total as f64 / ratio as f64;
for j in 0..ratio as usize {
entries.push((j as f64 * spacing, i, j));
}
}
entries.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.1.cmp(&b.1))
});
entries.iter().map(|&(_, idx, _)| idx).collect()
}
fn build_concat_lut(ratios: &[u64]) -> Vec<usize> {
let mut lut = Vec::new();
for (i, &ratio) in ratios.iter().enumerate() {
for _ in 0..ratio {
lut.push(i);
}
}
lut
}
#[cfg(test)]
mod tests {
use super::*;
fn names(seq: &OpSequence, count: usize) -> Vec<String> {
(0..count as u64).map(|c| seq.get(c).name.clone()).collect()
}
#[test]
fn bucket_uniform() {
let ops = vec![
ParsedOp::simple("A", "a"),
ParsedOp::simple("B", "b"),
ParsedOp::simple("C", "c"),
];
let seq = OpSequence::build(ops, &[1, 1, 1], SequencerType::Bucket);
assert_eq!(seq.stanza_length(), 3);
assert_eq!(names(&seq, 6), vec!["A", "B", "C", "A", "B", "C"]);
}
#[test]
fn bucket_weighted() {
let ops = vec![
ParsedOp::simple("R", "read"),
ParsedOp::simple("W", "write"),
ParsedOp::simple("D", "delete"),
];
let seq = OpSequence::build(ops, &[3, 2, 1], SequencerType::Bucket);
assert_eq!(seq.stanza_length(), 6);
let n = names(&seq, 6);
assert_eq!(n.iter().filter(|s| *s == "R").count(), 3);
assert_eq!(n.iter().filter(|s| *s == "W").count(), 2);
assert_eq!(n.iter().filter(|s| *s == "D").count(), 1);
}
#[test]
fn bucket_interleaves() {
let ops = vec![ParsedOp::simple("A", "a"), ParsedOp::simple("B", "b")];
let seq = OpSequence::build(ops, &[3, 1], SequencerType::Bucket);
assert_eq!(seq.lut(), &[0, 1, 0, 0]);
}
#[test]
fn interval_uniform() {
let ops = vec![ParsedOp::simple("A", "a"), ParsedOp::simple("B", "b")];
let seq = OpSequence::build(ops, &[1, 1], SequencerType::Interval);
assert_eq!(seq.stanza_length(), 2);
assert_eq!(names(&seq, 4), vec!["A", "B", "A", "B"]);
}
#[test]
fn interval_weighted() {
let ops = vec![ParsedOp::simple("A", "a"), ParsedOp::simple("B", "b")];
let seq = OpSequence::build(ops, &[4, 2], SequencerType::Interval);
assert_eq!(seq.stanza_length(), 6);
let n = names(&seq, 6);
assert_eq!(n.iter().filter(|s| *s == "A").count(), 4);
assert_eq!(n.iter().filter(|s| *s == "B").count(), 2);
}
#[test]
fn interval_distributes_evenly() {
let ops = vec![ParsedOp::simple("A", "a"), ParsedOp::simple("B", "b")];
let seq = OpSequence::build(ops, &[3, 3], SequencerType::Interval);
assert_eq!(seq.stanza_length(), 6);
assert_eq!(names(&seq, 6), vec!["A", "B", "A", "B", "A", "B"]);
}
#[test]
fn concat_sequential() {
let ops = vec![
ParsedOp::simple("A", "a"),
ParsedOp::simple("B", "b"),
ParsedOp::simple("C", "c"),
];
let seq = OpSequence::build(ops, &[3, 2, 1], SequencerType::Concat);
assert_eq!(seq.stanza_length(), 6);
assert_eq!(names(&seq, 6), vec!["A", "A", "A", "B", "B", "C"]);
}
#[test]
fn concat_wraps() {
let ops = vec![ParsedOp::simple("X", "x"), ParsedOp::simple("Y", "y")];
let seq = OpSequence::build(ops, &[2, 1], SequencerType::Concat);
assert_eq!(names(&seq, 6), vec!["X", "X", "Y", "X", "X", "Y"]);
}
#[test]
fn from_ops_extracts_ratios() {
let mut op1 = ParsedOp::simple("read", "SELECT");
op1.params.insert("ratio".into(), serde_json::json!(5));
let op2 = ParsedOp::simple("write", "INSERT");
let seq = OpSequence::from_ops(vec![op1, op2], SequencerType::Bucket);
assert_eq!(seq.stanza_length(), 6); }
#[test]
fn sequencer_type_parse() {
assert_eq!(
SequencerType::parse("bucket").unwrap(),
SequencerType::Bucket
);
assert_eq!(
SequencerType::parse("INTERVAL").unwrap(),
SequencerType::Interval
);
assert_eq!(
SequencerType::parse("Concat").unwrap(),
SequencerType::Concat
);
assert!(SequencerType::parse("bogus").is_err());
}
#[test]
fn single_op_any_strategy() {
for strategy in [
SequencerType::Bucket,
SequencerType::Interval,
SequencerType::Concat,
] {
let ops = vec![ParsedOp::simple("only", "SELECT 1")];
let seq = OpSequence::build(ops, &[1], strategy);
assert_eq!(seq.get(0).name, "only");
assert_eq!(seq.get(999).name, "only");
}
}
#[test]
fn stanza_repeats_cleanly() {
let ops = vec![ParsedOp::simple("A", "a"), ParsedOp::simple("B", "b")];
let seq = OpSequence::build(ops, &[2, 1], SequencerType::Bucket);
let stanza = seq.stanza_length();
let first: Vec<String> = (0..stanza as u64)
.map(|c| seq.get(c).name.clone())
.collect();
let second: Vec<String> = (stanza as u64..2 * stanza as u64)
.map(|c| seq.get(c).name.clone())
.collect();
assert_eq!(first, second);
}
}