///|
/// Offline switched-fabric queue/capacity/load model for L3F.

///|
pub(all) enum SwitchedFabricQueueDiscipline {
  FifoQueue
  StrictPriorityQueue
} derive(Eq, Debug)

///|
pub fn SwitchedFabricQueueDiscipline::label(
  self : SwitchedFabricQueueDiscipline,
) -> String {
  match self {
    FifoQueue => "fifo"
    StrictPriorityQueue => "strict-priority"
  }
}

///|
pub(all) struct SwitchedFabricPort {
  port_id : @core.PortId
  endpoint : @core.EndpointId
  name : String
  capacity_octets_per_tick : Int
  queue_limit_octets : Int
  base_delay : @core.Duration
} derive(Eq, Debug)

///|
pub fn SwitchedFabricPort::new(
  port_id~ : @core.PortId,
  endpoint~ : @core.EndpointId,
  name~ : String,
  capacity_octets_per_tick~ : Int,
  queue_limit_octets~ : Int,
  base_delay~ : @core.Duration,
) -> SwitchedFabricPort {
  {
    port_id,
    endpoint,
    name,
    capacity_octets_per_tick,
    queue_limit_octets,
    base_delay,
  }
}

///|
pub(all) struct SwitchedFabricTrafficFlow {
  flow_id : Int
  source : @core.EndpointId
  target : @core.EndpointId
  label : String
  priority : Int
  frame_size_octets : Int
  start_ns : Int64
  period_ns : Int64
  burst_count : Int
  background : Bool
} derive(Eq, Debug)

///|
pub fn SwitchedFabricTrafficFlow::is_background(
  self : SwitchedFabricTrafficFlow,
) -> Bool {
  self.background
}

///|
pub(all) struct SwitchedFabricOfflineProfile {
  name : String
  queue_discipline : SwitchedFabricQueueDiscipline
  ports : Array[SwitchedFabricPort]
  flows : Array[SwitchedFabricTrafficFlow]
  reorder_window_ns : Int64
  complete_switched_fabric : Bool
  live_io_evidence : Bool
  real_netload_evidence : Bool
} derive(Eq, Debug)

///|
pub fn SwitchedFabricOfflineProfile::background_flow_count(
  self : SwitchedFabricOfflineProfile,
) -> Int {
  let mut count = 0
  for flow in self.flows {
    if flow.is_background() {
      count += 1
    }
  }
  count
}

///|
pub(all) struct SwitchedFabricFrameRecord {
  record_id : Int
  flow_id : Int
  flow_label : String
  source : @core.EndpointId
  target : @core.EndpointId
  output_port : @core.PortId
  priority : Int
  frame_size_octets : Int
  enqueue_time : @core.VTime
  depart_time : @core.VTime?
  arrive_time : @core.VTime?
  serialization_delay : @core.Duration
  queue_occupancy_before : Int
  queue_occupancy_after : Int
  jitter_ns : Int64
  dropped : Bool
  reordered : Bool
  background : Bool
} derive(Eq, Debug)

///|
pub(all) struct SwitchedFabricOfflineRun {
  seed : Int
  profile : SwitchedFabricOfflineProfile
  frames : Array[SwitchedFabricFrameRecord]
  trace : @trace.TraceLog
  digest : @core.SimDigest
  replay_count : Int
  delivered_count : Int
  dropped_count : Int
  reordered_count : Int
  jittered_count : Int
  background_frame_count : Int
  total_payload_octets : Int
  total_background_octets : Int
  netload_octets : Int
  max_queue_occupancy_octets : Int
} derive(Debug)

///|
pub fn SwitchedFabricOfflineRun::passes(
  self : SwitchedFabricOfflineRun,
) -> Bool {
  self.profile.complete_switched_fabric &&
  !self.profile.live_io_evidence &&
  !self.profile.real_netload_evidence &&
  self.frames.length() >= 5 &&
  self.delivered_count > 0 &&
  self.dropped_count > 0 &&
  self.reordered_count > 0 &&
  self.jittered_count > 0 &&
  self.background_frame_count > 0 &&
  self.netload_octets ==
  self.total_payload_octets + self.total_background_octets &&
  self.max_queue_occupancy_octets > 0 &&
  self.trace.len() > self.frames.length() &&
  self.replay_count == self.trace.len()
}

///|
pub(all) struct SwitchedFabricOfflineReport {
  profile_name : String
  seed : Int
  queue_discipline : SwitchedFabricQueueDiscipline
  trace_event_count : Int
  replay_count : Int
  same_seed_stable : Bool
  digest : @core.SimDigest
  delivered_count : Int
  dropped_count : Int
  reordered_count : Int
  jittered_count : Int
  background_frame_count : Int
  netload_octets : Int
  max_queue_occupancy_octets : Int
  complete_switched_fabric : Bool
  live_io_evidence : Bool
  real_netload_evidence : Bool
} derive(Eq, Debug)

///|
pub fn SwitchedFabricOfflineReport::passes(
  self : SwitchedFabricOfflineReport,
) -> Bool {
  self.trace_event_count > 0 &&
  self.replay_count == self.trace_event_count &&
  self.same_seed_stable &&
  self.delivered_count > 0 &&
  self.dropped_count > 0 &&
  self.reordered_count > 0 &&
  self.jittered_count > 0 &&
  self.background_frame_count > 0 &&
  self.netload_octets > 0 &&
  self.max_queue_occupancy_octets > 0 &&
  self.complete_switched_fabric &&
  !self.live_io_evidence &&
  !self.real_netload_evidence
}

///|
pub fn switched_fabric_offline_profile(
  seed? : Int = 704,
) -> SwitchedFabricOfflineProfile {
  {
    name: "switched-fabric-offline-\{switched_normalized_seed(seed)}",
    queue_discipline: FifoQueue,
    ports: [
      SwitchedFabricPort::new(
        port_id=@core.PortId(1),
        endpoint=@core.EndpointId(1),
        name="controller",
        capacity_octets_per_tick=96,
        queue_limit_octets=512,
        base_delay=@core.Duration::from_ns(5L),
      ),
      SwitchedFabricPort::new(
        port_id=@core.PortId(2),
        endpoint=@core.EndpointId(2),
        name="drive-a",
        capacity_octets_per_tick=64,
        queue_limit_octets=256,
        base_delay=@core.Duration::from_ns(8L),
      ),
      SwitchedFabricPort::new(
        port_id=@core.PortId(3),
        endpoint=@core.EndpointId(3),
        name="drive-b",
        capacity_octets_per_tick=64,
        queue_limit_octets=256,
        base_delay=@core.Duration::from_ns(8L),
      ),
    ],
    flows: [
      {
        flow_id: 2,
        source: @core.EndpointId(3),
        target: @core.EndpointId(1),
        label: "background.diagnostic.burst",
        priority: 4,
        frame_size_octets: 192,
        start_ns: 55L,
        period_ns: 90L,
        burst_count: 2,
        background: true,
      },
      {
        flow_id: 1,
        source: @core.EndpointId(1),
        target: @core.EndpointId(2),
        label: "cyclic.control",
        priority: 1,
        frame_size_octets: 80,
        start_ns: 10L,
        period_ns: 20L,
        burst_count: 3,
        background: false,
      },
      {
        flow_id: 3,
        source: @core.EndpointId(2),
        target: @core.EndpointId(1),
        label: "acyclic.record",
        priority: 3,
        frame_size_octets: 144,
        start_ns: 70L,
        period_ns: 0L,
        burst_count: 1,
        background: true,
      },
      {
        flow_id: 4,
        source: @core.EndpointId(1),
        target: @core.EndpointId(3),
        label: "oversize.queue-drop",
        priority: 2,
        frame_size_octets: 384,
        start_ns: 12L,
        period_ns: 0L,
        burst_count: 1,
        background: false,
      },
    ],
    reorder_window_ns: 12L,
    complete_switched_fabric: true,
    live_io_evidence: false,
    real_netload_evidence: false,
  }
}

///|
pub fn run_switched_fabric_offline(
  seed? : Int = 704,
) -> SwitchedFabricOfflineRun {
  let profile = switched_fabric_offline_profile(seed~)
  let frames : Array[SwitchedFabricFrameRecord] = []
  let trace = @trace.TraceLog::new()
  let queue_end_by_port : Array[Int64] = []
  let queue_occupancy_by_port : Array[Int] = []
  for _port in profile.ports {
    queue_end_by_port.push(0L)
    queue_occupancy_by_port.push(0)
  }
  let mut record_id = 1
  let mut event_id = 1
  let mut delivered_count = 0
  let mut dropped_count = 0
  let mut reordered_count = 0
  let mut jittered_count = 0
  let mut background_frame_count = 0
  let mut total_payload_octets = 0
  let mut total_background_octets = 0
  let mut max_queue_occupancy_octets = 0
  let mut last_arrive_ns = 0L
  for flow in profile.flows {
    let bursts = if flow.burst_count <= 0 { 1 } else { flow.burst_count }
    for burst in 0.. enqueue_ns {
        queue_occupancy_by_port[port_index]
      } else {
        0
      }
      let dropped = queue_before + flow.frame_size_octets >
        port.queue_limit_octets
      let serialization = serialization_delay(
        flow.frame_size_octets,
        port.capacity_octets_per_tick,
      )
      let depart_ns = if dropped {
        0L
      } else {
        max_i64(enqueue_ns, queue_end_by_port[port_index]) + serialization.ns()
      }
      let arrive_ns = if dropped {
        0L
      } else {
        depart_ns + port.base_delay.ns() + jitter_ns
      }
      let reordered = !dropped &&
        last_arrive_ns > 0L &&
        arrive_ns + profile.reorder_window_ns < last_arrive_ns
      let queue_after = if dropped {
        queue_before
      } else {
        min_int(port.queue_limit_octets, queue_before + flow.frame_size_octets)
      }
      let record : SwitchedFabricFrameRecord = {
        record_id,
        flow_id: flow.flow_id,
        flow_label: flow.label,
        source: flow.source,
        target: flow.target,
        output_port: port.port_id,
        priority: flow.priority,
        frame_size_octets: flow.frame_size_octets,
        enqueue_time: @core.VTime::from_ns(enqueue_ns),
        depart_time: if dropped {
          None
        } else {
          Some(@core.VTime::from_ns(depart_ns))
        },
        arrive_time: if dropped {
          None
        } else {
          Some(@core.VTime::from_ns(arrive_ns))
        },
        serialization_delay: serialization,
        queue_occupancy_before: queue_before,
        queue_occupancy_after: queue_after,
        jitter_ns,
        dropped,
        reordered,
        background: flow.background,
      }
      frames.push(record)
      event_id = append_switched_frame_trace(trace, record, seed~, event_id~)
      if dropped {
        dropped_count += 1
      } else {
        delivered_count += 1
        queue_end_by_port[port_index] = depart_ns
        queue_occupancy_by_port[port_index] = flow.frame_size_octets
        last_arrive_ns = arrive_ns
      }
      if reordered {
        reordered_count += 1
      }
      if jitter_ns > 0L {
        jittered_count += 1
      }
      if flow.background {
        background_frame_count += 1
        total_background_octets += flow.frame_size_octets
      } else {
        total_payload_octets += flow.frame_size_octets
      }
      if queue_after > max_queue_occupancy_octets {
        max_queue_occupancy_octets = queue_after
      }
      record_id += 1
    }
  }
  append_switched_netload_trace(
    trace,
    event_id~,
    seed~,
    total_payload_octets~,
    total_background_octets~,
    max_queue_occupancy_octets~,
  )
  let digest = trace.portable_digest(seed~)
  {
    seed,
    profile,
    frames,
    trace,
    digest,
    replay_count: switched_replay_count(trace),
    delivered_count,
    dropped_count,
    reordered_count,
    jittered_count,
    background_frame_count,
    total_payload_octets,
    total_background_octets,
    netload_octets: total_payload_octets + total_background_octets,
    max_queue_occupancy_octets,
  }
}

///|
pub fn switched_fabric_offline_report(
  seed? : Int = 704,
) -> SwitchedFabricOfflineReport {
  let run = run_switched_fabric_offline(seed~)
  let same_seed = run.digest == run_switched_fabric_offline(seed~).digest
  {
    profile_name: run.profile.name,
    seed,
    queue_discipline: run.profile.queue_discipline,
    trace_event_count: run.trace.len(),
    replay_count: run.replay_count,
    same_seed_stable: same_seed,
    digest: run.digest,
    delivered_count: run.delivered_count,
    dropped_count: run.dropped_count,
    reordered_count: run.reordered_count,
    jittered_count: run.jittered_count,
    background_frame_count: run.background_frame_count,
    netload_octets: run.netload_octets,
    max_queue_occupancy_octets: run.max_queue_occupancy_octets,
    complete_switched_fabric: run.profile.complete_switched_fabric,
    live_io_evidence: run.profile.live_io_evidence,
    real_netload_evidence: run.profile.real_netload_evidence,
  }
}

///|
pub(all) enum SwitchedFabricIrtPhaseKind {
  IrtGreenPhase
  IrtRedPhase
} derive(Eq, Debug)

///|
pub fn SwitchedFabricIrtPhaseKind::label(
  self : SwitchedFabricIrtPhaseKind,
) -> String {
  match self {
    IrtGreenPhase => "green"
    IrtRedPhase => "red"
  }
}

///|
pub(all) struct SwitchedFabricIrtPhaseWindow {
  start_ns : Int64
  end_ns : Int64
  phase : SwitchedFabricIrtPhaseKind
} derive(Eq, Debug)

///|
pub(all) struct SwitchedFabricQosIrtProfile {
  name : String
  queue_discipline : SwitchedFabricQueueDiscipline
  ports : Array[SwitchedFabricPort]
  flows : Array[SwitchedFabricTrafficFlow]
  phase_table : Array[SwitchedFabricIrtPhaseWindow]
  red_phase_drop_priority_floor : Int
  complete_switched_fabric : Bool
  live_io_evidence : Bool
  real_netload_evidence : Bool
  real_irt_evidence : Bool
} derive(Eq, Debug)

///|
pub(all) struct SwitchedFabricQosIrtFrameRecord {
  record_id : Int
  flow_id : Int
  flow_label : String
  output_port : @core.PortId
  priority : Int
  enqueue_time : @core.VTime
  effective_enqueue_time : @core.VTime
  depart_time : @core.VTime?
  arrive_time : @core.VTime?
  phase_at_enqueue : SwitchedFabricIrtPhaseKind
  held_by_red_phase : Bool
  dropped_by_red_phase : Bool
  released_in_green_phase : Bool
  queue_occupancy_before : Int
  queue_occupancy_after : Int
  frame_size_octets : Int
  background : Bool
} derive(Eq, Debug)

///|
pub(all) struct SwitchedFabricQosIrtRun {
  seed : Int
  profile : SwitchedFabricQosIrtProfile
  frames : Array[SwitchedFabricQosIrtFrameRecord]
  trace : @trace.TraceLog
  digest : @core.SimDigest
  replay_count : Int
  delivered_count : Int
  dropped_count : Int
  red_phase_hold_count : Int
  red_phase_drop_count : Int
  green_phase_release_count : Int
  strict_priority_selection_count : Int
  netload_octets : Int
  high_priority_before_low_priority : Bool
} derive(Debug)

///|
pub fn SwitchedFabricQosIrtRun::passes(self : SwitchedFabricQosIrtRun) -> Bool {
  self.profile.queue_discipline == StrictPriorityQueue &&
  self.profile.complete_switched_fabric &&
  !self.profile.live_io_evidence &&
  !self.profile.real_netload_evidence &&
  !self.profile.real_irt_evidence &&
  self.frames.length() >= 5 &&
  self.delivered_count > 0 &&
  self.dropped_count > 0 &&
  self.red_phase_hold_count > 0 &&
  self.red_phase_drop_count > 0 &&
  self.green_phase_release_count > 0 &&
  self.strict_priority_selection_count > 0 &&
  self.high_priority_before_low_priority &&
  self.netload_octets > 0 &&
  self.trace.len() > self.frames.length() &&
  self.replay_count == self.trace.len()
}

///|
pub(all) struct SwitchedFabricQosIrtReport {
  profile_name : String
  seed : Int
  queue_discipline : SwitchedFabricQueueDiscipline
  trace_event_count : Int
  replay_count : Int
  same_seed_stable : Bool
  digest : @core.SimDigest
  delivered_count : Int
  dropped_count : Int
  red_phase_hold_count : Int
  red_phase_drop_count : Int
  green_phase_release_count : Int
  strict_priority_selection_count : Int
  netload_octets : Int
  high_priority_before_low_priority : Bool
  complete_switched_fabric : Bool
  live_io_evidence : Bool
  real_netload_evidence : Bool
  real_irt_evidence : Bool
} derive(Eq, Debug)

///|
pub fn SwitchedFabricQosIrtReport::passes(
  self : SwitchedFabricQosIrtReport,
) -> Bool {
  self.trace_event_count > 0 &&
  self.replay_count == self.trace_event_count &&
  self.same_seed_stable &&
  self.queue_discipline == StrictPriorityQueue &&
  self.delivered_count > 0 &&
  self.dropped_count > 0 &&
  self.red_phase_hold_count > 0 &&
  self.red_phase_drop_count > 0 &&
  self.green_phase_release_count > 0 &&
  self.strict_priority_selection_count > 0 &&
  self.high_priority_before_low_priority &&
  self.netload_octets > 0 &&
  self.complete_switched_fabric &&
  !self.live_io_evidence &&
  !self.real_netload_evidence &&
  !self.real_irt_evidence
}

///|
pub fn switched_fabric_qos_irt_profile(
  seed? : Int = 734,
) -> SwitchedFabricQosIrtProfile {
  {
    name: "switched-fabric-qos-irt-\{switched_normalized_seed(seed)}",
    queue_discipline: StrictPriorityQueue,
    ports: [
      SwitchedFabricPort::new(
        port_id=@core.PortId(1),
        endpoint=@core.EndpointId(1),
        name="controller",
        capacity_octets_per_tick=96,
        queue_limit_octets=512,
        base_delay=@core.Duration::from_ns(5L),
      ),
      SwitchedFabricPort::new(
        port_id=@core.PortId(2),
        endpoint=@core.EndpointId(2),
        name="drive-a",
        capacity_octets_per_tick=64,
        queue_limit_octets=256,
        base_delay=@core.Duration::from_ns(8L),
      ),
    ],
    flows: [
      {
        flow_id: 10,
        source: @core.EndpointId(3),
        target: @core.EndpointId(2),
        label: "background.low.same-slot",
        priority: 7,
        frame_size_octets: 96,
        start_ns: 10L,
        period_ns: 0L,
        burst_count: 1,
        background: true,
      },
      {
        flow_id: 11,
        source: @core.EndpointId(1),
        target: @core.EndpointId(2),
        label: "cyclic.high.same-slot",
        priority: 1,
        frame_size_octets: 64,
        start_ns: 10L,
        period_ns: 0L,
        burst_count: 1,
        background: false,
      },
      {
        flow_id: 12,
        source: @core.EndpointId(1),
        target: @core.EndpointId(2),
        label: "irt.red.held",
        priority: 1,
        frame_size_octets: 80,
        start_ns: 24L,
        period_ns: 0L,
        burst_count: 1,
        background: false,
      },
      {
        flow_id: 13,
        source: @core.EndpointId(3),
        target: @core.EndpointId(2),
        label: "netload.red.drop",
        priority: 6,
        frame_size_octets: 192,
        start_ns: 30L,
        period_ns: 0L,
        burst_count: 1,
        background: true,
      },
      {
        flow_id: 14,
        source: @core.EndpointId(1),
        target: @core.EndpointId(2),
        label: "irt.green.release",
        priority: 2,
        frame_size_octets: 72,
        start_ns: 46L,
        period_ns: 0L,
        burst_count: 1,
        background: false,
      },
    ],
    phase_table: [
      { start_ns: 0L, end_ns: 20L, phase: IrtGreenPhase },
      { start_ns: 20L, end_ns: 40L, phase: IrtRedPhase },
      { start_ns: 40L, end_ns: 90L, phase: IrtGreenPhase },
    ],
    red_phase_drop_priority_floor: 3,
    complete_switched_fabric: true,
    live_io_evidence: false,
    real_netload_evidence: false,
    real_irt_evidence: false,
  }
}

///|
pub fn run_switched_fabric_qos_irt_offline(
  seed? : Int = 734,
) -> SwitchedFabricQosIrtRun {
  let profile = switched_fabric_qos_irt_profile(seed~)
  let ordered = ordered_qos_irt_flows(profile)
  let frames : Array[SwitchedFabricQosIrtFrameRecord] = []
  let trace = @trace.TraceLog::new()
  let queue_end_by_port : Array[Int64] = []
  let queue_occupancy_by_port : Array[Int] = []
  for _port in profile.ports {
    queue_end_by_port.push(0L)
    queue_occupancy_by_port.push(0)
  }
  let mut event_id = 2000
  let mut record_id = 1
  let mut delivered_count = 0
  let mut dropped_count = 0
  let mut red_phase_hold_count = 0
  let mut red_phase_drop_count = 0
  let mut green_phase_release_count = 0
  let strict_priority_selection_count = strict_priority_same_slot_count(profile)
  let mut netload_octets = 0
  for flow in ordered {
    let port_index = find_qos_irt_port_index(profile, flow.target)
    let port = profile.ports[port_index]
    let phase = phase_at(profile, flow.start_ns)
    let held_by_red_phase = phase == IrtRedPhase &&
      flow.priority < profile.red_phase_drop_priority_floor
    let dropped_by_red_phase = phase == IrtRedPhase &&
      flow.priority >= profile.red_phase_drop_priority_floor
    let effective_enqueue_ns = if held_by_red_phase {
      next_green_start_ns(profile, flow.start_ns)
    } else {
      flow.start_ns
    }
    let queue_before = if queue_end_by_port[port_index] > effective_enqueue_ns {
      queue_occupancy_by_port[port_index]
    } else {
      0
    }
    let serialization = serialization_delay(
      flow.frame_size_octets,
      port.capacity_octets_per_tick,
    )
    let depart_ns = if dropped_by_red_phase {
      0L
    } else {
      max_i64(effective_enqueue_ns, queue_end_by_port[port_index]) +
      serialization.ns()
    }
    let arrive_ns = if dropped_by_red_phase {
      0L
    } else {
      depart_ns + port.base_delay.ns()
    }
    let released_in_green_phase = held_by_red_phase &&
      phase_at(profile, effective_enqueue_ns) == IrtGreenPhase
    let queue_after = if dropped_by_red_phase {
      queue_before
    } else {
      min_int(port.queue_limit_octets, queue_before + flow.frame_size_octets)
    }
    let record : SwitchedFabricQosIrtFrameRecord = {
      record_id,
      flow_id: flow.flow_id,
      flow_label: flow.label,
      output_port: port.port_id,
      priority: flow.priority,
      enqueue_time: @core.VTime::from_ns(flow.start_ns),
      effective_enqueue_time: @core.VTime::from_ns(effective_enqueue_ns),
      depart_time: if dropped_by_red_phase {
        None
      } else {
        Some(@core.VTime::from_ns(depart_ns))
      },
      arrive_time: if dropped_by_red_phase {
        None
      } else {
        Some(@core.VTime::from_ns(arrive_ns))
      },
      phase_at_enqueue: phase,
      held_by_red_phase,
      dropped_by_red_phase,
      released_in_green_phase,
      queue_occupancy_before: queue_before,
      queue_occupancy_after: queue_after,
      frame_size_octets: flow.frame_size_octets,
      background: flow.background,
    }
    frames.push(record)
    event_id = append_qos_irt_frame_trace(trace, record, seed~, event_id~)
    if dropped_by_red_phase {
      dropped_count += 1
      red_phase_drop_count += 1
    } else {
      delivered_count += 1
      queue_end_by_port[port_index] = depart_ns
      queue_occupancy_by_port[port_index] = flow.frame_size_octets
    }
    if held_by_red_phase {
      red_phase_hold_count += 1
    }
    if released_in_green_phase {
      green_phase_release_count += 1
    }
    netload_octets += flow.frame_size_octets
    record_id += 1
  }
  append_qos_irt_summary_trace(
    trace,
    event_id~,
    seed~,
    red_phase_hold_count~,
    red_phase_drop_count~,
    green_phase_release_count~,
  )
  let digest = trace.portable_digest(seed~)
  {
    seed,
    profile,
    frames,
    trace,
    digest,
    replay_count: switched_replay_count(trace),
    delivered_count,
    dropped_count,
    red_phase_hold_count,
    red_phase_drop_count,
    green_phase_release_count,
    strict_priority_selection_count,
    netload_octets,
    high_priority_before_low_priority: high_priority_before_low_priority(frames),
  }
}

///|
pub fn switched_fabric_qos_irt_report(
  seed? : Int = 734,
) -> SwitchedFabricQosIrtReport {
  let run = run_switched_fabric_qos_irt_offline(seed~)
  let same_seed = run.digest ==
    run_switched_fabric_qos_irt_offline(seed~).digest
  {
    profile_name: run.profile.name,
    seed,
    queue_discipline: run.profile.queue_discipline,
    trace_event_count: run.trace.len(),
    replay_count: run.replay_count,
    same_seed_stable: same_seed,
    digest: run.digest,
    delivered_count: run.delivered_count,
    dropped_count: run.dropped_count,
    red_phase_hold_count: run.red_phase_hold_count,
    red_phase_drop_count: run.red_phase_drop_count,
    green_phase_release_count: run.green_phase_release_count,
    strict_priority_selection_count: run.strict_priority_selection_count,
    netload_octets: run.netload_octets,
    high_priority_before_low_priority: run.high_priority_before_low_priority,
    complete_switched_fabric: run.profile.complete_switched_fabric,
    live_io_evidence: run.profile.live_io_evidence,
    real_netload_evidence: run.profile.real_netload_evidence,
    real_irt_evidence: run.profile.real_irt_evidence,
  }
}

///|
pub fn SwitchedFabricQosIrtRun::timeline_text(
  self : SwitchedFabricQosIrtRun,
) -> String {
  let buf = StringBuilder::new()
  buf.write_string("fabric=switched-qos-irt-offline|seed=")
  buf.write_string(self.seed.to_string())
  buf.write_string("|queue=")
  buf.write_string(self.profile.queue_discipline.label())
  buf.write_string("|frames=")
  buf.write_string(self.frames.length().to_string())
  buf.write_string("|red_hold=")
  buf.write_string(self.red_phase_hold_count.to_string())
  buf.write_string("|red_drop=")
  buf.write_string(self.red_phase_drop_count.to_string())
  buf.write_string("|green_release=")
  buf.write_string(self.green_phase_release_count.to_string())
  buf.write_string("|strict_priority=")
  buf.write_string(self.strict_priority_selection_count.to_string())
  for frame in self.frames {
    buf.write_char('\n')
    buf.write_string("frame=")
    buf.write_string(frame.record_id.to_string())
    buf.write_string("|flow=")
    buf.write_string(frame.flow_label)
    buf.write_string("|priority=")
    buf.write_string(frame.priority.to_string())
    buf.write_string("|phase=")
    buf.write_string(frame.phase_at_enqueue.label())
    buf.write_string("|enqueue_ns=")
    buf.write_string(frame.enqueue_time.ns().to_string())
    buf.write_string("|effective_enqueue_ns=")
    buf.write_string(frame.effective_enqueue_time.ns().to_string())
    buf.write_string("|red_hold=")
    buf.write_string(frame.held_by_red_phase.to_string())
    buf.write_string("|red_drop=")
    buf.write_string(frame.dropped_by_red_phase.to_string())
    buf.write_string("|green_release=")
    buf.write_string(frame.released_in_green_phase.to_string())
  }
  buf.to_string()
}

///|
pub fn SwitchedFabricOfflineRun::timeline_text(
  self : SwitchedFabricOfflineRun,
) -> String {
  let buf = StringBuilder::new()
  buf.write_string("fabric=switched-offline|seed=")
  buf.write_string(self.seed.to_string())
  buf.write_string("|queue=")
  buf.write_string(self.profile.queue_discipline.label())
  buf.write_string("|frames=")
  buf.write_string(self.frames.length().to_string())
  buf.write_string("|delivered=")
  buf.write_string(self.delivered_count.to_string())
  buf.write_string("|dropped=")
  buf.write_string(self.dropped_count.to_string())
  buf.write_string("|reordered=")
  buf.write_string(self.reordered_count.to_string())
  buf.write_string("|netload_octets=")
  buf.write_string(self.netload_octets.to_string())
  for frame in self.frames {
    buf.write_char('\n')
    buf.write_string("frame=")
    buf.write_string(frame.record_id.to_string())
    buf.write_string("|flow=")
    buf.write_string(frame.flow_label)
    buf.write_string("|port=")
    buf.write_string(frame.output_port_text())
    buf.write_string("|enqueue_ns=")
    buf.write_string(frame.enqueue_time.ns().to_string())
    buf.write_string("|queue_before=")
    buf.write_string(frame.queue_occupancy_before.to_string())
    buf.write_string("|dropped=")
    buf.write_string(frame.dropped.to_string())
    buf.write_string("|reordered=")
    buf.write_string(frame.reordered.to_string())
  }
  buf.to_string()
}

///|
fn SwitchedFabricFrameRecord::output_port_text(
  self : SwitchedFabricFrameRecord,
) -> String {
  match self.output_port {
    @core.PortId(value) => value.to_string()
  }
}

///|
fn append_switched_frame_trace(
  trace : @trace.TraceLog,
  frame : SwitchedFabricFrameRecord,
  seed~ : Int,
  event_id~ : Int,
) -> Int {
  let mut next = event_id
  trace.append(
    switched_trace_event(
      event_id=next,
      parent_id=None,
      vtime=frame.enqueue_time,
      direction=@trace.Tx,
      payload_digest=Some(frame.frame_size_octets),
      seed~,
      label="fabric.switched.tx." + frame.flow_label,
    ),
  )
  next += 1
  trace.append(
    switched_trace_event(
      event_id=next,
      parent_id=Some(frame.record_id),
      vtime=frame.enqueue_time,
      direction=@trace.Resource,
      payload_digest=Some(frame.queue_occupancy_after),
      seed~,
      label="fabric.switched.queue." + frame.flow_label,
    ),
  )
  next += 1
  if frame.dropped {
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=frame.enqueue_time,
        direction=@trace.Fault,
        payload_digest=Some(frame.queue_occupancy_after),
        seed~,
        label="fabric.switched.drop.queue",
      ),
    )
    next += 1
  } else {
    guard frame.arrive_time is Some(arrive) else { return next }
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=arrive,
        direction=@trace.Rx,
        payload_digest=Some(frame.frame_size_octets),
        seed~,
        label="fabric.switched.rx." + frame.flow_label,
      ),
    )
    next += 1
    if frame.reordered {
      trace.append(
        switched_trace_event(
          event_id=next,
          parent_id=Some(frame.record_id),
          vtime=arrive,
          direction=@trace.Probe,
          payload_digest=Some(frame.priority),
          seed~,
          label="fabric.switched.reorder",
        ),
      )
      next += 1
    }
  }
  next
}

///|
fn append_switched_netload_trace(
  trace : @trace.TraceLog,
  event_id~ : Int,
  seed~ : Int,
  total_payload_octets~ : Int,
  total_background_octets~ : Int,
  max_queue_occupancy_octets~ : Int,
) -> Unit {
  trace.append(
    switched_trace_event(
      event_id~,
      parent_id=None,
      vtime=@core.VTime::from_ns(999L),
      direction=@trace.Probe,
      payload_digest=Some(
        total_payload_octets +
        total_background_octets +
        max_queue_occupancy_octets,
      ),
      seed~,
      label="fabric.switched.netload.accounting",
    ),
  )
}

///|
fn ordered_qos_irt_flows(
  profile : SwitchedFabricQosIrtProfile,
) -> Array[SwitchedFabricTrafficFlow] {
  let ordered : Array[SwitchedFabricTrafficFlow] = []
  let used : Array[Bool] = []
  for _flow in profile.flows {
    used.push(false)
  }
  while ordered.length() < profile.flows.length() {
    let mut selected = -1
    for index, flow in profile.flows {
      if !used[index] &&
        (selected < 0 || qos_irt_flow_precedes(flow, profile.flows[selected])) {
        selected = index
      }
    }
    if selected >= 0 {
      ordered.push(profile.flows[selected])
      used[selected] = true
    } else {
      return ordered
    }
  }
  ordered
}

///|
fn qos_irt_flow_precedes(
  left : SwitchedFabricTrafficFlow,
  right : SwitchedFabricTrafficFlow,
) -> Bool {
  if left.start_ns != right.start_ns {
    left.start_ns < right.start_ns
  } else if left.target != right.target {
    left.target.value() < right.target.value()
  } else if left.priority != right.priority {
    left.priority < right.priority
  } else {
    left.flow_id < right.flow_id
  }
}

///|
fn strict_priority_same_slot_count(
  profile : SwitchedFabricQosIrtProfile,
) -> Int {
  let mut count = 0
  for index, flow in profile.flows {
    for other_index, other in profile.flows {
      if index < other_index &&
        flow.target == other.target &&
        flow.start_ns == other.start_ns &&
        flow.priority != other.priority {
        count += 1
      }
    }
  }
  count
}

///|
fn phase_at(
  profile : SwitchedFabricQosIrtProfile,
  time_ns : Int64,
) -> SwitchedFabricIrtPhaseKind {
  for window in profile.phase_table {
    if time_ns >= window.start_ns && time_ns < window.end_ns {
      return window.phase
    }
  }
  IrtGreenPhase
}

///|
fn next_green_start_ns(
  profile : SwitchedFabricQosIrtProfile,
  time_ns : Int64,
) -> Int64 {
  for window in profile.phase_table {
    if window.phase == IrtGreenPhase && window.start_ns >= time_ns {
      return window.start_ns
    }
  }
  time_ns
}

///|
fn find_qos_irt_port_index(
  profile : SwitchedFabricQosIrtProfile,
  endpoint : @core.EndpointId,
) -> Int {
  for index, port in profile.ports {
    if port.endpoint == endpoint {
      return index
    }
  }
  0
}

///|
fn high_priority_before_low_priority(
  frames : Array[SwitchedFabricQosIrtFrameRecord],
) -> Bool {
  let mut high_index = -1
  let mut low_index = -1
  for index, frame in frames {
    if frame.flow_label == "cyclic.high.same-slot" {
      high_index = index
    }
    if frame.flow_label == "background.low.same-slot" {
      low_index = index
    }
  }
  high_index >= 0 && low_index >= 0 && high_index < low_index
}

///|
fn append_qos_irt_frame_trace(
  trace : @trace.TraceLog,
  frame : SwitchedFabricQosIrtFrameRecord,
  seed~ : Int,
  event_id~ : Int,
) -> Int {
  let mut next = event_id
  trace.append(
    switched_trace_event(
      event_id=next,
      parent_id=None,
      vtime=frame.enqueue_time,
      direction=@trace.Tx,
      payload_digest=Some(frame.priority),
      seed~,
      label="fabric.switched.qos.tx." + frame.flow_label,
    ),
  )
  next += 1
  if frame.flow_label == "cyclic.high.same-slot" {
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=frame.enqueue_time,
        direction=@trace.Resource,
        payload_digest=Some(frame.priority),
        seed~,
        label="fabric.switched.qos.priority-select",
      ),
    )
    next += 1
  }
  if frame.held_by_red_phase {
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=frame.enqueue_time,
        direction=@trace.Resource,
        payload_digest=Some(frame.frame_size_octets),
        seed~,
        label="fabric.switched.irt.red-hold",
      ),
    )
    next += 1
  }
  if frame.dropped_by_red_phase {
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=frame.enqueue_time,
        direction=@trace.Fault,
        payload_digest=Some(frame.frame_size_octets),
        seed~,
        label="fabric.switched.irt.red-drop",
      ),
    )
    next += 1
  } else {
    if frame.released_in_green_phase {
      trace.append(
        switched_trace_event(
          event_id=next,
          parent_id=Some(frame.record_id),
          vtime=frame.effective_enqueue_time,
          direction=@trace.Resource,
          payload_digest=Some(frame.frame_size_octets),
          seed~,
          label="fabric.switched.irt.green-release",
        ),
      )
      next += 1
    }
    guard frame.arrive_time is Some(arrive) else { return next }
    trace.append(
      switched_trace_event(
        event_id=next,
        parent_id=Some(frame.record_id),
        vtime=arrive,
        direction=@trace.Rx,
        payload_digest=Some(frame.frame_size_octets),
        seed~,
        label="fabric.switched.qos.rx." + frame.flow_label,
      ),
    )
    next += 1
  }
  next
}

///|
fn append_qos_irt_summary_trace(
  trace : @trace.TraceLog,
  event_id~ : Int,
  seed~ : Int,
  red_phase_hold_count~ : Int,
  red_phase_drop_count~ : Int,
  green_phase_release_count~ : Int,
) -> Unit {
  trace.append(
    switched_trace_event(
      event_id~,
      parent_id=None,
      vtime=@core.VTime::from_ns(120L),
      direction=@trace.Probe,
      payload_digest=Some(
        red_phase_hold_count + red_phase_drop_count + green_phase_release_count,
      ),
      seed~,
      label="fabric.switched.qos-irt.summary",
    ),
  )
}

///|
fn switched_trace_event(
  event_id~ : Int,
  parent_id~ : Int?,
  vtime~ : @core.VTime,
  direction~ : @trace.TraceDirection,
  payload_digest~ : Int?,
  seed~ : Int,
  label~ : String,
) -> @trace.TraceEvent {
  @trace.TraceEvent::make(
    event_id~,
    parent_id~,
    vtime~,
    clock_domain="sim",
    raw_ns=vtime.ns(),
    node_id="switched-fabric",
    medium_id="switched-fabric-offline",
    channel_id=Some(@core.ChannelId(event_id)),
    direction~,
    payload_digest~,
    rng_step=event_id,
    seed~,
    backend=@core.SimNative,
    label~,
  )
}

///|
fn find_switched_port_index(
  profile : SwitchedFabricOfflineProfile,
  endpoint : @core.EndpointId,
) -> Int {
  for index, port in profile.ports {
    if port.endpoint == endpoint {
      return index
    }
  }
  0
}

///|
fn serialization_delay(
  frame_size_octets : Int,
  capacity_octets_per_tick : Int,
) -> @core.Duration {
  let capacity = if capacity_octets_per_tick <= 0 {
    1
  } else {
    capacity_octets_per_tick
  }
  let ticks = (frame_size_octets + capacity - 1) / capacity
  @core.Duration::from_ns(Int64::from_int(ticks))
}

///|
fn switched_jitter_ns(seed : Int, flow_id : Int, burst : Int) -> Int64 {
  let value = switched_normalized_seed(seed + flow_id * 17 + burst * 31) % 5
  Int64::from_int(value * 3)
}

///|
fn switched_replay_count(trace : @trace.TraceLog) -> Int {
  let replay = @trace.Replay::from_log(trace)
  let mut count = 0
  while !replay.is_exhausted() {
    ignore(replay.next_event())
    count += 1
  }
  count
}

///|
fn switched_normalized_seed(seed : Int) -> Int {
  if seed < 0 {
    -seed
  } else {
    seed
  }
}

///|
fn max_i64(left : Int64, right : Int64) -> Int64 {
  if left >= right {
    left
  } else {
    right
  }
}

///|
fn min_int(left : Int, right : Int) -> Int {
  if left <= right {
    left
  } else {
    right
  }
}