diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04ccc527..72aea3a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -409,9 +409,6 @@ jobs: ref: ${{ github.event_name == 'schedule' && 'master' || env.ROSS_REF }} path: ross - # No dependency-install step: the codes-ci-ubuntu24-mpich container already ships - # the toolchain, cmake/ninja, flex/bison, lcov, and our source-built mpich. - - name: Configure ROSS run: > cmake -S ross -B ross/build -G Ninja diff --git a/doc/example/CMakeLists.txt b/doc/example/CMakeLists.txt index ffea52b3..4f8958f7 100644 --- a/doc/example/CMakeLists.txt +++ b/doc/example/CMakeLists.txt @@ -18,7 +18,7 @@ configure_file(tutorial-ping-pong-surrogate.yaml.in tutorial-ping-pong-surrogate configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.template.conf.in @ONLY) configure_file(kb.dfdally-72-event-time-director.conf.in kb.dfdally-72-event-time-director.template.conf.in @ONLY) configure_file(kb.dfdally-72-milc-small.workload.conf.in kb.dfdally-72-milc-small.workload.template.conf.in @ONLY) -configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.template.conf.in @ONLY) +configure_file(fluid-flow-wan.yaml.in fluid-flow-wan.template.yaml.in @ONLY) set(single_quote "'") set(double_quote "\"") @@ -49,6 +49,7 @@ configure_file(tutorial-ping-pong-surrogate.yaml.in tutorial-ping-pong-surrogate configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.conf) configure_file(kb.dfdally-72-event-time-director.conf.in kb.dfdally-72-event-time-director.conf) configure_file(kb.dfdally-72-milc-small.workload.conf.in kb.dfdally-72-milc-small.workload.conf) +configure_file(fluid-flow-wan.yaml.in fluid-flow-wan.yaml) configure_file(kb.dfdally-72-milc-small.json kb.dfdally-72-milc-small.json COPYONLY) configure_file(kb.dfdally-72-milc-small.alloc.conf kb.dfdally-72-milc-small.alloc.conf COPYONLY) -configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.conf) +configure_file(fluid-flow-wan-topology.yaml fluid-flow-wan-topology.yaml COPYONLY) diff --git a/doc/example/fluid-flow-wan-topology.yaml b/doc/example/fluid-flow-wan-topology.yaml new file mode 100644 index 00000000..09869b6b --- /dev/null +++ b/doc/example/fluid-flow-wan-topology.yaml @@ -0,0 +1,31 @@ +# Representative organization-to-organization WAN example: +# - 100 Gbps terminal/access links inside each site +# - 10-30 Gbps inter-site links carried by an ISP or research/enterprise WAN +# - 64 Gb (8 GB) shared deep buffer per WAN switch/router +# +# A->C intentionally routes through B, while C->A has a direct 20 Gbps link, +# preserving an asymmetric multi-hop path for rate-feedback validation. +topology: + switches: + A: + terminals: 2 + terminal_bandwidth: "100 Gbps" + switch_buffer: "64 Gb" + connections: + B: "30 Gbps" + + B: + terminals: 2 + terminal_bandwidth: "100 Gbps" + switch_buffer: "64 Gb" + connections: + A: "30 Gbps" + C: "10 Gbps" + + C: + terminals: 2 + terminal_bandwidth: "100 Gbps" + switch_buffer: "64 Gb" + connections: + A: "20 Gbps" + B: "10 Gbps" diff --git a/doc/example/fluid-flow-wan.yaml.in b/doc/example/fluid-flow-wan.yaml.in new file mode 100644 index 00000000..4e127239 --- /dev/null +++ b/doc/example/fluid-flow-wan.yaml.in @@ -0,0 +1,53 @@ +# Interval-fluid switch/terminal pure-PDES example. +# Run from the build directory, for example: +# mpirun -np 1 ./src/model-net-fluid-flow-wan --sync=1 -- doc/example/fluid-flow-wan.yaml + +schema_version: 1 + +topology: + format: groups + params: + message_size: 32768 + pe_mem_factor: 1024 + groups: + FLUID_FLOW_WAN_GRP: + repetitions: 1 + lps: + fluid-flow-wan-switch-lp: 3 + fluid-flow-wan-terminal-lp: 6 + +sections: + fluid_flow_wan: + topology_yaml_file: fluid-flow-wan-topology.yaml + + interval_seconds: 1 + num_send_intervals: 20 + num_drain_intervals: 20 + rng_seed: 12345 + + # Generate one persistent random flow per terminal at each configured period. + # The initial send rate is inferred from the terminal access-link bandwidth. + flow_generation_every_n_intervals: 3 + + # Size values accept Mb or Gb. All-Gb/Gbps inputs produce Gbit output; + # any mixture of mega and giga units produces Mbit output. + # 200-300 Gb persistent transfers take roughly 20-30 seconds on the + # constrained 10 Gbps inter-site link and keep rate feedback observable. + random_flow_min: "200 Gb" + random_flow_max: "300 Gb" + + # Switches advertise equal per-flow shares of each outgoing link. Feedback + # moves one reverse-path hop per interval. Terminals cache exact-destination + # and shared path-prefix rates so later flows avoid full-rate startup bursts. + debug_prints: 0 + + # Per-ingress Ethernet PAUSE hysteresis using the shared switch buffer. + # Congested switches pause the largest ingress contributors first. + pause_high_watermark_fraction: 0.80 + pause_low_watermark_fraction: 0.50 + pause_duration_intervals: 2 + + terminal_log_path: logs/terminal-events.csv + switch_log_path: logs/switch-events.csv + flowlet_log_path: logs/flowlet-events.csv + switch_training_log_path: logs/switch-training.csv diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0278e0ec..3330a18e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -229,6 +229,7 @@ add_executable(model-net-synthetic network-workloads/model-net-synthetic.c) add_executable(model-net-synthetic-slimfly network-workloads/model-net-synthetic-slimfly.c) add_executable(model-net-synthetic-fattree network-workloads/model-net-synthetic-fattree.c) add_executable(model-net-synthetic-dragonfly-all network-workloads/model-net-synthetic-dragonfly-all.c) +add_executable(model-net-fluid-flow-wan network-workloads/model-net-fluid-flow-wan.cxx) set(CODES_TARGETS topology-test @@ -237,6 +238,7 @@ set(CODES_TARGETS model-net-synthetic-slimfly model-net-synthetic-fattree model-net-synthetic-dragonfly-all + model-net-fluid-flow-wan ) if(USE_DUMPI) diff --git a/src/network-workloads/generate-fluid-flow-wan-topology.py b/src/network-workloads/generate-fluid-flow-wan-topology.py new file mode 100755 index 00000000..1fe66072 --- /dev/null +++ b/src/network-workloads/generate-fluid-flow-wan-topology.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +""" +Generate a WAN-like topology YAML file for model-net-fluid-flow-wan. + +The generated graph is intentionally not a symmetric supercomputer-style +topology. It starts with a bidirectional ring to guarantee strong connectivity, +then adds random directed/asymmetric switch-to-switch links. Terminal access +links are generated with higher bandwidth than switch-to-switch links. +""" + +from __future__ import annotations + +import argparse +import random +import sys +from pathlib import Path + + +def positive_int(value: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc + if parsed <= 0: + raise argparse.ArgumentTypeError(f"expected positive integer, got {parsed}") + return parsed + + +def nonnegative_int(value: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc + if parsed < 0: + raise argparse.ArgumentTypeError(f"expected nonnegative integer, got {parsed}") + return parsed + + +def positive_float(value: str) -> float: + try: + parsed = float(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"expected float, got {value!r}") from exc + if parsed <= 0.0: + raise argparse.ArgumentTypeError(f"expected positive float, got {parsed}") + return parsed + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate a random WAN-like topology YAML for model-net-fluid-flow-wan." + ) + + parser.add_argument( + "--switches", + type=positive_int, + default=64, + help="number of switches to generate; default: 64", + ) + parser.add_argument( + "--terminals-per-switch", + type=positive_int, + default=2, + help="number of terminals attached to each switch; default: 2", + ) + parser.add_argument( + "--avg-switch-degree", + type=positive_float, + default=3.0, + help=( + "target average directed switch out-degree, including the ring links; " + "default: 3.0" + ), + ) + parser.add_argument( + "--reverse-link-probability", + type=positive_float, + default=0.35, + help=( + "probability of adding the reverse direction for each random extra link; " + "default: 0.35" + ), + ) + parser.add_argument( + "--switch-link-min-mbps", + type=positive_float, + default=10000.0, + help="minimum switch-to-switch bandwidth in Mbps; default: 10000 (10 Gbps)", + ) + parser.add_argument( + "--switch-link-max-mbps", + type=positive_float, + default=30000.0, + help="maximum switch-to-switch bandwidth in Mbps; default: 30000 (30 Gbps)", + ) + parser.add_argument( + "--terminal-link-min-mbps", + type=positive_float, + default=100000.0, + help="minimum terminal-to-switch bandwidth in Mbps; default: 100000 (100 Gbps)", + ) + parser.add_argument( + "--terminal-link-max-mbps", + type=positive_float, + default=100000.0, + help="maximum terminal-to-switch bandwidth in Mbps; default: 100000 (100 Gbps)", + ) + parser.add_argument( + "--switch-buffer-min-mb", + type=positive_float, + default=64000.0, + help="minimum shared switch buffer in Mbit; default: 64000 (64 Gb)", + ) + parser.add_argument( + "--switch-buffer-max-mb", + type=positive_float, + default=64000.0, + help="maximum shared switch buffer in Mbit; default: 64000 (64 Gb)", + ) + parser.add_argument( + "--seed", + type=nonnegative_int, + default=12345, + help="random seed; default: 12345", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("doc/example/fluid-flow-wan-topology.generated.yaml"), + help=( + "output YAML path, relative to the current working directory unless absolute; " + "default: doc/example/fluid-flow-wan-topology.generated.yaml" + ), + ) + parser.add_argument( + "--name-prefix", + default="S", + help="switch name prefix; default: S", + ) + + args = parser.parse_args() + + if args.switches < 2: + parser.error("--switches must be at least 2 to form a connected switch graph") + + if args.avg_switch_degree < 2.0: + parser.error( + "--avg-switch-degree must be at least 2.0 because the generator " + "uses a bidirectional ring backbone" + ) + + if args.reverse_link_probability < 0.0 or args.reverse_link_probability > 1.0: + parser.error("--reverse-link-probability must be in [0, 1]") + + if args.switch_link_min_mbps > args.switch_link_max_mbps: + parser.error("--switch-link-min-mbps cannot exceed --switch-link-max-mbps") + + if args.terminal_link_min_mbps > args.terminal_link_max_mbps: + parser.error("--terminal-link-min-mbps cannot exceed --terminal-link-max-mbps") + + if args.terminal_link_min_mbps <= args.switch_link_max_mbps: + parser.error( + "terminal access links must be faster than switch-to-switch links: " + "require --terminal-link-min-mbps > --switch-link-max-mbps" + ) + + if args.switch_buffer_min_mb > args.switch_buffer_max_mb: + parser.error("--switch-buffer-min-mb cannot exceed --switch-buffer-max-mb") + + return args + + +def rand_uniform_rounded(rng: random.Random, lo: float, hi: float) -> float: + return round(rng.uniform(lo, hi), 3) + + +def format_quantity(value_in_mega: float, mega_suffix: str, giga_suffix: str) -> str: + if value_in_mega >= 1000.0: + return f"{value_in_mega / 1000.0:g} {giga_suffix}" + return f"{value_in_mega:g} {mega_suffix}" + + +def add_link( + links: dict[int, dict[int, float]], + src: int, + dst: int, + bandwidth_mbps: float, +) -> bool: + if src == dst: + return False + if dst in links[src]: + return False + links[src][dst] = bandwidth_mbps + return True + + +def generate_links(args: argparse.Namespace, rng: random.Random) -> dict[int, dict[int, float]]: + n = args.switches + links: dict[int, dict[int, float]] = {i: {} for i in range(n)} + + def new_switch_bw() -> float: + return rand_uniform_rounded( + rng, args.switch_link_min_mbps, args.switch_link_max_mbps + ) + + # Strongly connected but asymmetric backbone. Each direction gets its own + # independently sampled capacity. + for i in range(n): + add_link(links, i, (i + 1) % n, new_switch_bw()) + add_link(links, i, (i - 1) % n, new_switch_bw()) + + target_edges = max(int(round(args.switches * args.avg_switch_degree)), 2 * n) + max_edges = n * (n - 1) + if target_edges > max_edges: + target_edges = max_edges + + current_edges = sum(len(v) for v in links.values()) + attempts = 0 + max_attempts = max(1000, 50 * max_edges) + + while current_edges < target_edges and attempts < max_attempts: + attempts += 1 + src = rng.randrange(n) + dst = rng.randrange(n) + if src == dst: + continue + + if add_link(links, src, dst, new_switch_bw()): + current_edges += 1 + + if current_edges >= target_edges: + break + + if rng.random() < args.reverse_link_probability: + if add_link(links, dst, src, new_switch_bw()): + current_edges += 1 + + if current_edges < target_edges: + raise RuntimeError( + f"could only generate {current_edges} directed switch links; " + f"target was {target_edges}" + ) + + return links + + +def switch_name(index: int, n_switches: int, prefix: str) -> str: + width = max(2, len(str(n_switches - 1))) + return f"{prefix}{index:0{width}d}" + + +def write_topology(args: argparse.Namespace, links: dict[int, dict[int, float]]) -> None: + rng = random.Random(args.seed + 1) + n = args.switches + names = [switch_name(i, n, args.name_prefix) for i in range(n)] + + args.output.parent.mkdir(parents=True, exist_ok=True) + + with args.output.open("w", encoding="utf-8") as f: + f.write("# Generated by src/network-workloads/generate-fluid-flow-wan-topology.py\n") + f.write(f"# switches={args.switches}\n") + f.write(f"# terminals_per_switch={args.terminals_per_switch}\n") + f.write(f"# avg_switch_degree={args.avg_switch_degree}\n") + f.write(f"# seed={args.seed}\n") + f.write("topology:\n") + f.write(" switches:\n") + + for i in range(n): + terminal_bw = rand_uniform_rounded( + rng, args.terminal_link_min_mbps, args.terminal_link_max_mbps + ) + switch_buffer = rand_uniform_rounded( + rng, args.switch_buffer_min_mb, args.switch_buffer_max_mb + ) + + f.write(f" {names[i]}:\n") + f.write(f" terminals: {args.terminals_per_switch}\n") + terminal_bandwidth = format_quantity(terminal_bw, "Mbps", "Gbps") + buffer_size = format_quantity(switch_buffer, "Mb", "Gb") + f.write(f' terminal_bandwidth: "{terminal_bandwidth}"\n') + f.write(f' switch_buffer: "{buffer_size}"\n') + f.write(" connections:\n") + + for dst in sorted(links[i]): + bandwidth = format_quantity(links[i][dst], "Mbps", "Gbps") + f.write(f' {names[dst]}: "{bandwidth}"\n') + + f.write("\n") + + +def summarize(args: argparse.Namespace, links: dict[int, dict[int, float]]) -> None: + edge_count = sum(len(v) for v in links.values()) + degrees = [len(links[i]) for i in range(args.switches)] + bws = [bw for edges in links.values() for bw in edges.values()] + + print(f"wrote {args.output}") + print(f"switches: {args.switches}") + print(f"terminals: {args.switches * args.terminals_per_switch}") + print(f"directed switch links: {edge_count}") + print(f"average directed switch out-degree: {edge_count / args.switches:.3f}") + print(f"min/max directed switch out-degree: {min(degrees)} / {max(degrees)}") + print(f"switch-link Mbps min/max: {min(bws):.3f} / {max(bws):.3f}") + print( + "terminal-link Mbps range: " + f"{args.terminal_link_min_mbps:.3f} / {args.terminal_link_max_mbps:.3f}" + ) + + +def main() -> int: + args = parse_args() + rng = random.Random(args.seed) + links = generate_links(args, rng) + write_topology(args, links) + summarize(args, links) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/network-workloads/model-net-fluid-flow-wan.cxx b/src/network-workloads/model-net-fluid-flow-wan.cxx new file mode 100644 index 00000000..7c08d247 --- /dev/null +++ b/src/network-workloads/model-net-fluid-flow-wan.cxx @@ -0,0 +1,3804 @@ + +/* + * Standalone interval-fluid switch/terminal workload model. + * + * Terminal LPs generate persistent stochastic flows, retain unsent bytes at + * the source, and inject interval-sized segments at delayed advertised rates. + * Switch LPs stage arrivals, service buffered residuals first, advertise fair + * per-flow rates hop by hop, and buffer only unsent arrival residuals. Terminals + * cache learned rates for exact destinations and shared switch-path prefixes. + * + * Supports sequential validation and optimistic execution. Optimistic mode uses + * event-local reverse metadata to undo terminal generation, switch arrivals, + * queue mutations, egress byte deltas, and RNG draws. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "codes/codes.h" +#include "codes/codes_mapping.h" +#include "codes/configuration.h" +#include "codes/lp-type-lookup.h" + +static const char* GROUP_NAME = "FLUID_FLOW_WAN_GRP"; +static const char* TERMINAL_LP_NAME = "fluid-flow-wan-terminal-lp"; +static const char* SWITCH_LP_NAME = "fluid-flow-wan-switch-lp"; + +static constexpr int MAX_PORTS_PER_SWITCH = 128; +/* + * This upper bound is intentionally part of the event-message footprint: every + * fluid_msg carries rc_allocs[MAX_RC_ALLOCATIONS] so optimistic execution can + * reverse a switch-egress allocation without external state. Keep + * PARAMS.message_size >= sizeof(fluid_msg), and reduce this bound only after + * measuring the maximum allocations per switch-egress event for the target + * topology/workload. + */ +static constexpr int MAX_RC_ALLOCATIONS = 256; +static constexpr int MAX_PAUSE_INGRESS_LINKS = 128; +static constexpr int MAX_SOURCE_FLOWS_PER_TERMINAL = 256; +static constexpr int MAX_FLOW_ENTRIES_PER_PORT = 256; +static constexpr int MAX_RATE_CACHE_ENTRIES = 512; +static constexpr int MAX_SIMULATION_INTERVALS = 4096; +static constexpr int INTERVAL_FLAG_WORDS = (MAX_SIMULATION_INTERVALS + 63) / 64; +static constexpr double EPS = 1e-9; + + +template struct fixed_vector { + T data[Capacity]; + int count; + + int size(void) const { return count; } + T* begin(void) { return data; } + T* end(void) { return data + count; } + const T* begin(void) const { return data; } + const T* end(void) const { return data + count; } + T& operator[](int index) { return data[index]; } + const T& operator[](int index) const { return data[index]; } + + void push_back(const T& value) { + if (count >= Capacity) { + tw_error(TW_LOC, "fixed_vector capacity %d exceeded", Capacity); + } + data[count++] = value; + } + + void pop_back(void) { + if (count <= 0) { + tw_error(TW_LOC, "fixed_vector pop_back on empty container"); + } + --count; + } + + T* erase(T* position) { + const int index = (int)(position - data); + if (index < 0 || index >= count) { + tw_error(TW_LOC, "fixed_vector erase index %d out of range", index); + } + for (int i = index; i + 1 < count; ++i) { + data[i] = data[i + 1]; + } + --count; + return data + index; + } + + T* erase(T* first, T* last) { + const int begin_index = (int)(first - data); + const int end_index = (int)(last - data); + if (begin_index < 0 || end_index < begin_index || end_index > count) { + tw_error(TW_LOC, "fixed_vector erase range [%d,%d) out of range", begin_index, + end_index); + } + const int removed = end_index - begin_index; + for (int i = begin_index; i + removed < count; ++i) { + data[i] = data[i + removed]; + } + count -= removed; + return data + begin_index; + } + + T* insert(T* position, const T& value) { + const int index = (int)(position - data); + if (index < 0 || index > count) { + tw_error(TW_LOC, "fixed_vector insert index %d out of range", index); + } + if (count >= Capacity) { + tw_error(TW_LOC, "fixed_vector capacity %d exceeded", Capacity); + } + for (int i = count; i > index; --i) { + data[i] = data[i - 1]; + } + data[index] = value; + ++count; + return data + index; + } +}; + +struct interval_flags { + std::uint64_t words[INTERVAL_FLAG_WORDS]; +}; + +static bool interval_flag_is_set(const interval_flags& flags, int interval_id) { + const int word = interval_id / 64; + const int bit = interval_id % 64; + return (flags.words[word] & (std::uint64_t{1} << bit)) != 0; +} + +static void interval_flag_set(interval_flags* flags, int interval_id, bool value) { + const int word = interval_id / 64; + const int bit = interval_id % 64; + const std::uint64_t mask = std::uint64_t{1} << bit; + if (value) { + flags->words[word] |= mask; + } else { + flags->words[word] &= ~mask; + } +} + +static constexpr double PHASE_EARLY_SWITCH_EGRESS = 0.05; +static constexpr double PHASE_TERMINAL_RATE_UPDATE = 0.08; +static constexpr double PHASE_TERMINAL_WORKLOAD_GENERATE = 0.10; +static constexpr double PHASE_TERMINAL_SEND = 0.15; +static constexpr double PHASE_FLOWLET_ARRIVAL = 0.20; +static constexpr double PHASE_SWITCH_RATE_FEEDBACK = 0.50; +static constexpr double PHASE_LATE_SWITCH_EGRESS = 0.60; +static constexpr double PHASE_SWITCH_RATE_EVAL = 0.65; +static constexpr double PHASE_ETHERNET_PAUSE_EVAL = 0.70; +static constexpr double PHASE_ETHERNET_PAUSE_FRAME_UPDATE = 0.75; + +struct link_info { + int dst_switch; + double bandwidth_mbps; +}; + +struct switch_info { + std::string name; + int terminal_count = 0; + int terminal_start = 0; + double terminal_bandwidth_mbps = 100000.0; + double switch_buffer_mbit = 64000.0; + std::vector links; +}; + +struct terminal_info { + int switch_id = -1; + std::string name; +}; + +struct sim_config { + double interval_seconds = 1.0; + int num_send_intervals = 20; + int num_drain_intervals = 20; + int rng_seed = 12345; + int flow_generation_every_n_intervals = 3; + double random_flow_min_mbit = 10000.0; + double random_flow_max_mbit = 50000.0; + int debug_prints = 0; + char topology_yaml_file[1024] = ""; + char terminal_log_path[1024] = ""; + char switch_log_path[1024] = ""; + char flowlet_log_path[1024] = ""; + char switch_training_log_path[1024] = ""; + double pause_high_watermark_fraction = 0.80; + double pause_low_watermark_fraction = 0.50; + int pause_duration_intervals = 2; +}; + +static sim_config cfg; +/* Immutable process-wide topology metadata built before tw_run(). */ +static std::vector switches; +static std::vector terminals; +static std::map switch_name_to_id; +static std::vector> next_switch_table; +static int total_switch_lps = 0; +static int total_terminal_lps = 0; + +struct configured_unit_state { + bool saw_mega = false; + bool saw_giga = false; + + void observe_mega(void) { saw_mega = true; } + void observe_giga(void) { saw_giga = true; } +}; + +static configured_unit_state configured_units; + +static bool output_uses_gbit(void) { + return configured_units.saw_giga && !configured_units.saw_mega; +} + +static double to_output_data_unit(double mbit) { + return output_uses_gbit() ? mbit / 1000.0 : mbit; +} + +static const char* output_data_field_suffix(void) { + return output_uses_gbit() ? "gbit" : "mbit"; +} + +static const char* output_data_unit_symbol(void) { + return output_uses_gbit() ? "Gb" : "Mb"; +} + +struct queued_flowlet { + int valid; + unsigned long long flowlet_id; + int source_terminal; + int destination_terminal; + int creation_interval; + int enqueue_interval; + int age_intervals; + int ingress_id; + int final_segment_sent; + double remaining_mbit; +}; + +struct ingress_desc { + int is_terminal; + int peer_index; + double queued_mbit; + int pause_asserted; + int last_pause_frame_interval; +}; + +struct port_desc { + int is_terminal; + int target_index; + double capacity_mbit_per_interval; +}; + +struct source_flow { + unsigned long long flow_id; + int destination_terminal; + int creation_interval; + double remaining_source_mbit; + double current_send_rate_mbps; + int rate_epoch; +}; + +enum rate_cache_key_type { + RATE_CACHE_SWITCH_PREFIX = 1, + RATE_CACHE_DESTINATION_TERMINAL = 2, +}; + +struct rate_cache_entry { + int valid; + int key_type; + int key_index; + int prefix_hops; + std::uint64_t path_hash; + double rate_mbps; + int rate_epoch; +}; + +struct switch_rate_flow { + unsigned long long flow_id; + int source_terminal; + int destination_terminal; + int ingress_id; + int final_segment_seen; + double downstream_rate_mbps; + int downstream_rate_epoch; +}; + +struct terminal_state { + int terminal_id; + int attached_switch; + unsigned long long next_flowlet_seq; + /* Fixed LP-local storage; ROSS allocates it with terminal_state. */ + fixed_vector source_flows; + rate_cache_entry rate_cache[MAX_RATE_CACHE_ENTRIES]; + int rate_cache_entry_count; + double generated_mbit; + double sent_to_switch_mbit; + double received_mbit; + int link_paused_until_interval; + unsigned long long pause_updates_received; + unsigned long long pause_frames_received; + unsigned long long resume_frames_received; + unsigned long long link_paused_intervals; + int generated_flowlets; + int received_fragments; + unsigned long long rate_updates_received; +}; + +struct switch_state { + int switch_id; + int num_ports; + double shared_buffer_mbit; + port_desc ports[MAX_PORTS_PER_SWITCH]; + /* Fixed LP-local storage; no event-time heap allocation is used. */ + fixed_vector queues[MAX_PORTS_PER_SWITCH]; + /* + * Current-interval arrivals wait here without consuming physical buffer + * space. SWITCH_EGRESS_LATE transmits them with capacity left after the + * early residual-queue pass and buffers only their unsent remainder. + */ + fixed_vector staged_arrivals[MAX_PORTS_PER_SWITCH]; + fixed_vector rate_flows[MAX_PORTS_PER_SWITCH]; + + /* + * Rollback-safe demand-driven egress scheduling. Early and late egress + * events are tracked independently for every port and interval so Time + * Warp may have multiple future intervals outstanding at once. + */ + interval_flags scheduled_early_egress[MAX_PORTS_PER_SWITCH]; + interval_flags scheduled_late_egress[MAX_PORTS_PER_SWITCH]; + interval_flags scheduled_rate_eval[MAX_PORTS_PER_SWITCH]; + int capacity_accounting_interval[MAX_PORTS_PER_SWITCH]; + double capacity_used_mbit[MAX_PORTS_PER_SWITCH]; + int output_link_paused_until_interval[MAX_PORTS_PER_SWITCH]; + + int num_ingress_links; + ingress_desc ingress_links[MAX_PAUSE_INGRESS_LINKS]; + double pause_high_watermark_mbit; + double pause_low_watermark_mbit; + unsigned long long pause_updates_sent; + unsigned long long pause_frames_sent; + unsigned long long resume_frames_sent; + unsigned long long pause_updates_received; + unsigned long long pause_frames_received; + unsigned long long resume_frames_received; + unsigned long long paused_egress_intervals; + + double buffered_residual_mbit; + double sent_mbit; + double delivered_local_mbit; + double dropped_mbit; + int received_fragments; + int sent_fragments; +}; + +enum fluid_event_type { + TERMINAL_WORKLOAD_GENERATE = 1, + FLOWLET_ARRIVAL = 2, + SWITCH_EGRESS_EARLY = 3, + ETHERNET_PAUSE_EVAL = 4, + SWITCH_EGRESS_LATE = 5, + ETHERNET_PAUSE_FRAME_UPDATE = 6, + TERMINAL_SEND = 7, + SWITCH_RATE_EVAL = 8, + SWITCH_RATE_FEEDBACK = 9, + TERMINAL_RATE_UPDATE = 10, +}; + +struct rc_alloc_record { + int valid; + int source_is_staged; + int queue_index; + queued_flowlet before; + double send_mbit; + double buffered_mbit; + double dropped_mbit; + int residual_queue_index; + int residual_coalesced; + int residual_prev_final_segment_sent; +}; + + +struct fluid_msg { + int event_type; + int interval_id; + int source_terminal; + int destination_terminal; + int source_switch; + int port_id; + int creation_interval; + unsigned long long flowlet_id; + int final_segment_sent; + double mbit; + double rate_mbps; + int rate_epoch; + int rate_scope_key_type; + int rate_scope_key_index; + int pause_asserted; + int pause_source_switch; + + /* + * Reverse-computation metadata. These fields are written by the forward + * event handler and consumed by the reverse handler if the event rolls back. + */ + int rc_rng_count; + int rc_generated; + int rc_terminal_flow_index; + int rc_terminal_flow_appended; + source_flow rc_terminal_flow_before; + int rc_rate_cache_changed; + int rc_rate_cache_index; + int rc_rate_cache_was_created; + rate_cache_entry rc_rate_cache_before; + int rc_rate_flow_created; + int rc_rate_flow_appended; + int rc_rate_flow_index; + switch_rate_flow rc_rate_flow_before; + int rc_rate_update_applied; + int rc_no_route; + int rc_port_id; + int rc_queue_index; + int rc_coalesced; + int rc_prev_final_segment_sent; + double rc_accepted_mbit; + double rc_dropped_mbit; + int rc_alloc_count; + int rc_prev_pause_until_interval; + int rc_pause_target_port; + int rc_ingress_id; + int rc_pause_change_count; + int rc_pause_ingress_id[MAX_PAUSE_INGRESS_LINKS]; + int rc_pause_prev_asserted[MAX_PAUSE_INGRESS_LINKS]; + int rc_pause_prev_last_interval[MAX_PAUSE_INGRESS_LINKS]; + int rc_pause_sent_asserted[MAX_PAUSE_INGRESS_LINKS]; + + int rc_egress_event_active; + int rc_egress_request_created; + int rc_egress_request_event_type; + int rc_egress_request_interval; + int rc_egress_request_port; + int rc_prev_capacity_accounting_interval; + double rc_prev_capacity_used_mbit; + int rc_paused_interval_counted; + + int rc_rate_eval_event_active; + int rc_rate_eval_request_created; + int rc_rate_eval_request_interval; + int rc_rate_eval_request_port; + + int rc_log_target_is_terminal; + int rc_log_target_index; + double rc_log_capacity_mbit; + double rc_log_port_queued_before_mbit; + double rc_log_port_queued_after_mbit; + double rc_log_shared_queued_before_mbit; + double rc_log_shared_queued_after_mbit; + double rc_log_flowlet_remaining_after_mbit; + double rc_log_sent_total_mbit; + int rc_log_active_before_entries; + int rc_log_active_after_entries; + + rc_alloc_record rc_allocs[MAX_RC_ALLOCATIONS]; +}; + +static_assert(std::is_trivially_copyable::value, + "terminal_state must remain trivially copyable for ROSS LP storage"); +static_assert(std::is_trivially_copyable::value, + "switch_state must remain trivially copyable for ROSS LP storage"); +static_assert(std::is_trivially_copyable::value, + "fluid_msg must remain trivially copyable for ROSS events"); + +static void terminal_init(terminal_state* ns, tw_lp* lp); +static void terminal_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void terminal_rev_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void terminal_commit_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void terminal_finalize(terminal_state* ns, tw_lp* lp); +static void switch_init(switch_state* ns, tw_lp* lp); +static void switch_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void switch_rev_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void switch_commit_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp); +static void switch_finalize(switch_state* ns, tw_lp* lp); + +static tw_lptype terminal_lp = { + (init_f)terminal_init, + (pre_run_f)NULL, + (event_f)terminal_event, + (revent_f)terminal_rev_event, + (commit_f)terminal_commit_event, + (final_f)terminal_finalize, + (map_f)codes_mapping, + sizeof(terminal_state), +}; + +static tw_lptype switch_lp = { + (init_f)switch_init, (pre_run_f)NULL, + (event_f)switch_event, (revent_f)switch_rev_event, + (commit_f)switch_commit_event, (final_f)switch_finalize, + (map_f)codes_mapping, sizeof(switch_state), +}; + +static const tw_lptype* terminal_get_lp_type(void) { + return &terminal_lp; +} +static const tw_lptype* switch_get_lp_type(void) { + return &switch_lp; +} + +static void add_lp_types(void) { + lp_type_register(TERMINAL_LP_NAME, terminal_get_lp_type()); + lp_type_register(SWITCH_LP_NAME, switch_get_lp_type()); +} + +static int get_configured_message_size_bytes(void) { + int configured_message_size = 0; + if (configuration_get_value_int(&config, "PARAMS", "message_size", NULL, + &configured_message_size) != 0) { + return 0; + } + return configured_message_size; +} + +static void validate_ross_message_size_or_abort(int rank) { + int configured_message_size = get_configured_message_size_bytes(); + size_t required_message_size = sizeof(fluid_msg); + + if (configured_message_size <= 0 || (size_t)configured_message_size < required_message_size) { + if (rank == 0) { + fprintf(stderr, + "fluid-flow-wan error: PARAMS.message_size=%d is too small for " + "sizeof(fluid_msg)=%zu. Increase PARAMS.message_size in the " + "CODES config before calling codes_mapping_setup(). " + "fluid_msg is large because MAX_RC_ALLOCATIONS=%d reverse " + "allocation records are carried in each event.\n", + configured_message_size, required_message_size, MAX_RC_ALLOCATIONS); + } + MPI_Abort(MPI_COMM_CODES, 1); + } +} + +static double seconds_to_ns(double seconds) { + return seconds * 1000.0 * 1000.0 * 1000.0; +} + +static double event_time_ns(int interval_id, double phase_seconds) { + return seconds_to_ns(((double)interval_id * cfg.interval_seconds) + phase_seconds); +} + +static double delay_until_ns(int target_interval, double phase_seconds, tw_lp* lp) { + double target = event_time_ns(target_interval, phase_seconds); + double delta = target - tw_now(lp); + if (delta <= g_tw_lookahead) { + delta = g_tw_lookahead + 1.0; + } + return delta; +} + +static std::string trim(const std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace((unsigned char)s[b])) { + ++b; + } + size_t e = s.size(); + while (e > b && std::isspace((unsigned char)s[e - 1])) { + --e; + } + return s.substr(b, e - b); +} + +static int leading_spaces(const std::string& s) { + int n = 0; + while (n < (int)s.size() && s[n] == ' ') { + ++n; + } + return n; +} + +static std::string strip_quotes(std::string s) { + s = trim(s); + if (s.size() >= 2 && + ((s.front() == '"' && s.back() == '"') || (s.front() == '\'' && s.back() == '\''))) { + return s.substr(1, s.size() - 2); + } + return s; +} + +static std::string remove_inline_comment(const std::string& s) { + bool in_quote = false; + char quote_char = 0; + for (size_t i = 0; i < s.size(); ++i) { + if ((s[i] == '"' || s[i] == '\'') && (i == 0 || s[i - 1] != '\\')) { + if (!in_quote) { + in_quote = true; + quote_char = s[i]; + } else if (quote_char == s[i]) { + in_quote = false; + quote_char = 0; + } + } + if (!in_quote && s[i] == '#') { + return s.substr(0, i); + } + } + return s; +} + +static bool split_key_value(const std::string& line, std::string* key, std::string* value) { + size_t pos = line.find(':'); + if (pos == std::string::npos) { + return false; + } + *key = trim(line.substr(0, pos)); + *value = trim(line.substr(pos + 1)); + return !key->empty(); +} + +static double parse_scaled_quantity(const std::string& raw, const char* mega_suffix, + const char* giga_suffix, const char* description) { + const std::string s = trim(strip_quotes(raw)); + errno = 0; + char* end = NULL; + const double value = std::strtod(s.c_str(), &end); + if (end == s.c_str() || errno == ERANGE || !std::isfinite(value) || value < 0.0) { + tw_error(TW_LOC, "invalid %s value '%s'", description, raw.c_str()); + } + + while (*end != '\0' && std::isspace((unsigned char)*end)) { + ++end; + } + const std::string suffix = trim(end); + + if (suffix.empty() || suffix == mega_suffix) { + configured_units.observe_mega(); + return value; + } + if (suffix == giga_suffix) { + configured_units.observe_giga(); + return value * 1000.0; + } + + tw_error(TW_LOC, "invalid unit in %s value '%s'; expected %s or %s", description, raw.c_str(), + mega_suffix, giga_suffix); + return 0.0; +} + +static double parse_mbps(const std::string& raw) { + return parse_scaled_quantity(raw, "Mbps", "Gbps", "bandwidth"); +} + +static double parse_mbit(const std::string& raw) { + return parse_scaled_quantity(raw, "Mb", "Gb", "data size"); +} + +static int get_or_add_switch(const std::string& name) { + std::map::iterator it = switch_name_to_id.find(name); + if (it != switch_name_to_id.end()) { + return it->second; + } + int id = (int)switches.size(); + switch_info sw; + sw.name = name; + switches.push_back(sw); + switch_name_to_id[name] = id; + return id; +} + +static void add_or_update_link(int src, int dst, double bw_mbps) { + for (link_info& link : switches[src].links) { + if (link.dst_switch == dst) { + link.bandwidth_mbps = bw_mbps; + return; + } + } + link_info link; + link.dst_switch = dst; + link.bandwidth_mbps = bw_mbps; + switches[src].links.push_back(link); +} + +static void load_topology_yaml(const char* path) { + switches.clear(); + terminals.clear(); + switch_name_to_id.clear(); + + std::ifstream in(path); + if (!in.good()) { + tw_error(TW_LOC, "could not open topology YAML file: %s", path); + } + + std::string section; + bool in_switches = false; + bool in_connections = false; + int current_switch = -1; + int current_conn_dst = -1; + + std::string raw; + while (std::getline(in, raw)) { + std::string no_comment = remove_inline_comment(raw); + if (trim(no_comment).empty()) { + continue; + } + int indent = leading_spaces(no_comment); + std::string line = trim(no_comment); + if (line == "topology:") { + section = "topology"; + in_switches = false; + in_connections = false; + current_switch = -1; + continue; + } + if (section == "topology" && line == "switches:") { + in_switches = true; + in_connections = false; + current_switch = -1; + continue; + } + + std::string key; + std::string value; + if (!split_key_value(line, &key, &value)) { + continue; + } + + if (section == "topology" && in_switches) { + if (indent == 4 && value.empty()) { + current_switch = get_or_add_switch(key); + in_connections = false; + current_conn_dst = -1; + continue; + } + if (current_switch < 0) { + continue; + } + if (indent == 6 && key == "connections") { + in_connections = true; + current_conn_dst = -1; + continue; + } + if (!in_connections && indent >= 6) { + if (key == "terminals") + switches[current_switch].terminal_count = atoi(strip_quotes(value).c_str()); + else if (key == "terminal_bandwidth") + switches[current_switch].terminal_bandwidth_mbps = parse_mbps(value); + else if (key == "switch_buffer") + switches[current_switch].switch_buffer_mbit = parse_mbit(value); + continue; + } + if (in_connections && indent == 8) { + int dst = get_or_add_switch(key); + current_conn_dst = dst; + if (!value.empty()) { + add_or_update_link(current_switch, dst, parse_mbps(value)); + } + continue; + } + if (in_connections && indent >= 10 && current_conn_dst >= 0) { + if (key == "bandwidth") { + double bw = parse_mbps(value); + add_or_update_link(current_switch, current_conn_dst, bw); + } + } + } + } + + int terminal_start = 0; + for (int s = 0; s < (int)switches.size(); ++s) { + switches[s].terminal_start = terminal_start; + for (int t = 0; t < switches[s].terminal_count; ++t) { + terminal_info ti; + ti.switch_id = s; + std::ostringstream name; + name << switches[s].name << "." << t; + ti.name = name.str(); + terminals.push_back(ti); + ++terminal_start; + } + } + + if (switches.empty()) { + tw_error(TW_LOC, "topology YAML defined no switches"); + } + if (terminals.size() < 2) { + tw_error(TW_LOC, "topology YAML must define at least two terminals"); + } + if (cfg.interval_seconds <= 0.0) + tw_error(TW_LOC, "interval_seconds must be positive"); + if (cfg.num_send_intervals <= 0) + tw_error(TW_LOC, "num_send_intervals must be positive"); + if (cfg.num_drain_intervals < 0) + cfg.num_drain_intervals = 0; + if (cfg.flow_generation_every_n_intervals <= 0) + cfg.flow_generation_every_n_intervals = 3; + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (total_intervals > MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, + "num_send_intervals + num_drain_intervals = %d exceeds " + "MAX_SIMULATION_INTERVALS=%d", + total_intervals, MAX_SIMULATION_INTERVALS); + } + if (cfg.random_flow_min_mbit < 0.0) + tw_error(TW_LOC, "random_flow_min must be nonnegative"); + if (cfg.random_flow_max_mbit < cfg.random_flow_min_mbit) + tw_error(TW_LOC, "random_flow_max must be greater than or equal to random_flow_min"); +} + +static void compute_routes(void) { + int n = (int)switches.size(); + next_switch_table.assign(n, std::vector(n, -1)); + for (int src = 0; src < n; ++src) { + std::vector prev(n, -1); + std::vector seen(n, 0); + std::queue q; + seen[src] = 1; + q.push(src); + while (!q.empty()) { + int u = q.front(); + q.pop(); + for (size_t i = 0; i < switches[u].links.size(); ++i) { + int v = switches[u].links[i].dst_switch; + if (!seen[v]) { + seen[v] = 1; + prev[v] = u; + q.push(v); + } + } + } + for (int dst = 0; dst < n; ++dst) { + if (dst == src) { + next_switch_table[src][dst] = src; + continue; + } + if (!seen[dst]) { + next_switch_table[src][dst] = -1; + continue; + } + int cur = dst; + int parent = prev[cur]; + while (parent >= 0 && parent != src) { + cur = parent; + parent = prev[cur]; + } + next_switch_table[src][dst] = cur; + } + } +} + +static void read_int_param(const char* section, const char* key, int* value) { + int tmp = 0; + if (configuration_get_value_int(&config, section, key, NULL, &tmp) == 0) { + *value = tmp; + } +} + +static void read_double_param(const char* section, const char* key, double* value) { + double tmp = 0.0; + if (configuration_get_value_double(&config, section, key, NULL, &tmp) == 0) { + *value = tmp; + } +} + +static void read_data_quantity_param(const char* section, const char* key, double* value) { + char raw[128]; + memset(raw, 0, sizeof(raw)); + if (configuration_get_value(&config, section, key, NULL, raw, sizeof(raw)) > 0) { + *value = parse_mbit(raw); + } +} + +static void read_relpath_param(const char* section, const char* key, char* value, size_t len) { + char tmp[1024]; + memset(tmp, 0, sizeof(tmp)); + if (configuration_get_value_relpath(&config, section, key, NULL, tmp, sizeof(tmp)) > 0) { + snprintf(value, len, "%s", tmp); + } +} + +static void load_config(void) { + configured_units = configured_unit_state{}; + + read_relpath_param("FLUID_FLOW_WAN", "topology_yaml_file", cfg.topology_yaml_file, + sizeof(cfg.topology_yaml_file)); + read_relpath_param("FLUID_FLOW_WAN", "terminal_log_path", cfg.terminal_log_path, + sizeof(cfg.terminal_log_path)); + read_relpath_param("FLUID_FLOW_WAN", "switch_log_path", cfg.switch_log_path, + sizeof(cfg.switch_log_path)); + read_relpath_param("FLUID_FLOW_WAN", "flowlet_log_path", cfg.flowlet_log_path, + sizeof(cfg.flowlet_log_path)); + read_relpath_param("FLUID_FLOW_WAN", "switch_training_log_path", cfg.switch_training_log_path, + sizeof(cfg.switch_training_log_path)); + read_double_param("FLUID_FLOW_WAN", "pause_high_watermark_fraction", + &cfg.pause_high_watermark_fraction); + read_double_param("FLUID_FLOW_WAN", "pause_low_watermark_fraction", + &cfg.pause_low_watermark_fraction); + read_int_param("FLUID_FLOW_WAN", "pause_duration_intervals", &cfg.pause_duration_intervals); + read_double_param("FLUID_FLOW_WAN", "interval_seconds", &cfg.interval_seconds); + read_int_param("FLUID_FLOW_WAN", "num_send_intervals", &cfg.num_send_intervals); + read_int_param("FLUID_FLOW_WAN", "num_drain_intervals", &cfg.num_drain_intervals); + read_int_param("FLUID_FLOW_WAN", "rng_seed", &cfg.rng_seed); + read_int_param("FLUID_FLOW_WAN", "flow_generation_every_n_intervals", + &cfg.flow_generation_every_n_intervals); + read_data_quantity_param("FLUID_FLOW_WAN", "random_flow_min", &cfg.random_flow_min_mbit); + read_data_quantity_param("FLUID_FLOW_WAN", "random_flow_max", &cfg.random_flow_max_mbit); + read_int_param("FLUID_FLOW_WAN", "debug_prints", &cfg.debug_prints); + + if (cfg.pause_duration_intervals <= 0) { + tw_error(TW_LOC, "pause_duration_intervals must be positive, got %d", + cfg.pause_duration_intervals); + } + + if (!(cfg.pause_low_watermark_fraction >= 0.0 && + cfg.pause_low_watermark_fraction < cfg.pause_high_watermark_fraction && + cfg.pause_high_watermark_fraction <= 1.0)) { + tw_error(TW_LOC, + "PAUSE watermarks must satisfy 0 <= low < high <= 1; got low=%.6f high=%.6f", + cfg.pause_low_watermark_fraction, cfg.pause_high_watermark_fraction); + } + + if (cfg.topology_yaml_file[0] == '\0') { + tw_error(TW_LOC, "FLUID_FLOW_WAN.topology_yaml_file is required"); + } + load_topology_yaml(cfg.topology_yaml_file); + compute_routes(); +} + +static tw_lpid get_switch_gid(int switch_id) { + if (switch_id < 0 || switch_id >= (int)switches.size()) { + tw_error(TW_LOC, "switch id %d out of range [0, %zu)", switch_id, switches.size()); + } + + tw_lpid gid = 0; + + /* + * LPGROUPS has one FLUID_FLOW_WAN_GRP repetition containing all switch LPs. + * The switch index is therefore the LP-type offset, not the group repetition. + */ + codes_mapping_get_lp_id(GROUP_NAME, SWITCH_LP_NAME, NULL, 1, 0, switch_id, &gid); + + return gid; +} + +static tw_lpid get_terminal_gid(int terminal_id) { + if (terminal_id < 0 || terminal_id >= (int)terminals.size()) { + tw_error(TW_LOC, "terminal id %d out of range [0, %zu)", terminal_id, terminals.size()); + } + + tw_lpid gid = 0; + + /* + * LPGROUPS has one FLUID_FLOW_WAN_GRP repetition containing all terminal LPs. + * The terminal index is therefore the LP-type offset, not the group repetition. + */ + codes_mapping_get_lp_id(GROUP_NAME, TERMINAL_LP_NAME, NULL, 1, 0, terminal_id, &gid); + + return gid; +} + +static int find_switch_link_port(const switch_state* ns, int dst_switch) { + for (int p = 0; p < ns->num_ports; ++p) { + if (!ns->ports[p].is_terminal && ns->ports[p].target_index == dst_switch) { + return p; + } + } + return -1; +} + +static int find_terminal_port(const switch_state* ns, int dst_terminal) { + for (int p = 0; p < ns->num_ports; ++p) { + if (ns->ports[p].is_terminal && ns->ports[p].target_index == dst_terminal) { + return p; + } + } + return -1; +} + +static double queued_mbit_on_switch(const switch_state* ns); + +static double enqueue_flowlet(switch_state* ns, int port_id, const fluid_msg* m, + double* port_queued_before_out, double* shared_queued_before_out, + double* shared_queued_after_out, double* dropped_out, + double* flowlet_remaining_after_out, int* coalesced_out, + int* queue_index_out) { + if (port_queued_before_out != NULL) { + *port_queued_before_out = 0.0; + } + if (shared_queued_before_out != NULL) { + *shared_queued_before_out = 0.0; + } + if (shared_queued_after_out != NULL) { + *shared_queued_after_out = 0.0; + } + if (dropped_out != NULL) { + *dropped_out = 0.0; + } + if (flowlet_remaining_after_out != NULL) { + *flowlet_remaining_after_out = 0.0; + } + if (coalesced_out != NULL) { + *coalesced_out = 0; + } + if (queue_index_out != NULL) { + *queue_index_out = -1; + } + + if (port_id < 0 || port_id >= ns->num_ports) { + if (dropped_out != NULL) { + *dropped_out = m->mbit; + } + return 0.0; + } + + fixed_vector& qv = ns->queues[port_id]; + + double port_queued_before = 0.0; + for (int i = 0; i < qv.size(); ++i) { + port_queued_before += qv[i].remaining_mbit; + } + + double shared_queued_before = queued_mbit_on_switch(ns); + + if (port_queued_before_out != NULL) { + *port_queued_before_out = port_queued_before; + } + if (shared_queued_before_out != NULL) { + *shared_queued_before_out = shared_queued_before; + } + + /* + * Admission is controlled by the shared switch-wide buffer. Output queues + * remain per port, but they all draw from this one occupancy limit. + */ + double shared_available = std::max(0.0, ns->shared_buffer_mbit - shared_queued_before); + double accepted = std::min(m->mbit, shared_available); + double dropped = std::max(0.0, m->mbit - accepted); + + if (dropped > EPS) { + ns->dropped_mbit += dropped; + } + + if (dropped_out != NULL) { + *dropped_out = dropped; + } + + if (accepted <= EPS) { + if (shared_queued_after_out != NULL) { + *shared_queued_after_out = shared_queued_before; + } + return 0.0; + } + + for (int i = 0; i < qv.size(); ++i) { + if (!qv[i].valid) { + continue; + } + + if (qv[i].flowlet_id == m->flowlet_id && qv[i].source_terminal == m->source_terminal && + qv[i].destination_terminal == m->destination_terminal && + qv[i].creation_interval == m->creation_interval && + qv[i].ingress_id == m->rc_ingress_id) { + qv[i].remaining_mbit += accepted; + qv[i].age_intervals = m->interval_id - m->creation_interval; + qv[i].final_segment_sent |= m->final_segment_sent; + ns->buffered_residual_mbit += accepted; + + if (flowlet_remaining_after_out != NULL) { + *flowlet_remaining_after_out = qv[i].remaining_mbit; + } + if (coalesced_out != NULL) { + *coalesced_out = 1; + } + if (queue_index_out != NULL) { + *queue_index_out = (int)i; + } + if (shared_queued_after_out != NULL) { + *shared_queued_after_out = shared_queued_before + accepted; + } + + return accepted; + } + } + + queued_flowlet q; + memset(&q, 0, sizeof(q)); + q.valid = 1; + q.flowlet_id = m->flowlet_id; + q.source_terminal = m->source_terminal; + q.destination_terminal = m->destination_terminal; + q.creation_interval = m->creation_interval; + q.enqueue_interval = m->interval_id; + q.age_intervals = m->interval_id - m->creation_interval; + q.ingress_id = m->rc_ingress_id; + q.final_segment_sent = m->final_segment_sent; + q.remaining_mbit = accepted; + + if (qv.size() >= MAX_FLOW_ENTRIES_PER_PORT) { + tw_error(TW_LOC, + "switch %d port %d residual queue exceeded " + "MAX_FLOW_ENTRIES_PER_PORT=%d", + ns->switch_id, port_id, MAX_FLOW_ENTRIES_PER_PORT); + } + qv.push_back(q); + if (queue_index_out != NULL) { + *queue_index_out = (int)qv.size() - 1; + } + ns->buffered_residual_mbit += accepted; + + if (flowlet_remaining_after_out != NULL) { + *flowlet_remaining_after_out = accepted; + } + if (shared_queued_after_out != NULL) { + *shared_queued_after_out = shared_queued_before + accepted; + } + + return accepted; +} + +static double stage_flowlet_arrival(switch_state* ns, int port_id, const fluid_msg* m, + int* coalesced_out, int* queue_index_out, + double* staged_remaining_after_out) { + if (coalesced_out != NULL) { + *coalesced_out = 0; + } + if (queue_index_out != NULL) { + *queue_index_out = -1; + } + if (staged_remaining_after_out != NULL) { + *staged_remaining_after_out = 0.0; + } + + if (port_id < 0 || port_id >= ns->num_ports) { + return 0.0; + } + + fixed_vector& staged = ns->staged_arrivals[port_id]; + for (int i = 0; i < staged.size(); ++i) { + queued_flowlet& q = staged[i]; + if (!q.valid) { + continue; + } + if (q.enqueue_interval == m->interval_id && q.flowlet_id == m->flowlet_id && + q.source_terminal == m->source_terminal && + q.destination_terminal == m->destination_terminal && + q.creation_interval == m->creation_interval && q.ingress_id == m->rc_ingress_id) { + q.remaining_mbit += m->mbit; + q.age_intervals = m->interval_id - m->creation_interval; + q.final_segment_sent |= m->final_segment_sent; + if (coalesced_out != NULL) { + *coalesced_out = 1; + } + if (queue_index_out != NULL) { + *queue_index_out = (int)i; + } + if (staged_remaining_after_out != NULL) { + *staged_remaining_after_out = q.remaining_mbit; + } + return m->mbit; + } + } + + queued_flowlet q; + memset(&q, 0, sizeof(q)); + q.valid = 1; + q.flowlet_id = m->flowlet_id; + q.source_terminal = m->source_terminal; + q.destination_terminal = m->destination_terminal; + q.creation_interval = m->creation_interval; + q.enqueue_interval = m->interval_id; + q.age_intervals = m->interval_id - m->creation_interval; + q.ingress_id = m->rc_ingress_id; + q.final_segment_sent = m->final_segment_sent; + q.remaining_mbit = m->mbit; + if (staged.size() >= MAX_FLOW_ENTRIES_PER_PORT) { + tw_error(TW_LOC, + "switch %d port %d staged-arrival queue exceeded " + "MAX_FLOW_ENTRIES_PER_PORT=%d", + ns->switch_id, port_id, MAX_FLOW_ENTRIES_PER_PORT); + } + staged.push_back(q); + + if (queue_index_out != NULL) { + *queue_index_out = (int)staged.size() - 1; + } + if (staged_remaining_after_out != NULL) { + *staged_remaining_after_out = m->mbit; + } + return m->mbit; +} + + +static double queued_mbit_on_port(const switch_state* ns, int port_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return 0.0; + } + double total = 0.0; + const fixed_vector& qv = ns->queues[port_id]; + for (int i = 0; i < qv.size(); ++i) { + total += qv[i].remaining_mbit; + } + return total; +} + +static double queued_mbit_on_switch(const switch_state* ns) { + double total = 0.0; + for (int p = 0; p < ns->num_ports; ++p) { + total += queued_mbit_on_port(ns, p); + } + return total; +} + + +static int active_flowlet_count_on_port(const switch_state* ns, int port_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return 0; + } + int count = 0; + const fixed_vector& qv = ns->queues[port_id]; + for (int i = 0; i < qv.size(); ++i) { + if (qv[i].valid && qv[i].remaining_mbit > EPS) { + ++count; + } + } + return count; +} + +static int find_queue_index_for_flowlet(const switch_state* ns, int port_id, + const queued_flowlet& needle) { + if (port_id < 0 || port_id >= ns->num_ports) { + return -1; + } + + const fixed_vector& qv = ns->queues[port_id]; + + for (int i = 0; i < (int)qv.size(); ++i) { + if (!qv[i].valid) { + continue; + } + + if (qv[i].flowlet_id == needle.flowlet_id && + qv[i].source_terminal == needle.source_terminal && + qv[i].destination_terminal == needle.destination_terminal && + qv[i].creation_interval == needle.creation_interval && + qv[i].ingress_id == needle.ingress_id) { + return i; + } + } + + return -1; +} + +static int find_staged_index_for_msg(const switch_state* ns, int port_id, const fluid_msg* m) { + if (port_id < 0 || port_id >= ns->num_ports) { + return -1; + } + + const fixed_vector& staged = + ns->staged_arrivals[port_id]; + for (int i = 0; i < (int)staged.size(); ++i) { + if (!staged[i].valid) { + continue; + } + if (staged[i].enqueue_interval == m->interval_id && staged[i].flowlet_id == m->flowlet_id && + staged[i].source_terminal == m->source_terminal && + staged[i].destination_terminal == m->destination_terminal && + staged[i].creation_interval == m->creation_interval && + staged[i].ingress_id == m->rc_ingress_id) { + return i; + } + } + return -1; +} + +static void compact_staged_arrivals(switch_state* ns, int port_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return; + } + fixed_vector& staged = ns->staged_arrivals[port_id]; + staged.erase(std::remove_if(staged.begin(), staged.end(), + [](const queued_flowlet& q) { + return !q.valid || q.remaining_mbit <= EPS; + }), + staged.end()); +} + +static void compact_port_queue(switch_state* ns, int port_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return; + } + fixed_vector& qv = ns->queues[port_id]; + qv.erase(std::remove_if(qv.begin(), qv.end(), + [](const queued_flowlet& q) { + return !q.valid || q.remaining_mbit <= EPS; + }), + qv.end()); +} + +static bool fluid_commit_logging_active = false; +/* Process-owned output buffers; these are committed logs, not LP rollback state. */ +static std::ostringstream terminal_log_buffer; +static std::ostringstream switch_log_buffer; +static std::ostringstream flowlet_log_buffer; +static std::ostringstream switch_training_log_buffer; + +static bool fluid_optimistic_mode(void) { + return g_tw_synchronization_protocol == OPTIMISTIC || + g_tw_synchronization_protocol == OPTIMISTIC_DEBUG || + g_tw_synchronization_protocol == OPTIMISTIC_REALTIME; +} + +static bool fluid_csv_forward_logs_enabled(void) { + return !fluid_optimistic_mode(); +} + +static bool fluid_csv_commit_logs_enabled(void) { + return fluid_optimistic_mode(); +} + +static bool fluid_csv_logs_enabled(void) { + return fluid_csv_forward_logs_enabled() || fluid_commit_logging_active; +} + +static void fluid_commit_logging_begin(void) { + fluid_commit_logging_active = true; +} + +static void fluid_commit_logging_end(void) { + fluid_commit_logging_active = false; +} + +static void append_log_row(const char* path, std::ostringstream* log_buffer, + const std::string& row) { + if (path[0] == '\0' || !fluid_csv_logs_enabled()) { + return; + } + + (*log_buffer) << row; +} + +static void append_terminal_log(int interval_id, const char* event_name, int terminal_id, + int peer_id, double mbit) { + std::ostringstream row; + row << interval_id << ',' << event_name << ',' << terminal_id << ',' + << terminals[terminal_id].name << ',' << terminals[terminal_id].switch_id << ',' << peer_id + << ',' << to_output_data_unit(mbit) << '\n'; + append_log_row(cfg.terminal_log_path, &terminal_log_buffer, row.str()); +} + +static void append_switch_log(int interval_id, const char* event_name, int switch_id, int port_id, + int target_is_terminal, int target_index, double capacity_mbit, + double queued_before_mbit, double sent_mbit, double queued_after_mbit, + double shared_queued_before_mbit, double shared_queued_after_mbit, + double shared_buffer_mbit, double dropped_mbit, + int active_queue_entries) { + std::ostringstream row; + row << interval_id << ',' << event_name << ',' << switch_id << ',' << switches[switch_id].name + << ',' << port_id << ',' << (target_is_terminal ? "terminal" : "switch") << ',' + << target_index << ',' << to_output_data_unit(capacity_mbit) << ',' + << to_output_data_unit(queued_before_mbit) << ',' << to_output_data_unit(sent_mbit) << ',' + << to_output_data_unit(queued_after_mbit) << ',' + << to_output_data_unit(shared_queued_before_mbit) << ',' + << to_output_data_unit(shared_queued_after_mbit) << ',' + << to_output_data_unit(shared_buffer_mbit) << ',' << to_output_data_unit(dropped_mbit) + << ',' << active_queue_entries << '\n'; + append_log_row(cfg.switch_log_path, &switch_log_buffer, row.str()); +} + +static void append_flowlet_log(int interval_id, const char* event_name, int switch_id, int port_id, + int target_is_terminal, int target_index, + unsigned long long flowlet_id, int source_terminal, + int destination_terminal, int creation_interval, + double capacity_mbit, double queued_before_mbit, double send_mbit, + double remaining_after_mbit, double dropped_mbit) { + std::ostringstream row; + row << interval_id << ',' << event_name << ',' << switch_id << ',' << switches[switch_id].name + << ',' << port_id << ',' << (target_is_terminal ? "terminal" : "switch") << ',' + << target_index << ',' << flowlet_id << ',' << source_terminal << ',' + << destination_terminal << ',' << creation_interval << ',' + << (interval_id - creation_interval) << ',' << to_output_data_unit(capacity_mbit) << ',' + << to_output_data_unit(queued_before_mbit) << ',' << to_output_data_unit(send_mbit) << ',' + << to_output_data_unit(remaining_after_mbit) << ',' << to_output_data_unit(dropped_mbit) + << '\n'; + append_log_row(cfg.flowlet_log_path, &flowlet_log_buffer, row.str()); +} + +static void append_switch_training_log(int interval_id, int switch_id, int port_id, + int target_is_terminal, int target_index, + double capacity_mbit, double queued_before_mbit, + double sent_mbit, double queued_after_mbit, + double dropped_mbit, int active_before, int active_after, + const fluid_msg* message) { + std::ostringstream row; + row << interval_id << ',' << switch_id << ',' << switches[switch_id].name << ',' << port_id + << ',' << (target_is_terminal ? "terminal" : "switch") << ',' << target_index << ',' + << to_output_data_unit(capacity_mbit) << ',' << to_output_data_unit(queued_before_mbit) + << ',' << to_output_data_unit(sent_mbit) << ',' << to_output_data_unit(queued_after_mbit) + << ',' << to_output_data_unit(dropped_mbit) << ',' << active_before << ',' << active_after + << ','; + bool wrote_allocation = false; + for (int i = 0; i < message->rc_alloc_count; ++i) { + const rc_alloc_record& rc = message->rc_allocs[i]; + if (!rc.valid || rc.send_mbit <= EPS) { + continue; + } + double remaining_after = rc.before.remaining_mbit - rc.send_mbit; + if (remaining_after < 0.0 && remaining_after > -EPS) { + remaining_after = 0.0; + } + if (wrote_allocation) { + row << ';'; + } + row << rc.before.flowlet_id << ':' << rc.before.source_terminal << ':' + << rc.before.destination_terminal << ':' << rc.before.creation_interval << ':' + << to_output_data_unit(rc.before.remaining_mbit) << ':' + << to_output_data_unit(rc.send_mbit) << ':' << to_output_data_unit(remaining_after); + wrote_allocation = true; + } + row << '\n'; + append_log_row(cfg.switch_training_log_path, &switch_training_log_buffer, row.str()); +} + +static int next_terminal_generate_interval_after(int interval_id) { + if (interval_id < 0) { + return 0; + } + return interval_id + cfg.flow_generation_every_n_intervals; +} + +static int first_terminal_generate_interval(void) { + /* + * The first eligible terminal workload interval is always 0. Later events + * advance by flow_generation_every_n_intervals, so terminals do not carry a + * speculative self-event chain through intervals where no workload can be + * generated. + */ + return 0; +} + +static void schedule_workload_generate(terminal_state* ns, int interval_id, tw_lp* lp) { + if (interval_id >= cfg.num_send_intervals) { + return; + } + + /* + * Defensive alignment: callers should already pass eligible send intervals, + * but if a future caller passes an arbitrary interval, round up to the next + * eligible workload-generation interval instead of scheduling a no-op event. + */ + const int period = cfg.flow_generation_every_n_intervals; + int rem = interval_id % period; + if (rem != 0) { + interval_id += period - rem; + } + + if (interval_id >= cfg.num_send_intervals) { + return; + } + + tw_event* e = + tw_event_new(lp->gid, delay_until_ns(interval_id, PHASE_TERMINAL_WORKLOAD_GENERATE, lp), + lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = TERMINAL_WORKLOAD_GENERATE; + m->interval_id = interval_id; + m->source_terminal = ns->terminal_id; + tw_event_send(e); +} + +static void schedule_terminal_send(int interval_id, tw_lp* lp) { + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + tw_event* e = tw_event_new(lp->gid, delay_until_ns(interval_id, PHASE_TERMINAL_SEND, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = TERMINAL_SEND; + m->interval_id = interval_id; + tw_event_send(e); +} + +static void schedule_switch_rate_eval(int interval_id, int port_id, tw_lp* lp) { + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + tw_event* e = + tw_event_new(lp->gid, delay_until_ns(interval_id, PHASE_SWITCH_RATE_EVAL, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = SWITCH_RATE_EVAL; + m->interval_id = interval_id; + m->port_id = port_id; + tw_event_send(e); +} + +static void undo_requested_switch_rate_eval(switch_state* ns, fluid_msg* m) { + if (!m->rc_rate_eval_request_created) { + return; + } + + const int port_id = m->rc_rate_eval_request_port; + const int interval_id = m->rc_rate_eval_request_interval; + if (port_id < 0 || port_id >= ns->num_ports || interval_id < 0 || + interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "invalid rollback rate-eval request: switch %d port %d interval %d", + ns->switch_id, port_id, interval_id); + } + if (!interval_flag_is_set(ns->scheduled_rate_eval[port_id], interval_id)) { + tw_error(TW_LOC, + "rollback expected scheduled rate-eval flag: switch %d port %d interval %d", + ns->switch_id, port_id, interval_id); + } + interval_flag_set(&ns->scheduled_rate_eval[port_id], interval_id, false); +} + +static void request_switch_rate_eval(switch_state* ns, int interval_id, int port_id, tw_lp* lp, + fluid_msg* cause_msg) { + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + if (port_id < 0 || port_id >= ns->num_ports) { + tw_error(TW_LOC, "invalid rate-eval request on switch %d port %d", ns->switch_id, port_id); + } + + interval_flags* scheduled = &ns->scheduled_rate_eval[port_id]; + if (interval_flag_is_set(*scheduled, interval_id)) { + return; + } + if (cause_msg != NULL && cause_msg->rc_rate_eval_request_created) { + tw_error(TW_LOC, "event attempted to create more than one rate-eval request on switch %d", + ns->switch_id); + } + + interval_flag_set(scheduled, interval_id, true); + schedule_switch_rate_eval(interval_id, port_id, lp); + + if (cause_msg != NULL) { + cause_msg->rc_rate_eval_request_created = 1; + cause_msg->rc_rate_eval_request_interval = interval_id; + cause_msg->rc_rate_eval_request_port = port_id; + } +} + +static void schedule_terminal_rate_update(int interval_id, int terminal_id, + unsigned long long flow_id, int destination_terminal, + double rate_mbps, int rate_epoch, int scope_key_type, + int scope_key_index, tw_lp* lp) { + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + tw_event* e = tw_event_new(get_terminal_gid(terminal_id), + delay_until_ns(interval_id, PHASE_TERMINAL_RATE_UPDATE, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = TERMINAL_RATE_UPDATE; + m->interval_id = interval_id; + m->source_terminal = terminal_id; + m->destination_terminal = destination_terminal; + m->flowlet_id = flow_id; + m->rate_mbps = rate_mbps; + m->rate_epoch = rate_epoch; + m->rate_scope_key_type = scope_key_type; + m->rate_scope_key_index = scope_key_index; + tw_event_send(e); +} + +static void schedule_switch_rate_feedback(int interval_id, int upstream_switch, + int downstream_switch, unsigned long long flow_id, + int source_terminal, int destination_terminal, + double rate_mbps, int rate_epoch, int scope_key_type, + int scope_key_index, tw_lp* lp) { + const int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + tw_event* e = tw_event_new(get_switch_gid(upstream_switch), + delay_until_ns(interval_id, PHASE_SWITCH_RATE_FEEDBACK, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = SWITCH_RATE_FEEDBACK; + m->interval_id = interval_id; + m->source_switch = downstream_switch; + m->source_terminal = source_terminal; + m->destination_terminal = destination_terminal; + m->flowlet_id = flow_id; + m->rate_mbps = rate_mbps; + m->rate_epoch = rate_epoch; + m->rate_scope_key_type = scope_key_type; + m->rate_scope_key_index = scope_key_index; + tw_event_send(e); +} + +static void schedule_switch_egress(int event_type, int interval_id, int port_id, tw_lp* lp) { + if (interval_id >= cfg.num_send_intervals + cfg.num_drain_intervals) { + return; + } + + double phase = 0.0; + if (event_type == SWITCH_EGRESS_EARLY) { + phase = PHASE_EARLY_SWITCH_EGRESS; + } else if (event_type == SWITCH_EGRESS_LATE) { + phase = PHASE_LATE_SWITCH_EGRESS; + } else { + tw_error(TW_LOC, "invalid switch egress event type %d", event_type); + } + + tw_event* e = tw_event_new(lp->gid, delay_until_ns(interval_id, phase, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = event_type; + m->interval_id = interval_id; + m->port_id = port_id; + tw_event_send(e); +} + +static interval_flags* scheduled_egress_flags(switch_state* ns, int event_type, int port_id) { + if (event_type == SWITCH_EGRESS_EARLY) { + return &ns->scheduled_early_egress[port_id]; + } + if (event_type == SWITCH_EGRESS_LATE) { + return &ns->scheduled_late_egress[port_id]; + } + tw_error(TW_LOC, "invalid switch egress event type %d", event_type); + return NULL; +} + +static void undo_requested_switch_egress(switch_state* ns, fluid_msg* m) { + if (!m->rc_egress_request_created) { + return; + } + + interval_flags* flags = + scheduled_egress_flags(ns, m->rc_egress_request_event_type, m->rc_egress_request_port); + int interval_id = m->rc_egress_request_interval; + if (flags == NULL || interval_id < 0 || interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "invalid rollback egress request: switch %d port %d interval %d", + ns->switch_id, m->rc_egress_request_port, interval_id); + } + if (!interval_flag_is_set(*flags, interval_id)) { + tw_error(TW_LOC, "rollback expected scheduled egress flag: switch %d port %d interval %d", + ns->switch_id, m->rc_egress_request_port, interval_id); + } + interval_flag_set(flags, interval_id, false); +} + +static void request_switch_egress(switch_state* ns, int event_type, int interval_id, int port_id, + tw_lp* lp, fluid_msg* cause_msg) { + if (port_id < 0 || port_id >= ns->num_ports) { + tw_error(TW_LOC, "invalid request_switch_egress port %d on switch %d", port_id, + ns->switch_id); + } + + int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + + interval_flags* flags = scheduled_egress_flags(ns, event_type, port_id); + if (flags == NULL || interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "egress scheduling interval %d out of range on switch %d port %d", + interval_id, ns->switch_id, port_id); + } + if (interval_flag_is_set(*flags, interval_id)) { + return; + } + + if (cause_msg != NULL && cause_msg->rc_egress_request_created) { + tw_error(TW_LOC, "event attempted to create more than one egress request on switch %d", + ns->switch_id); + } + + interval_flag_set(flags, interval_id, true); + schedule_switch_egress(event_type, interval_id, port_id, lp); + + if (cause_msg != NULL) { + cause_msg->rc_egress_request_created = 1; + cause_msg->rc_egress_request_event_type = event_type; + cause_msg->rc_egress_request_interval = interval_id; + cause_msg->rc_egress_request_port = port_id; + } +} + +static void schedule_pause_update(int interval_id, tw_lpid dst_gid, int pause_asserted, + int pause_source_switch, tw_lp* lp) { + tw_event* e = + tw_event_new(dst_gid, delay_until_ns(interval_id, PHASE_ETHERNET_PAUSE_FRAME_UPDATE, lp), + lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = ETHERNET_PAUSE_FRAME_UPDATE; + m->interval_id = interval_id; + m->pause_asserted = pause_asserted; + m->pause_source_switch = pause_source_switch; + tw_event_send(e); +} + +static bool switch_has_directed_link_to(int src_switch, int dst_switch) { + const switch_info& sw = switches[src_switch]; + for (size_t i = 0; i < sw.links.size(); ++i) { + if (sw.links[i].dst_switch == dst_switch) { + return true; + } + } + return false; +} + +static int find_ingress_link(const switch_state* ns, int is_terminal, int peer_index) { + for (int i = 0; i < ns->num_ingress_links; ++i) { + const ingress_desc& ingress = ns->ingress_links[i]; + if (ingress.is_terminal == is_terminal && ingress.peer_index == peer_index) { + return i; + } + } + return -1; +} + +static void send_pause_update_for_ingress(switch_state* ns, int ingress_id, int interval_id, + int pause_asserted, tw_lp* lp) { + if (ingress_id < 0 || ingress_id >= ns->num_ingress_links) { + tw_error(TW_LOC, "invalid ingress id %d on switch %d", ingress_id, ns->switch_id); + } + const ingress_desc& ingress = ns->ingress_links[ingress_id]; + tw_lpid dst_gid = ingress.is_terminal ? get_terminal_gid(ingress.peer_index) + : get_switch_gid(ingress.peer_index); + schedule_pause_update(interval_id, dst_gid, pause_asserted, ns->switch_id, lp); + ns->pause_updates_sent++; + if (pause_asserted) { + ns->pause_frames_sent++; + } else { + ns->resume_frames_sent++; + } +} + +static void schedule_ethernet_pause_eval(int interval_id, tw_lp* lp) { + int total_intervals = cfg.num_send_intervals + cfg.num_drain_intervals; + if (interval_id < 0 || interval_id >= total_intervals) { + return; + } + + tw_event* e = + tw_event_new(lp->gid, delay_until_ns(interval_id, PHASE_ETHERNET_PAUSE_EVAL, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = ETHERNET_PAUSE_EVAL; + m->interval_id = interval_id; + tw_event_send(e); +} + +static void record_pause_change(fluid_msg* m, int ingress_id, const ingress_desc& ingress, + int sent_asserted) { + if (m->rc_pause_change_count >= MAX_PAUSE_INGRESS_LINKS) { + tw_error(TW_LOC, "too many PAUSE ingress changes in one event"); + } + int i = m->rc_pause_change_count++; + m->rc_pause_ingress_id[i] = ingress_id; + m->rc_pause_prev_asserted[i] = ingress.pause_asserted; + m->rc_pause_prev_last_interval[i] = ingress.last_pause_frame_interval; + m->rc_pause_sent_asserted[i] = sent_asserted; +} + +static void handle_ethernet_pause_eval(switch_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_pause_change_count = 0; + + const double shared_occupancy_mbit = queued_mbit_on_switch(ns); + + /* + * The physical buffer is shared. PAUSE decisions therefore use global + * high/low watermarks, while per-ingress queued_mbit is used only to choose + * which directly connected senders should be throttled. + * + * Hysteresis policy: + * - at or below the low watermark, resume every paused ingress; + * - at or above the high watermark, keep already-paused ingresses and + * pause the largest unpaused contributors until their combined queued + * traffic covers the occupancy above the high watermark; + * - between the watermarks, preserve the current PAUSE set; + * - refresh each asserted PAUSE only when its configured timer expires. + * + * Contributor ordering is deterministic: descending queued bytes, then + * ascending ingress id. This is required for sequential/optimistic parity. + */ + if (shared_occupancy_mbit <= ns->pause_low_watermark_mbit + EPS) { + for (int ingress_id = 0; ingress_id < ns->num_ingress_links; ++ingress_id) { + ingress_desc& ingress = ns->ingress_links[ingress_id]; + if (!ingress.pause_asserted) { + continue; + } + + record_pause_change(m, ingress_id, ingress, 0); + ingress.pause_asserted = 0; + ingress.last_pause_frame_interval = -1; + send_pause_update_for_ingress(ns, ingress_id, m->interval_id, 0, lp); + } + } else { + if (shared_occupancy_mbit + EPS >= ns->pause_high_watermark_mbit) { + double covered_mbit = 0.0; + bool has_paused_ingress = false; + int candidates[MAX_PAUSE_INGRESS_LINKS]; + int candidate_count = 0; + + for (int ingress_id = 0; ingress_id < ns->num_ingress_links; ++ingress_id) { + const ingress_desc& ingress = ns->ingress_links[ingress_id]; + if (ingress.pause_asserted) { + has_paused_ingress = true; + covered_mbit += ingress.queued_mbit; + } else if (ingress.queued_mbit > EPS) { + candidates[candidate_count++] = ingress_id; + } + } + + std::sort(candidates, candidates + candidate_count, [ns](int lhs, int rhs) { + const double lhs_mbit = ns->ingress_links[lhs].queued_mbit; + const double rhs_mbit = ns->ingress_links[rhs].queued_mbit; + if (std::fabs(lhs_mbit - rhs_mbit) > EPS) { + return lhs_mbit > rhs_mbit; + } + return lhs < rhs; + }); + + const double excess_mbit = shared_occupancy_mbit - ns->pause_high_watermark_mbit; + for (int candidate = 0; candidate < candidate_count; ++candidate) { + const int ingress_id = candidates[candidate]; + if (has_paused_ingress && covered_mbit + EPS >= excess_mbit) { + break; + } + + ingress_desc& ingress = ns->ingress_links[ingress_id]; + record_pause_change(m, ingress_id, ingress, 1); + ingress.pause_asserted = 1; + ingress.last_pause_frame_interval = m->interval_id; + has_paused_ingress = true; + covered_mbit += ingress.queued_mbit; + send_pause_update_for_ingress(ns, ingress_id, m->interval_id, 1, lp); + } + } + + for (int ingress_id = 0; ingress_id < ns->num_ingress_links; ++ingress_id) { + ingress_desc& ingress = ns->ingress_links[ingress_id]; + if (!ingress.pause_asserted || + m->interval_id < ingress.last_pause_frame_interval + cfg.pause_duration_intervals) { + continue; + } + + record_pause_change(m, ingress_id, ingress, 1); + ingress.last_pause_frame_interval = m->interval_id; + send_pause_update_for_ingress(ns, ingress_id, m->interval_id, 1, lp); + } + } + + schedule_ethernet_pause_eval(m->interval_id + 1, lp); +} + +static void schedule_arrival(int interval_id, tw_lpid dst_gid, const fluid_msg* src_msg, + double mbit, tw_lp* lp) { + tw_event* e = tw_event_new(dst_gid, delay_until_ns(interval_id, PHASE_FLOWLET_ARRIVAL, lp), lp); + fluid_msg* m = (fluid_msg*)tw_event_data(e); + memset(m, 0, sizeof(*m)); + m->event_type = FLOWLET_ARRIVAL; + m->interval_id = interval_id; + m->source_terminal = src_msg->source_terminal; + m->destination_terminal = src_msg->destination_terminal; + m->source_switch = src_msg->source_switch; + m->creation_interval = src_msg->creation_interval; + m->flowlet_id = src_msg->flowlet_id; + m->final_segment_sent = src_msg->final_segment_sent; + m->mbit = mbit; + tw_event_send(e); +} + + +static int find_source_flow_index(const terminal_state* ns, unsigned long long flow_id) { + for (int i = 0; i < (int)ns->source_flows.size(); ++i) { + const source_flow& f = ns->source_flows[i]; + if (f.flow_id == flow_id) { + return i; + } + } + return -1; +} + +static std::uint64_t extend_path_hash(std::uint64_t hash, int switch_id) { + /* FNV-1a over fixed-width switch ids. */ + const std::uint32_t value = (std::uint32_t)switch_id; + for (int byte = 0; byte < 4; ++byte) { + hash ^= (value >> (byte * 8)) & 0xffu; + hash *= UINT64_C(1099511628211); + } + return hash; +} + +static int find_reusable_source_flow_index(const terminal_state* ns) { + for (int i = 0; i < ns->source_flows.size(); ++i) { + if (ns->source_flows[i].remaining_source_mbit <= EPS) { + return i; + } + } + return -1; +} + +static int find_rate_cache_index(const terminal_state* ns, int key_type, int key_index, + int prefix_hops, std::uint64_t path_hash) { + for (int i = 0; i < MAX_RATE_CACHE_ENTRIES; ++i) { + const rate_cache_entry& entry = ns->rate_cache[i]; + if (!entry.valid || entry.key_type != key_type || entry.key_index != key_index) { + continue; + } + if (key_type == RATE_CACHE_DESTINATION_TERMINAL || + (entry.prefix_hops == prefix_hops && entry.path_hash == path_hash)) { + return i; + } + } + return -1; +} + +static int find_free_rate_cache_index(const terminal_state* ns) { + for (int i = 0; i < MAX_RATE_CACHE_ENTRIES; ++i) { + if (!ns->rate_cache[i].valid) { + return i; + } + } + return -1; +} + +static bool route_prefix_identity(int source_switch, int destination_switch, int prefix_end_switch, + int* prefix_hops_out, std::uint64_t* path_hash_out) { + int current_switch = source_switch; + int hop_count = 0; + std::uint64_t path_hash = UINT64_C(1469598103934665603); + path_hash = extend_path_hash(path_hash, source_switch); + + while (current_switch != destination_switch) { + const int next_switch = next_switch_table[current_switch][destination_switch]; + if (next_switch < 0 || next_switch == current_switch) { + return false; + } + ++hop_count; + if (hop_count > (int)switches.size()) { + tw_error(TW_LOC, "route loop while constructing rate-cache key"); + } + path_hash = extend_path_hash(path_hash, next_switch); + if (next_switch == prefix_end_switch) { + *prefix_hops_out = hop_count; + *path_hash_out = path_hash; + return true; + } + current_switch = next_switch; + } + return false; +} + +static double cached_initial_rate_mbps(const terminal_state* ns, int destination_terminal) { + const double access_rate = switches[ns->attached_switch].terminal_bandwidth_mbps; + double cached_rate = access_rate; + + const int exact_index = + find_rate_cache_index(ns, RATE_CACHE_DESTINATION_TERMINAL, destination_terminal, 0, 0); + if (exact_index >= 0) { + cached_rate = std::min(cached_rate, ns->rate_cache[exact_index].rate_mbps); + } + + const int destination_switch = terminals[destination_terminal].switch_id; + int current_switch = ns->attached_switch; + int hop_count = 0; + std::uint64_t path_hash = UINT64_C(1469598103934665603); + path_hash = extend_path_hash(path_hash, current_switch); + + /* Apply every known constraint on the selected route prefix. */ + while (current_switch != destination_switch) { + const int next_switch = next_switch_table[current_switch][destination_switch]; + if (next_switch < 0 || next_switch == current_switch) { + break; + } + ++hop_count; + if (hop_count > (int)switches.size()) { + tw_error(TW_LOC, "route loop while looking up terminal %d rate cache", ns->terminal_id); + } + path_hash = extend_path_hash(path_hash, next_switch); + + const int cache_index = + find_rate_cache_index(ns, RATE_CACHE_SWITCH_PREFIX, next_switch, hop_count, path_hash); + if (cache_index >= 0) { + cached_rate = std::min(cached_rate, ns->rate_cache[cache_index].rate_mbps); + } + current_switch = next_switch; + } + + return std::max(0.0, cached_rate); +} + +static bool update_rate_cache(terminal_state* ns, fluid_msg* m, double new_rate_mbps) { + m->rc_rate_cache_changed = 0; + m->rc_rate_cache_index = -1; + m->rc_rate_cache_was_created = 0; + + if (m->rate_scope_key_type != RATE_CACHE_SWITCH_PREFIX && + m->rate_scope_key_type != RATE_CACHE_DESTINATION_TERMINAL) { + return false; + } + + int prefix_hops = 0; + std::uint64_t path_hash = 0; + if (m->rate_scope_key_type == RATE_CACHE_SWITCH_PREFIX) { + const int destination_switch = terminals[m->destination_terminal].switch_id; + if (!route_prefix_identity(ns->attached_switch, destination_switch, m->rate_scope_key_index, + &prefix_hops, &path_hash)) { + return false; + } + } + + int index = find_rate_cache_index(ns, m->rate_scope_key_type, m->rate_scope_key_index, + prefix_hops, path_hash); + if (index < 0) { + index = find_free_rate_cache_index(ns); + if (index < 0) { + tw_error(TW_LOC, "terminal %d exhausted MAX_RATE_CACHE_ENTRIES=%d", ns->terminal_id, + MAX_RATE_CACHE_ENTRIES); + } + rate_cache_entry& entry = ns->rate_cache[index]; + memset(&entry, 0, sizeof(entry)); + entry.valid = 1; + entry.key_type = m->rate_scope_key_type; + entry.key_index = m->rate_scope_key_index; + entry.prefix_hops = prefix_hops; + entry.path_hash = path_hash; + entry.rate_mbps = new_rate_mbps; + entry.rate_epoch = m->rate_epoch; + ns->rate_cache_entry_count++; + m->rc_rate_cache_changed = 1; + m->rc_rate_cache_index = index; + m->rc_rate_cache_was_created = 1; + return true; + } + + rate_cache_entry& entry = ns->rate_cache[index]; + if (m->rate_epoch < entry.rate_epoch) { + return false; + } + + const double updated_rate = m->rate_epoch == entry.rate_epoch + ? std::min(entry.rate_mbps, new_rate_mbps) + : new_rate_mbps; + if (m->rate_epoch == entry.rate_epoch && std::fabs(updated_rate - entry.rate_mbps) <= EPS) { + return false; + } + + m->rc_rate_cache_before = entry; + m->rc_rate_cache_changed = 1; + m->rc_rate_cache_index = index; + entry.rate_mbps = updated_rate; + entry.rate_epoch = m->rate_epoch; + return true; +} + +static void terminal_init(terminal_state* ns, tw_lp* lp) { + memset(ns, 0, sizeof(*ns)); + ns->terminal_id = codes_mapping_get_lp_relative_id(lp->gid, 0, 0); + if (ns->terminal_id < 0 || ns->terminal_id >= (int)terminals.size()) { + tw_error(TW_LOC, "terminal LP relative id %d out of range", ns->terminal_id); + } + ns->attached_switch = terminals[ns->terminal_id].switch_id; + ns->next_flowlet_seq = 0; + ns->link_paused_until_interval = -1; + schedule_workload_generate(ns, first_terminal_generate_interval(), lp); + schedule_terminal_send(0, lp); +} + +static int choose_random_destination(int self, tw_lp* lp) { + int n = (int)terminals.size(); + if (n <= 1) { + return self; + } + int offset = (int)tw_rand_integer(lp->rng, 1, n - 1); + return (self + offset) % n; +} + +static double random_flow_size_mbit(tw_lp* lp) { + double u = tw_rand_unif(lp->rng); + return cfg.random_flow_min_mbit + u * (cfg.random_flow_max_mbit - cfg.random_flow_min_mbit); +} + +static void compute_max_min_allocations(const double* requested, int count, double budget, + double* allocations) { + for (int i = 0; i < count; ++i) { + allocations[i] = 0.0; + } + double remaining = std::max(0.0, budget); + while (remaining > EPS) { + int unsatisfied = 0; + for (int i = 0; i < count; ++i) { + if (requested[i] - allocations[i] > EPS) { + ++unsatisfied; + } + } + if (unsatisfied == 0) { + break; + } + const double share = remaining / unsatisfied; + double allocated_round = 0.0; + for (int i = 0; i < count; ++i) { + const double need = requested[i] - allocations[i]; + if (need <= EPS) { + continue; + } + const double give = std::min(share, need); + allocations[i] += give; + allocated_round += give; + } + if (allocated_round <= EPS) { + break; + } + remaining -= allocated_round; + if (remaining < 0.0 && remaining > -EPS) { + remaining = 0.0; + } + } +} + +static void log_terminal_generate_event(const terminal_state* ns, const fluid_msg* m) { + if (m->rc_generated) { + append_terminal_log(m->interval_id, "generate_flow", ns->terminal_id, + m->destination_terminal, m->mbit); + } +} + +static void log_terminal_send_event(const terminal_state* ns, const fluid_msg* m) { + for (int i = 0; i < m->rc_alloc_count; ++i) { + const rc_alloc_record& rc = m->rc_allocs[i]; + if (rc.valid && rc.send_mbit > EPS) { + append_terminal_log(m->interval_id, "send", ns->terminal_id, + rc.before.destination_terminal, rc.send_mbit); + } + } +} + +static void log_terminal_receive_event(const terminal_state* ns, const fluid_msg* m) { + append_terminal_log(m->interval_id, "receive", ns->terminal_id, m->source_terminal, m->mbit); +} + +static void log_switch_arrival_event(const switch_state* ns, const fluid_msg* m) { + if (m->rc_no_route) { + append_switch_log(m->interval_id, "drop_no_route", ns->switch_id, -1, 0, -1, 0.0, 0.0, 0.0, + 0.0, m->rc_log_shared_queued_before_mbit, + m->rc_log_shared_queued_after_mbit, ns->shared_buffer_mbit, + m->rc_dropped_mbit, 0); + append_flowlet_log(m->interval_id, "drop_no_route", ns->switch_id, -1, 0, -1, m->flowlet_id, + m->source_terminal, m->destination_terminal, m->creation_interval, 0.0, + 0.0, 0.0, 0.0, m->rc_dropped_mbit); + return; + } + + if (m->rc_accepted_mbit > EPS) { + append_flowlet_log(m->interval_id, + m->rc_coalesced ? "stage_arrival_coalesce" : "stage_arrival", + ns->switch_id, m->rc_port_id, m->rc_log_target_is_terminal, + m->rc_log_target_index, m->flowlet_id, m->source_terminal, + m->destination_terminal, m->creation_interval, m->rc_log_capacity_mbit, + m->rc_log_port_queued_before_mbit, 0.0, + m->rc_log_flowlet_remaining_after_mbit, 0.0); + } +} + +static void log_switch_egress_event(const switch_state* ns, const fluid_msg* m) { + for (int i = 0; i < m->rc_alloc_count; ++i) { + const rc_alloc_record* rc = &m->rc_allocs[i]; + + if (!rc->valid) { + continue; + } + + double remaining_after = rc->before.remaining_mbit - rc->send_mbit; + if (remaining_after < 0.0 && remaining_after > -EPS) { + remaining_after = 0.0; + } + + if (rc->send_mbit > EPS) { + append_flowlet_log(m->interval_id, + rc->source_is_staged ? "allocate_send_arrival" + : "allocate_send_buffered", + ns->switch_id, m->port_id, m->rc_log_target_is_terminal, + m->rc_log_target_index, rc->before.flowlet_id, + rc->before.source_terminal, rc->before.destination_terminal, + rc->before.creation_interval, m->rc_log_capacity_mbit, + m->rc_log_port_queued_before_mbit, rc->send_mbit, remaining_after, + 0.0); + } + if (rc->buffered_mbit > EPS) { + append_flowlet_log(m->interval_id, "enqueue_residual", ns->switch_id, m->port_id, + m->rc_log_target_is_terminal, m->rc_log_target_index, + rc->before.flowlet_id, rc->before.source_terminal, + rc->before.destination_terminal, rc->before.creation_interval, + m->rc_log_capacity_mbit, m->rc_log_port_queued_before_mbit, 0.0, + rc->buffered_mbit, 0.0); + } + if (rc->dropped_mbit > EPS) { + append_flowlet_log(m->interval_id, "drop_shared_buffer_overflow", ns->switch_id, + m->port_id, m->rc_log_target_is_terminal, m->rc_log_target_index, + rc->before.flowlet_id, rc->before.source_terminal, + rc->before.destination_terminal, rc->before.creation_interval, + m->rc_log_capacity_mbit, m->rc_log_port_queued_before_mbit, 0.0, 0.0, + rc->dropped_mbit); + } + } + + append_switch_log(m->interval_id, "egress", ns->switch_id, m->port_id, + m->rc_log_target_is_terminal, m->rc_log_target_index, m->rc_log_capacity_mbit, + m->rc_log_port_queued_before_mbit, m->rc_log_sent_total_mbit, + m->rc_log_port_queued_after_mbit, m->rc_log_shared_queued_before_mbit, + m->rc_log_shared_queued_after_mbit, ns->shared_buffer_mbit, + m->rc_dropped_mbit, m->rc_log_active_after_entries); + + append_switch_training_log(m->interval_id, ns->switch_id, m->port_id, + m->rc_log_target_is_terminal, m->rc_log_target_index, + m->rc_log_capacity_mbit, m->rc_log_port_queued_before_mbit, + m->rc_log_sent_total_mbit, m->rc_log_port_queued_after_mbit, + m->rc_dropped_mbit, m->rc_log_active_before_entries, + m->rc_log_active_after_entries, m); +} + +static void handle_workload_generate(terminal_state* ns, fluid_msg* m, tw_lp* lp) { + const int interval = m->interval_id; + m->rc_rng_count = 0; + m->rc_generated = 0; + m->rc_terminal_flow_index = -1; + m->rc_terminal_flow_appended = 0; + + const int dst = choose_random_destination(ns->terminal_id, lp); + m->rc_rng_count++; + const double total_mbit = random_flow_size_mbit(lp); + m->rc_rng_count++; + + if (total_mbit > EPS) { + int flow_index = find_reusable_source_flow_index(ns); + if (flow_index < 0 && ns->source_flows.size() >= MAX_SOURCE_FLOWS_PER_TERMINAL) { + tw_error(TW_LOC, + "terminal %d has %d concurrent source flows, exceeding " + "MAX_SOURCE_FLOWS_PER_TERMINAL=%d", + ns->terminal_id, ns->source_flows.size(), MAX_SOURCE_FLOWS_PER_TERMINAL); + } + source_flow flow; + memset(&flow, 0, sizeof(flow)); + flow.flow_id = ((unsigned long long)ns->terminal_id << 48) | + (unsigned long long)ns->next_flowlet_seq++; + flow.destination_terminal = dst; + flow.creation_interval = interval; + flow.remaining_source_mbit = total_mbit; + flow.current_send_rate_mbps = cached_initial_rate_mbps(ns, dst); + flow.rate_epoch = -1; + + if (flow_index >= 0) { + m->rc_terminal_flow_before = ns->source_flows[flow_index]; + ns->source_flows[flow_index] = flow; + } else { + ns->source_flows.push_back(flow); + flow_index = ns->source_flows.size() - 1; + m->rc_terminal_flow_appended = 1; + } + ns->generated_mbit += total_mbit; + ns->generated_flowlets++; + + m->rc_generated = 1; + m->rc_terminal_flow_index = flow_index; + m->destination_terminal = dst; + m->flowlet_id = flow.flow_id; + m->mbit = total_mbit; + log_terminal_generate_event(ns, m); + } + + schedule_workload_generate(ns, next_terminal_generate_interval_after(interval), lp); +} + +static void handle_terminal_send(terminal_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_alloc_count = 0; + m->rc_pause_target_port = -1; + + double requested[MAX_SOURCE_FLOWS_PER_TERMINAL] = {0.0}; + int active_count = 0; + for (int i = 0; i < (int)ns->source_flows.size(); ++i) { + const source_flow& flow = ns->source_flows[i]; + if (flow.remaining_source_mbit <= EPS) { + continue; + } + requested[i] = std::min(flow.remaining_source_mbit, + flow.current_send_rate_mbps * cfg.interval_seconds); + ++active_count; + } + if (active_count > MAX_RC_ALLOCATIONS) { + tw_error(TW_LOC, "terminal %d has %d active source flows, exceeding MAX_RC_ALLOCATIONS=%d", + ns->terminal_id, active_count, MAX_RC_ALLOCATIONS); + } + + if (m->interval_id < ns->link_paused_until_interval) { + if (active_count > 0) { + ns->link_paused_intervals++; + m->rc_pause_target_port = -2; + } + } else { + const double terminal_budget = + switches[ns->attached_switch].terminal_bandwidth_mbps * cfg.interval_seconds; + double allocations[MAX_SOURCE_FLOWS_PER_TERMINAL] = {0.0}; + compute_max_min_allocations(requested, ns->source_flows.size(), terminal_budget, + allocations); + + for (int i = 0; i < ns->source_flows.size(); ++i) { + const double send_mbit = allocations[i]; + if (send_mbit <= EPS) { + continue; + } + + source_flow& flow = ns->source_flows[i]; + rc_alloc_record& rc = m->rc_allocs[m->rc_alloc_count++]; + memset(&rc, 0, sizeof(rc)); + rc.valid = 1; + rc.queue_index = i; + rc.before.flowlet_id = flow.flow_id; + rc.before.destination_terminal = flow.destination_terminal; + rc.send_mbit = send_mbit; + + flow.remaining_source_mbit -= send_mbit; + if (flow.remaining_source_mbit < 0.0 && flow.remaining_source_mbit > -EPS) { + flow.remaining_source_mbit = 0.0; + } + + fluid_msg out_msg; + memset(&out_msg, 0, sizeof(out_msg)); + out_msg.event_type = FLOWLET_ARRIVAL; + out_msg.interval_id = m->interval_id + 1; + out_msg.source_terminal = ns->terminal_id; + out_msg.destination_terminal = flow.destination_terminal; + out_msg.source_switch = ns->attached_switch; + out_msg.creation_interval = flow.creation_interval; + out_msg.flowlet_id = flow.flow_id; + out_msg.final_segment_sent = flow.remaining_source_mbit <= EPS; + out_msg.mbit = send_mbit; + + schedule_arrival(m->interval_id + 1, get_switch_gid(ns->attached_switch), &out_msg, + send_mbit, lp); + ns->sent_to_switch_mbit += send_mbit; + } + } + + log_terminal_send_event(ns, m); + schedule_terminal_send(m->interval_id + 1, lp); +} + +static void handle_terminal_rate_update(terminal_state* ns, fluid_msg* m) { + m->rc_rate_update_applied = 0; + m->rc_terminal_flow_index = -1; + + const double access_rate = switches[ns->attached_switch].terminal_bandwidth_mbps; + const double new_rate = std::max(0.0, std::min(m->rate_mbps, access_rate)); + const bool cache_changed = update_rate_cache(ns, m, new_rate); + + m->rc_terminal_flow_index = find_source_flow_index(ns, m->flowlet_id); + if (m->rc_terminal_flow_index >= 0) { + source_flow& flow = ns->source_flows[m->rc_terminal_flow_index]; + if (flow.remaining_source_mbit > EPS && m->rate_epoch >= flow.rate_epoch) { + m->rc_terminal_flow_before = flow; + if (m->rate_epoch == flow.rate_epoch) { + flow.current_send_rate_mbps = std::min(flow.current_send_rate_mbps, new_rate); + } else { + flow.current_send_rate_mbps = new_rate; + flow.rate_epoch = m->rate_epoch; + } + m->rc_rate_update_applied = 1; + } + } + + if (cache_changed || m->rc_rate_update_applied) { + ns->rate_updates_received++; + } +} + +static void handle_terminal_arrival(terminal_state* ns, fluid_msg* m) { + ns->received_mbit += m->mbit; + ns->received_fragments++; + log_terminal_receive_event(ns, m); +} + +static void terminal_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + switch (m->event_type) { + case TERMINAL_WORKLOAD_GENERATE: + handle_workload_generate(ns, m, lp); + break; + case TERMINAL_SEND: + handle_terminal_send(ns, m, lp); + break; + case TERMINAL_RATE_UPDATE: + handle_terminal_rate_update(ns, m); + break; + case FLOWLET_ARRIVAL: + handle_terminal_arrival(ns, m); + break; + case ETHERNET_PAUSE_FRAME_UPDATE: + m->rc_prev_pause_until_interval = ns->link_paused_until_interval; + if (m->pause_asserted) { + ns->link_paused_until_interval = + std::max(ns->link_paused_until_interval, + m->interval_id + cfg.pause_duration_intervals + 1); + } else { + ns->link_paused_until_interval = m->interval_id; + } + ns->pause_updates_received++; + if (m->pause_asserted) { + ns->pause_frames_received++; + } else { + ns->resume_frames_received++; + } + break; + default: + tw_error(TW_LOC, "terminal received unknown event type %d", m->event_type); + } +} + +static void terminal_rev_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + switch (m->event_type) { + case TERMINAL_WORKLOAD_GENERATE: + if (m->rc_generated) { + if (m->rc_terminal_flow_appended) { + if (m->rc_terminal_flow_index != ns->source_flows.size() - 1) { + tw_error(TW_LOC, "terminal flow rollback order mismatch"); + } + ns->source_flows.pop_back(); + } else { + ns->source_flows[m->rc_terminal_flow_index] = m->rc_terminal_flow_before; + } + ns->generated_mbit -= m->mbit; + ns->generated_flowlets--; + if (ns->next_flowlet_seq == 0) { + tw_error(TW_LOC, "terminal %d flow sequence underflow", ns->terminal_id); + } + ns->next_flowlet_seq--; + } + for (int i = 0; i < m->rc_rng_count; ++i) { + tw_rand_reverse_unif(lp->rng); + } + break; + case TERMINAL_SEND: + if (m->rc_pause_target_port == -2) { + ns->link_paused_intervals--; + } + for (int i = m->rc_alloc_count - 1; i >= 0; --i) { + const rc_alloc_record& rc = m->rc_allocs[i]; + if (!rc.valid) { + continue; + } + source_flow& flow = ns->source_flows[rc.queue_index]; + flow.remaining_source_mbit += rc.send_mbit; + ns->sent_to_switch_mbit -= rc.send_mbit; + } + break; + case TERMINAL_RATE_UPDATE: + if (m->rc_rate_update_applied) { + ns->source_flows[m->rc_terminal_flow_index] = m->rc_terminal_flow_before; + } + if (m->rc_rate_cache_changed) { + if (m->rc_rate_cache_was_created) { + ns->rate_cache[m->rc_rate_cache_index].valid = 0; + if (ns->rate_cache_entry_count <= 0) { + tw_error(TW_LOC, "terminal %d rate-cache count underflow", ns->terminal_id); + } + ns->rate_cache_entry_count--; + } else { + ns->rate_cache[m->rc_rate_cache_index] = m->rc_rate_cache_before; + } + } + if (m->rc_rate_update_applied || m->rc_rate_cache_changed) { + ns->rate_updates_received--; + } + break; + case FLOWLET_ARRIVAL: + ns->received_mbit -= m->mbit; + ns->received_fragments--; + break; + case ETHERNET_PAUSE_FRAME_UPDATE: + ns->link_paused_until_interval = m->rc_prev_pause_until_interval; + ns->pause_updates_received--; + if (m->pause_asserted) { + ns->pause_frames_received--; + } else { + ns->resume_frames_received--; + } + break; + default: + tw_error(TW_LOC, "terminal reverse received unknown event type %d", m->event_type); + } +} + +static void terminal_commit_event(terminal_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + (void)lp; + if (!fluid_csv_commit_logs_enabled()) { + return; + } + fluid_commit_logging_begin(); + switch (m->event_type) { + case TERMINAL_WORKLOAD_GENERATE: + log_terminal_generate_event(ns, m); + break; + case TERMINAL_SEND: + log_terminal_send_event(ns, m); + break; + case FLOWLET_ARRIVAL: + log_terminal_receive_event(ns, m); + break; + default: + break; + } + fluid_commit_logging_end(); +} + +static void terminal_finalize(terminal_state* ns, tw_lp* lp) { + double source_backlog_mbit = 0.0; + int active_source_flows = 0; + for (const source_flow& f : ns->source_flows) { + source_backlog_mbit += f.remaining_source_mbit; + if (f.remaining_source_mbit > EPS) { + ++active_source_flows; + } + } + const char* unit = output_data_field_suffix(); + printf("fluid-flow-wan-terminal gid=%llu terminal=%d switch=%d generated_%s=%.6f " + "sent_%s=%.6f source_backlog_%s=%.6f active_source_flows=%d " + "received_%s=%.6f rate_updates_received=%llu rate_cache_entries=%d " + "pause_updates_received=%llu " + "pause_frames_received=%llu resume_frames_received=%llu " + "link_paused_intervals=%llu generated_flows=%d received_fragments=%d\n", + (unsigned long long)lp->gid, ns->terminal_id, ns->attached_switch, unit, + to_output_data_unit(ns->generated_mbit), unit, + to_output_data_unit(ns->sent_to_switch_mbit), unit, + to_output_data_unit(source_backlog_mbit), active_source_flows, unit, + to_output_data_unit(ns->received_mbit), ns->rate_updates_received, + ns->rate_cache_entry_count, ns->pause_updates_received, ns->pause_frames_received, + ns->resume_frames_received, ns->link_paused_intervals, ns->generated_flowlets, + ns->received_fragments); +} + +static int find_rate_flow_index(const switch_state* ns, int port_id, unsigned long long flow_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return -1; + } + const fixed_vector& flows = + ns->rate_flows[port_id]; + for (int i = 0; i < (int)flows.size(); ++i) { + if (flows[i].flow_id == flow_id) { + return i; + } + } + return -1; +} + +static bool port_has_buffered_flow(const switch_state* ns, int port_id, + unsigned long long flow_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return false; + } + for (const queued_flowlet& q : ns->queues[port_id]) { + if (q.valid && q.flowlet_id == flow_id && q.remaining_mbit > EPS) { + return true; + } + } + for (const queued_flowlet& q : ns->staged_arrivals[port_id]) { + if (q.valid && q.flowlet_id == flow_id && q.remaining_mbit > EPS) { + return true; + } + } + return false; +} + +static bool rate_flow_is_active(const switch_state* ns, int port_id, const switch_rate_flow& flow) { + return !flow.final_segment_seen || port_has_buffered_flow(ns, port_id, flow.flow_id); +} + +static int active_rate_flow_count_on_port(const switch_state* ns, int port_id) { + if (port_id < 0 || port_id >= ns->num_ports) { + return 0; + } + + int active = 0; + for (const switch_rate_flow& flow : ns->rate_flows[port_id]) { + if (rate_flow_is_active(ns, port_id, flow)) { + ++active; + } + } + return active; +} + +static double effective_rate_mbps(const switch_state* ns, int port_id, const switch_rate_flow& flow, + int active_flow_count) { + if (active_flow_count <= 0) { + return 0.0; + } + + const double capacity_mbps = + ns->ports[port_id].capacity_mbit_per_interval / cfg.interval_seconds; + const double local_rate_mbps = capacity_mbps / active_flow_count; + const double source_access_rate = + switches[terminals[flow.source_terminal].switch_id].terminal_bandwidth_mbps; + + double advertised = std::min(local_rate_mbps, source_access_rate); + if (std::isfinite(flow.downstream_rate_mbps)) { + advertised = std::min(advertised, flow.downstream_rate_mbps); + } + return std::max(0.0, advertised); +} + +static void observe_rate_flow(switch_state* ns, int port_id, const fluid_msg* m, + fluid_msg* rc_msg) { + rc_msg->rc_rate_flow_created = 0; + rc_msg->rc_rate_flow_appended = 0; + rc_msg->rc_rate_flow_index = find_rate_flow_index(ns, port_id, m->flowlet_id); + fixed_vector& flows = ns->rate_flows[port_id]; + if (rc_msg->rc_rate_flow_index >= 0) { + switch_rate_flow& flow = flows[rc_msg->rc_rate_flow_index]; + rc_msg->rc_rate_flow_before = flow; + flow.ingress_id = m->rc_ingress_id; + flow.final_segment_seen |= m->final_segment_sent; + return; + } + + switch_rate_flow new_flow; + memset(&new_flow, 0, sizeof(new_flow)); + new_flow.flow_id = m->flowlet_id; + new_flow.source_terminal = m->source_terminal; + new_flow.destination_terminal = m->destination_terminal; + new_flow.ingress_id = m->rc_ingress_id; + new_flow.final_segment_seen = m->final_segment_sent; + new_flow.downstream_rate_mbps = std::numeric_limits::infinity(); + new_flow.downstream_rate_epoch = -1; + + if (flows.size() < MAX_FLOW_ENTRIES_PER_PORT) { + /* Keep completed entries while space remains so late downstream + * feedback can still traverse the reverse path and populate caches. */ + flows.push_back(new_flow); + rc_msg->rc_rate_flow_index = flows.size() - 1; + rc_msg->rc_rate_flow_appended = 1; + } else { + int reusable_index = -1; + for (int i = 0; i < flows.size(); ++i) { + if (!rate_flow_is_active(ns, port_id, flows[i])) { + reusable_index = i; + break; + } + } + if (reusable_index < 0) { + tw_error(TW_LOC, + "switch %d port %d has %d concurrent rate flows, " + "exceeding MAX_FLOW_ENTRIES_PER_PORT=%d", + ns->switch_id, port_id, flows.size(), MAX_FLOW_ENTRIES_PER_PORT); + } + rc_msg->rc_rate_flow_before = flows[reusable_index]; + flows[reusable_index] = new_flow; + rc_msg->rc_rate_flow_index = reusable_index; + } + rc_msg->rc_rate_flow_created = 1; +} + +static int output_port_for_destination(const switch_state* ns, int destination_terminal) { + int dst_sw = terminals[destination_terminal].switch_id; + if (dst_sw == ns->switch_id) { + return find_terminal_port(ns, destination_terminal); + } + int next_sw = next_switch_table[ns->switch_id][dst_sw]; + return next_sw < 0 ? -1 : find_switch_link_port(ns, next_sw); +} + +static void send_rate_feedback_upstream(switch_state* ns, const switch_rate_flow& flow, + double rate_mbps, int rate_epoch, int scope_key_type, + int scope_key_index, int interval_id, tw_lp* lp) { + if (flow.ingress_id < 0 || flow.ingress_id >= ns->num_ingress_links) { + tw_error(TW_LOC, "invalid rate-feedback ingress on switch %d", ns->switch_id); + } + const ingress_desc& ingress = ns->ingress_links[flow.ingress_id]; + int delivery_interval = interval_id + 1; + if (ingress.is_terminal) { + schedule_terminal_rate_update(delivery_interval, ingress.peer_index, flow.flow_id, + flow.destination_terminal, rate_mbps, rate_epoch, + scope_key_type, scope_key_index, lp); + } else { + schedule_switch_rate_feedback(delivery_interval, ingress.peer_index, ns->switch_id, + flow.flow_id, flow.source_terminal, flow.destination_terminal, + rate_mbps, rate_epoch, scope_key_type, scope_key_index, lp); + } +} + +static void handle_switch_rate_feedback(switch_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_rate_update_applied = 0; + m->rc_rate_flow_created = 0; + m->rc_rate_flow_appended = 0; + m->rc_rate_flow_index = -1; + int port_id = output_port_for_destination(ns, m->destination_terminal); + if (port_id < 0) { + return; + } + int idx = find_rate_flow_index(ns, port_id, m->flowlet_id); + if (idx < 0) { + return; + } + + switch_rate_flow& flow = (ns->rate_flows[port_id])[idx]; + const int active_count = active_rate_flow_count_on_port(ns, port_id); + const double old_effective_rate = rate_flow_is_active(ns, port_id, flow) + ? effective_rate_mbps(ns, port_id, flow, active_count) + : 0.0; + + m->rc_rate_flow_index = idx; + m->rc_port_id = port_id; + m->rc_rate_flow_before = flow; + if (m->rate_epoch < flow.downstream_rate_epoch) { + return; + } + if (m->rate_epoch == flow.downstream_rate_epoch) { + flow.downstream_rate_mbps = + std::min(flow.downstream_rate_mbps, std::max(0.0, m->rate_mbps)); + } else { + flow.downstream_rate_mbps = std::max(0.0, m->rate_mbps); + flow.downstream_rate_epoch = m->rate_epoch; + } + m->rc_rate_update_applied = 1; + + if (!rate_flow_is_active(ns, port_id, flow)) { + return; + } + + const double new_effective_rate = effective_rate_mbps(ns, port_id, flow, active_count); + if (fabs(new_effective_rate - old_effective_rate) > EPS) { + send_rate_feedback_upstream(ns, flow, new_effective_rate, m->rate_epoch, + m->rate_scope_key_type, m->rate_scope_key_index, m->interval_id, + lp); + } +} + +static void handle_switch_rate_eval(switch_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_rate_eval_event_active = 0; + + const int port_id = m->port_id; + if (port_id < 0 || port_id >= ns->num_ports || m->interval_id < 0 || + m->interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "invalid rate-eval event on switch %d port %d interval %d", ns->switch_id, + port_id, m->interval_id); + } + if (!interval_flag_is_set(ns->scheduled_rate_eval[port_id], m->interval_id)) { + return; + } + + interval_flag_set(&ns->scheduled_rate_eval[port_id], m->interval_id, false); + m->rc_rate_eval_event_active = 1; + + const int active_count = active_rate_flow_count_on_port(ns, port_id); + if (active_count <= 0) { + return; + } + + for (const switch_rate_flow& flow : ns->rate_flows[port_id]) { + if (!rate_flow_is_active(ns, port_id, flow)) { + continue; + } + const port_desc& port = ns->ports[port_id]; + const int scope_key_type = + port.is_terminal ? RATE_CACHE_DESTINATION_TERMINAL : RATE_CACHE_SWITCH_PREFIX; + const int scope_key_index = port.target_index; + send_rate_feedback_upstream(ns, flow, effective_rate_mbps(ns, port_id, flow, active_count), + m->interval_id, scope_key_type, scope_key_index, m->interval_id, + lp); + } +} + +static void switch_init(switch_state* ns, tw_lp* lp) { + memset(ns, 0, sizeof(*ns)); + ns->switch_id = codes_mapping_get_lp_relative_id(lp->gid, 0, 0); + if (ns->switch_id < 0 || ns->switch_id >= (int)switches.size()) { + tw_error(TW_LOC, "switch LP relative id %d out of range", ns->switch_id); + } + + for (int p = 0; p < MAX_PORTS_PER_SWITCH; ++p) { + ns->capacity_accounting_interval[p] = -1; + ns->capacity_used_mbit[p] = 0.0; + ns->output_link_paused_until_interval[p] = -1; + } + + const switch_info& sw = switches[ns->switch_id]; + + /* + * switch_buffer is modeled as one shared switch-wide pool. Output queues + * remain per port for scheduling, but admission checks total queued bytes + * across all ports on this switch. + */ + ns->shared_buffer_mbit = sw.switch_buffer_mbit; + if (ns->shared_buffer_mbit < 0.0) { + tw_error(TW_LOC, "negative shared switch buffer on switch %d", ns->switch_id); + } + ns->num_ingress_links = 0; + + for (int t = 0; t < sw.terminal_count; ++t) { + ingress_desc& ingress = ns->ingress_links[ns->num_ingress_links++]; + ingress.is_terminal = 1; + ingress.peer_index = sw.terminal_start + t; + ingress.queued_mbit = 0.0; + ingress.pause_asserted = 0; + ingress.last_pause_frame_interval = -1; + } + for (int upstream = 0; upstream < (int)switches.size(); ++upstream) { + if (upstream == ns->switch_id || !switch_has_directed_link_to(upstream, ns->switch_id)) { + continue; + } + if (ns->num_ingress_links >= MAX_PAUSE_INGRESS_LINKS) { + tw_error(TW_LOC, "too many ingress links on switch %d", ns->switch_id); + } + ingress_desc& ingress = ns->ingress_links[ns->num_ingress_links++]; + ingress.is_terminal = 0; + ingress.peer_index = upstream; + ingress.queued_mbit = 0.0; + ingress.pause_asserted = 0; + ingress.last_pause_frame_interval = -1; + } + if (ns->num_ingress_links <= 0) { + tw_error(TW_LOC, "switch %d has no ingress links", ns->switch_id); + } + ns->pause_high_watermark_mbit = ns->shared_buffer_mbit * cfg.pause_high_watermark_fraction; + ns->pause_low_watermark_mbit = ns->shared_buffer_mbit * cfg.pause_low_watermark_fraction; + + for (size_t i = 0; i < sw.links.size(); ++i) { + if (ns->num_ports >= MAX_PORTS_PER_SWITCH) { + tw_error(TW_LOC, "too many ports on switch %d", ns->switch_id); + } + port_desc* p = &ns->ports[ns->num_ports++]; + memset(p, 0, sizeof(*p)); + p->is_terminal = 0; + p->target_index = sw.links[i].dst_switch; + p->capacity_mbit_per_interval = sw.links[i].bandwidth_mbps * cfg.interval_seconds; + } + for (int t = 0; t < sw.terminal_count; ++t) { + if (ns->num_ports >= MAX_PORTS_PER_SWITCH) { + tw_error(TW_LOC, "too many ports on switch %d", ns->switch_id); + } + port_desc* p = &ns->ports[ns->num_ports++]; + memset(p, 0, sizeof(*p)); + p->is_terminal = 1; + p->target_index = sw.terminal_start + t; + p->capacity_mbit_per_interval = sw.terminal_bandwidth_mbps * cfg.interval_seconds; + } + + /* + * Egress is demand-driven. We no longer pre-schedule periodic empty + * egress events for every port. Arrivals schedule late egress in their + * arrival interval. Any residual bytes admitted to the physical buffer + * schedule early egress in the following interval. + * + * PAUSE hysteresis is evaluated exactly once per switch and interval by a + * periodic ETHERNET_PAUSE_EVAL event. + * + * Rate evaluation is demand-driven. A port is reevaluated only when its + * active flow set changes; downstream feedback is forwarded only when it + * changes the effective end-to-end rate. + */ + schedule_ethernet_pause_eval(0, lp); +} + + +static void handle_switch_arrival(switch_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_egress_request_created = 0; + m->rc_rate_eval_request_created = 0; + m->rc_rate_flow_created = 0; + m->rc_rate_flow_appended = 0; + m->rc_rate_flow_index = -1; + ns->received_fragments++; + + int ingress_id = -1; + if (m->source_switch == ns->switch_id) { + ingress_id = find_ingress_link(ns, 1, m->source_terminal); + } else { + ingress_id = find_ingress_link(ns, 0, m->source_switch); + } + if (ingress_id < 0) { + tw_error(TW_LOC, "switch %d could not identify ingress for source terminal %d switch %d", + ns->switch_id, m->source_terminal, m->source_switch); + } + m->rc_ingress_id = ingress_id; + + int dst_sw = terminals[m->destination_terminal].switch_id; + int port_id = -1; + if (dst_sw == ns->switch_id) { + port_id = find_terminal_port(ns, m->destination_terminal); + } else { + int next_sw = next_switch_table[ns->switch_id][dst_sw]; + if (next_sw >= 0) { + port_id = find_switch_link_port(ns, next_sw); + } + } + + if (port_id < 0) { + double shared_before = queued_mbit_on_switch(ns); + m->rc_no_route = 1; + m->rc_port_id = -1; + m->rc_accepted_mbit = 0.0; + m->rc_dropped_mbit = m->mbit; + m->rc_log_target_is_terminal = 0; + m->rc_log_target_index = -1; + m->rc_log_capacity_mbit = 0.0; + m->rc_log_port_queued_before_mbit = 0.0; + m->rc_log_port_queued_after_mbit = 0.0; + m->rc_log_shared_queued_before_mbit = shared_before; + m->rc_log_shared_queued_after_mbit = shared_before; + m->rc_log_flowlet_remaining_after_mbit = 0.0; + m->rc_log_sent_total_mbit = 0.0; + m->rc_log_active_before_entries = 0; + m->rc_log_active_after_entries = 0; + ns->dropped_mbit += m->mbit; + log_switch_arrival_event(ns, m); + return; + } + + port_desc* p = &ns->ports[port_id]; + const double port_queued_before = queued_mbit_on_port(ns, port_id); + const double shared_queued_before = queued_mbit_on_switch(ns); + double staged_remaining_after = 0.0; + int coalesced = 0; + int queue_index = -1; + + m->rc_no_route = 0; + m->rc_port_id = port_id; + m->rc_queue_index = -1; + m->rc_coalesced = 0; + m->rc_accepted_mbit = 0.0; + m->rc_dropped_mbit = 0.0; + m->rc_log_target_is_terminal = p->is_terminal; + m->rc_log_target_index = p->target_index; + m->rc_log_capacity_mbit = p->capacity_mbit_per_interval; + m->rc_log_port_queued_before_mbit = 0.0; + m->rc_log_port_queued_after_mbit = 0.0; + m->rc_log_shared_queued_before_mbit = 0.0; + m->rc_log_shared_queued_after_mbit = 0.0; + m->rc_log_flowlet_remaining_after_mbit = 0.0; + m->rc_log_sent_total_mbit = 0.0; + m->rc_log_active_before_entries = 0; + m->rc_log_active_after_entries = 0; + + observe_rate_flow(ns, port_id, m, m); + + const int prior_staged_index = find_staged_index_for_msg(ns, port_id, m); + m->rc_prev_final_segment_sent = + prior_staged_index >= 0 + ? (ns->staged_arrivals[port_id])[prior_staged_index].final_segment_sent + : 0; + + double staged_mbit = + stage_flowlet_arrival(ns, port_id, m, &coalesced, &queue_index, &staged_remaining_after); + + m->rc_queue_index = queue_index; + m->rc_coalesced = coalesced; + m->rc_accepted_mbit = staged_mbit; + m->rc_dropped_mbit = 0.0; + + m->rc_log_port_queued_before_mbit = port_queued_before; + m->rc_log_port_queued_after_mbit = port_queued_before; + m->rc_log_shared_queued_before_mbit = shared_queued_before; + m->rc_log_shared_queued_after_mbit = shared_queued_before; + m->rc_log_flowlet_remaining_after_mbit = staged_remaining_after; + m->rc_log_active_after_entries = ns->staged_arrivals[port_id].size(); + + log_switch_arrival_event(ns, m); + + if (staged_mbit > EPS) { + request_switch_egress(ns, SWITCH_EGRESS_LATE, m->interval_id, port_id, lp, m); + } + if (m->rc_rate_flow_created) { + request_switch_rate_eval(ns, m->interval_id, port_id, lp, m); + } +} + + +static void send_flowlet_fragment(switch_state* ns, int port_id, const queued_flowlet* q, + double send_mbit, int interval_id, tw_lp* lp) { + if (send_mbit <= EPS) { + return; + } + fluid_msg out_msg; + memset(&out_msg, 0, sizeof(out_msg)); + out_msg.event_type = FLOWLET_ARRIVAL; + out_msg.interval_id = interval_id + 1; + out_msg.source_terminal = q->source_terminal; + out_msg.destination_terminal = q->destination_terminal; + out_msg.source_switch = ns->switch_id; + out_msg.creation_interval = q->creation_interval; + out_msg.flowlet_id = q->flowlet_id; + out_msg.final_segment_sent = q->final_segment_sent; + out_msg.mbit = send_mbit; + + const port_desc* p = &ns->ports[port_id]; + if (p->is_terminal) { + tw_lpid term_gid = get_terminal_gid(p->target_index); + schedule_arrival(interval_id + 1, term_gid, &out_msg, send_mbit, lp); + ns->delivered_local_mbit += send_mbit; + } else { + tw_lpid sw_gid = get_switch_gid(p->target_index); + schedule_arrival(interval_id + 1, sw_gid, &out_msg, send_mbit, lp); + } + ns->sent_mbit += send_mbit; + ns->sent_fragments++; +} + +static void handle_switch_egress(switch_state* ns, fluid_msg* m, tw_lp* lp) { + m->rc_egress_request_created = 0; + m->rc_rate_eval_request_created = 0; + m->rc_paused_interval_counted = 0; + m->rc_accepted_mbit = 0.0; + m->rc_dropped_mbit = 0.0; + + int port_id = m->port_id; + if (port_id < 0 || port_id >= ns->num_ports) { + tw_error(TW_LOC, "invalid switch egress port %d", port_id); + } + + interval_flags* scheduled = scheduled_egress_flags(ns, m->event_type, port_id); + m->rc_egress_event_active = scheduled != NULL && m->interval_id >= 0 && + m->interval_id < MAX_SIMULATION_INTERVALS && + interval_flag_is_set(*scheduled, m->interval_id); + + /* A duplicate or canceled event is a forward and reverse no-op. */ + if (!m->rc_egress_event_active) { + return; + } + interval_flag_set(scheduled, m->interval_id, false); + + port_desc* p = &ns->ports[port_id]; + const int rate_active_before = active_rate_flow_count_on_port(ns, port_id); + fixed_vector& qv = ns->queues[port_id]; + fixed_vector& staged = ns->staged_arrivals[port_id]; + const bool service_staged = (m->event_type == SWITCH_EGRESS_LATE); + fixed_vector& source = service_staged ? staged : qv; + + m->rc_prev_capacity_accounting_interval = ns->capacity_accounting_interval[port_id]; + m->rc_prev_capacity_used_mbit = ns->capacity_used_mbit[port_id]; + if (ns->capacity_accounting_interval[port_id] != m->interval_id) { + ns->capacity_accounting_interval[port_id] = m->interval_id; + ns->capacity_used_mbit[port_id] = 0.0; + } + + const double capacity = p->capacity_mbit_per_interval; + double remaining_capacity = std::max(0.0, capacity - ns->capacity_used_mbit[port_id]); + const double queued_before = queued_mbit_on_port(ns, port_id); + const double shared_queued_before = queued_mbit_on_switch(ns); + + int active_before = 0; + for (const queued_flowlet& q : source) { + if (q.valid && q.remaining_mbit > EPS) { + ++active_before; + } + } + if (active_before > MAX_RC_ALLOCATIONS) { + tw_error(TW_LOC, + "switch %d port %d egress has %d active flowlets, exceeding " + "MAX_RC_ALLOCATIONS=%d", + ns->switch_id, port_id, active_before, MAX_RC_ALLOCATIONS); + } + + m->rc_alloc_count = 0; + m->rc_log_target_is_terminal = p->is_terminal; + m->rc_log_target_index = p->target_index; + m->rc_log_capacity_mbit = capacity; + m->rc_log_port_queued_before_mbit = queued_before; + m->rc_log_port_queued_after_mbit = queued_before; + m->rc_log_shared_queued_before_mbit = shared_queued_before; + m->rc_log_shared_queued_after_mbit = shared_queued_before; + m->rc_log_flowlet_remaining_after_mbit = 0.0; + m->rc_log_sent_total_mbit = 0.0; + m->rc_log_active_before_entries = active_before; + m->rc_log_active_after_entries = active_flowlet_count_on_port(ns, port_id); + m->rc_pause_target_port = -1; + + if (m->interval_id < ns->output_link_paused_until_interval[port_id]) { + if (active_before > 0) { + ns->paused_egress_intervals++; + m->rc_paused_interval_counted = 1; + } + remaining_capacity = 0.0; + } + + double send_plan[MAX_FLOW_ENTRIES_PER_PORT] = {0.0}; + while (remaining_capacity > EPS) { + int unsatisfied_count = 0; + for (int i = 0; i < (int)source.size(); ++i) { + if (source[i].valid && source[i].remaining_mbit - send_plan[i] > EPS) { + ++unsatisfied_count; + } + } + if (unsatisfied_count == 0) { + break; + } + + const double equal_share = remaining_capacity / unsatisfied_count; + double allocated_this_round = 0.0; + for (int i = 0; i < (int)source.size(); ++i) { + if (!source[i].valid || source[i].remaining_mbit - send_plan[i] <= EPS) { + continue; + } + const double available_for_flowlet = source[i].remaining_mbit - send_plan[i]; + const double send = std::min(equal_share, available_for_flowlet); + send_plan[i] += send; + allocated_this_round += send; + } + if (allocated_this_round <= EPS) { + break; + } + remaining_capacity -= allocated_this_round; + if (remaining_capacity < 0.0 && remaining_capacity > -EPS) { + remaining_capacity = 0.0; + } + } + + double sent_total = 0.0; + if (!service_staged) { + /* Early egress: previously buffered residuals have strict priority. */ + for (int i = 0; i < (int)qv.size(); ++i) { + if (!qv[i].valid || send_plan[i] <= EPS) { + continue; + } + + queued_flowlet before = qv[i]; + double send = std::min(send_plan[i], before.remaining_mbit); + rc_alloc_record* rc = &m->rc_allocs[m->rc_alloc_count++]; + memset(rc, 0, sizeof(*rc)); + rc->valid = 1; + rc->source_is_staged = 0; + rc->queue_index = i; + rc->before = before; + rc->send_mbit = send; + + send_flowlet_fragment(ns, port_id, &before, send, m->interval_id, lp); + qv[i].remaining_mbit -= send; + ns->ingress_links[before.ingress_id].queued_mbit -= send; + if (ns->ingress_links[before.ingress_id].queued_mbit < 0.0 && + ns->ingress_links[before.ingress_id].queued_mbit > -EPS) { + ns->ingress_links[before.ingress_id].queued_mbit = 0.0; + } + sent_total += send; + } + compact_port_queue(ns, port_id); + } else { + /* + * Late egress: use only capacity left by the early residual pass. + * Bytes sent here bypass the physical buffer. Only unsent residuals + * are admitted to the shared buffer, and overflow is dropped here. + */ + for (int i = 0; i < (int)staged.size(); ++i) { + if (!staged[i].valid || staged[i].remaining_mbit <= EPS) { + continue; + } + + queued_flowlet before = staged[i]; + double send = std::min(send_plan[i], before.remaining_mbit); + double residual = std::max(0.0, before.remaining_mbit - send); + + rc_alloc_record* rc = &m->rc_allocs[m->rc_alloc_count++]; + memset(rc, 0, sizeof(*rc)); + rc->valid = 1; + rc->source_is_staged = 1; + rc->queue_index = i; + rc->before = before; + rc->send_mbit = send; + rc->residual_queue_index = -1; + + if (send > EPS) { + send_flowlet_fragment(ns, port_id, &before, send, m->interval_id, lp); + sent_total += send; + } + + if (residual > EPS) { + fluid_msg residual_msg; + memset(&residual_msg, 0, sizeof(residual_msg)); + residual_msg.interval_id = m->interval_id; + residual_msg.source_terminal = before.source_terminal; + residual_msg.destination_terminal = before.destination_terminal; + residual_msg.creation_interval = before.creation_interval; + residual_msg.flowlet_id = before.flowlet_id; + residual_msg.final_segment_sent = before.final_segment_sent; + residual_msg.mbit = residual; + residual_msg.rc_ingress_id = before.ingress_id; + + const int prior_residual_index = find_queue_index_for_flowlet(ns, port_id, before); + rc->residual_prev_final_segment_sent = + prior_residual_index >= 0 + ? (ns->queues[port_id])[prior_residual_index].final_segment_sent + : 0; + + double ignored_port_before = 0.0; + double ignored_shared_before = 0.0; + double ignored_shared_after = 0.0; + double dropped = 0.0; + double ignored_remaining_after = 0.0; + int coalesced = 0; + int residual_queue_index = -1; + double buffered = + enqueue_flowlet(ns, port_id, &residual_msg, &ignored_port_before, + &ignored_shared_before, &ignored_shared_after, &dropped, + &ignored_remaining_after, &coalesced, &residual_queue_index); + + rc->buffered_mbit = buffered; + rc->dropped_mbit = dropped; + rc->residual_queue_index = residual_queue_index; + rc->residual_coalesced = coalesced; + m->rc_accepted_mbit += buffered; + m->rc_dropped_mbit += dropped; + ns->ingress_links[before.ingress_id].queued_mbit += buffered; + } + + staged[i].remaining_mbit = 0.0; + } + compact_staged_arrivals(ns, port_id); + } + + ns->capacity_used_mbit[port_id] += sent_total; + if (ns->capacity_used_mbit[port_id] > capacity + EPS) { + tw_error(TW_LOC, "switch %d port %d exceeded interval capacity: used %.12f capacity %.12f", + ns->switch_id, port_id, ns->capacity_used_mbit[port_id], capacity); + } + + const double queued_after = queued_mbit_on_port(ns, port_id); + const double shared_queued_after = queued_mbit_on_switch(ns); + const int active_after = active_flowlet_count_on_port(ns, port_id); + m->rc_log_port_queued_after_mbit = queued_after; + m->rc_log_shared_queued_after_mbit = shared_queued_after; + m->rc_log_sent_total_mbit = sent_total; + m->rc_log_active_after_entries = active_after; + + log_switch_egress_event(ns, m); + + if (queued_after > EPS) { + request_switch_egress(ns, SWITCH_EGRESS_EARLY, m->interval_id + 1, port_id, lp, m); + } + + const int rate_active_after = active_rate_flow_count_on_port(ns, port_id); + if (rate_active_after != rate_active_before) { + request_switch_rate_eval(ns, m->interval_id, port_id, lp, m); + } +} + +static void handle_switch_pause_update(switch_state* ns, fluid_msg* m) { + int port_id = find_switch_link_port(ns, m->pause_source_switch); + if (port_id < 0) { + tw_error(TW_LOC, + "switch %d received PAUSE update from switch %d but has no output link to it", + ns->switch_id, m->pause_source_switch); + } + + m->rc_pause_target_port = port_id; + m->rc_prev_pause_until_interval = ns->output_link_paused_until_interval[port_id]; + if (m->pause_asserted) { + ns->output_link_paused_until_interval[port_id] = + std::max(ns->output_link_paused_until_interval[port_id], + m->interval_id + cfg.pause_duration_intervals + 1); + } else { + ns->output_link_paused_until_interval[port_id] = m->interval_id; + } + ns->pause_updates_received++; + if (m->pause_asserted) { + ns->pause_frames_received++; + } else { + ns->resume_frames_received++; + } +} + +static void switch_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + switch (m->event_type) { + case FLOWLET_ARRIVAL: + handle_switch_arrival(ns, m, lp); + break; + case SWITCH_EGRESS_EARLY: + case SWITCH_EGRESS_LATE: + handle_switch_egress(ns, m, lp); + break; + case ETHERNET_PAUSE_FRAME_UPDATE: + handle_switch_pause_update(ns, m); + break; + case ETHERNET_PAUSE_EVAL: + handle_ethernet_pause_eval(ns, m, lp); + break; + case SWITCH_RATE_FEEDBACK: + handle_switch_rate_feedback(ns, m, lp); + break; + case SWITCH_RATE_EVAL: + handle_switch_rate_eval(ns, m, lp); + break; + default: + tw_error(TW_LOC, "switch received unknown event type %d", m->event_type); + } +} + +static void rollback_switch_arrival(switch_state* ns, fluid_msg* m) { + ns->received_fragments--; + + undo_requested_switch_egress(ns, m); + undo_requested_switch_rate_eval(ns, m); + + if (m->rc_no_route) { + ns->dropped_mbit -= m->rc_dropped_mbit; + return; + } + + int port_id = m->rc_port_id; + + if (port_id < 0 || port_id >= ns->num_ports) { + tw_error(TW_LOC, "invalid rollback arrival port %d on switch %d", port_id, ns->switch_id); + } + + if (m->rc_accepted_mbit <= EPS) { + return; + } + + fixed_vector& staged = ns->staged_arrivals[port_id]; + + int idx = m->rc_queue_index; + if (idx < 0 || idx >= (int)staged.size() || staged[idx].flowlet_id != m->flowlet_id) { + idx = find_staged_index_for_msg(ns, port_id, m); + } + + if (idx < 0) { + tw_error(TW_LOC, "could not find flowlet %llu on switch %d port %d during arrival rollback", + (unsigned long long)m->flowlet_id, ns->switch_id, port_id); + } + + if (m->rc_coalesced) { + staged[idx].remaining_mbit -= m->rc_accepted_mbit; + staged[idx].final_segment_sent = m->rc_prev_final_segment_sent; + + if (staged[idx].remaining_mbit <= EPS) { + tw_error( + TW_LOC, + "coalesced flowlet %llu became empty during arrival rollback on switch %d port %d", + (unsigned long long)m->flowlet_id, ns->switch_id, port_id); + } + } else { + staged.erase(staged.begin() + idx); + } + + fixed_vector& rate_flows = ns->rate_flows[port_id]; + if (m->rc_rate_flow_created) { + const int rate_idx = m->rc_rate_flow_index; + if (rate_idx < 0 || rate_idx >= rate_flows.size() || + rate_flows[rate_idx].flow_id != m->flowlet_id) { + tw_error(TW_LOC, "invalid created rate-flow rollback on switch %d", ns->switch_id); + } + if (m->rc_rate_flow_appended) { + if (rate_idx != rate_flows.size() - 1) { + tw_error(TW_LOC, "rate-flow rollback order mismatch on switch %d", ns->switch_id); + } + rate_flows.pop_back(); + } else { + rate_flows[rate_idx] = m->rc_rate_flow_before; + } + } else if (m->rc_rate_flow_index >= 0) { + rate_flows[m->rc_rate_flow_index] = m->rc_rate_flow_before; + } +} + +static void rollback_switch_egress(switch_state* ns, fluid_msg* m) { + if (!m->rc_egress_event_active) { + return; + } + + int port_id = m->port_id; + if (port_id < 0 || port_id >= ns->num_ports) { + tw_error(TW_LOC, "invalid rollback egress port %d on switch %d", port_id, ns->switch_id); + } + + undo_requested_switch_egress(ns, m); + undo_requested_switch_rate_eval(ns, m); + + interval_flags* scheduled = scheduled_egress_flags(ns, m->event_type, port_id); + if (scheduled == NULL || m->interval_id < 0 || m->interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "invalid rollback egress interval %d on switch %d port %d", m->interval_id, + ns->switch_id, port_id); + } + interval_flag_set(scheduled, m->interval_id, true); + ns->capacity_accounting_interval[port_id] = m->rc_prev_capacity_accounting_interval; + ns->capacity_used_mbit[port_id] = m->rc_prev_capacity_used_mbit; + + if (m->rc_paused_interval_counted) { + ns->paused_egress_intervals--; + } + + fixed_vector& qv = ns->queues[port_id]; + fixed_vector& staged = ns->staged_arrivals[port_id]; + const port_desc* p = &ns->ports[port_id]; + + /* + * Undo late-egress residual admissions in reverse insertion order. This + * restores the physical queue to its post-early-egress state. + */ + for (int r = m->rc_alloc_count - 1; r >= 0; --r) { + rc_alloc_record* rc = &m->rc_allocs[r]; + if (!rc->valid || !rc->source_is_staged) { + continue; + } + + if (rc->buffered_mbit > EPS) { + int idx = rc->residual_queue_index; + if (idx < 0 || idx >= (int)qv.size() || qv[idx].flowlet_id != rc->before.flowlet_id) { + idx = find_queue_index_for_flowlet(ns, port_id, rc->before); + } + if (idx < 0) { + tw_error(TW_LOC, + "could not find residual flowlet %llu on switch %d port %d " + "during egress rollback", + (unsigned long long)rc->before.flowlet_id, ns->switch_id, port_id); + } + + if (rc->residual_coalesced) { + qv[idx].remaining_mbit -= rc->buffered_mbit; + qv[idx].final_segment_sent = rc->residual_prev_final_segment_sent; + if (qv[idx].remaining_mbit <= EPS) { + tw_error(TW_LOC, + "coalesced residual flowlet %llu became empty during " + "egress rollback on switch %d port %d", + (unsigned long long)rc->before.flowlet_id, ns->switch_id, port_id); + } + } else { + qv.erase(qv.begin() + idx); + } + + ns->buffered_residual_mbit -= rc->buffered_mbit; + ns->ingress_links[rc->before.ingress_id].queued_mbit -= rc->buffered_mbit; + if (ns->ingress_links[rc->before.ingress_id].queued_mbit < 0.0 && + ns->ingress_links[rc->before.ingress_id].queued_mbit > -EPS) { + ns->ingress_links[rc->before.ingress_id].queued_mbit = 0.0; + } + } + ns->dropped_mbit -= rc->dropped_mbit; + + if (rc->send_mbit > EPS) { + ns->sent_mbit -= rc->send_mbit; + ns->sent_fragments--; + if (p->is_terminal) { + ns->delivered_local_mbit -= rc->send_mbit; + } + } + } + + /* + * Restore early-egress queue entries in ascending original-index order. + * Ascending insertion preserves the exact pre-event vector order when + * several fully drained entries were compacted away. + */ + for (int r = 0; r < m->rc_alloc_count; ++r) { + rc_alloc_record* rc = &m->rc_allocs[r]; + if (!rc->valid || rc->source_is_staged || rc->send_mbit <= EPS) { + continue; + } + + int idx = rc->queue_index; + if (idx < 0 || idx >= (int)qv.size() || qv[idx].flowlet_id != rc->before.flowlet_id) { + idx = find_queue_index_for_flowlet(ns, port_id, rc->before); + } + if (idx >= 0) { + qv[idx] = rc->before; + } else { + int insert_idx = std::max(0, std::min(rc->queue_index, (int)qv.size())); + qv.insert(qv.begin() + insert_idx, rc->before); + } + + ns->ingress_links[rc->before.ingress_id].queued_mbit += rc->send_mbit; + ns->sent_mbit -= rc->send_mbit; + ns->sent_fragments--; + if (p->is_terminal) { + ns->delivered_local_mbit -= rc->send_mbit; + } + } + + /* Late egress removes every staged entry; rebuild the original order. */ + for (int r = 0; r < m->rc_alloc_count; ++r) { + rc_alloc_record* rc = &m->rc_allocs[r]; + if (!rc->valid || !rc->source_is_staged) { + continue; + } + int insert_idx = std::max(0, std::min(rc->queue_index, (int)staged.size())); + staged.insert(staged.begin() + insert_idx, rc->before); + } +} + +static void switch_rev_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + (void)lp; + + switch (m->event_type) { + case FLOWLET_ARRIVAL: + rollback_switch_arrival(ns, m); + break; + + case SWITCH_EGRESS_EARLY: + case SWITCH_EGRESS_LATE: + rollback_switch_egress(ns, m); + break; + + case ETHERNET_PAUSE_EVAL: + for (int i = m->rc_pause_change_count - 1; i >= 0; --i) { + int ingress_id = m->rc_pause_ingress_id[i]; + ingress_desc& ingress = ns->ingress_links[ingress_id]; + ingress.pause_asserted = m->rc_pause_prev_asserted[i]; + ingress.last_pause_frame_interval = m->rc_pause_prev_last_interval[i]; + ns->pause_updates_sent--; + if (m->rc_pause_sent_asserted[i]) { + ns->pause_frames_sent--; + } else { + ns->resume_frames_sent--; + } + } + break; + + case ETHERNET_PAUSE_FRAME_UPDATE: + ns->output_link_paused_until_interval[m->rc_pause_target_port] = + m->rc_prev_pause_until_interval; + ns->pause_updates_received--; + if (m->pause_asserted) { + ns->pause_frames_received--; + } else { + ns->resume_frames_received--; + } + break; + + case SWITCH_RATE_FEEDBACK: + if (m->rc_rate_update_applied) { + if (m->rc_port_id < 0 || m->rc_port_id >= ns->num_ports || m->rc_rate_flow_index < 0 || + m->rc_rate_flow_index >= (int)ns->rate_flows[m->rc_port_id].size()) { + tw_error(TW_LOC, "invalid switch rate-feedback rollback state"); + } + ns->rate_flows[m->rc_port_id][m->rc_rate_flow_index] = m->rc_rate_flow_before; + } + break; + + case SWITCH_RATE_EVAL: + if (m->rc_rate_eval_event_active) { + if (m->port_id < 0 || m->port_id >= ns->num_ports || m->interval_id < 0 || + m->interval_id >= MAX_SIMULATION_INTERVALS) { + tw_error(TW_LOC, "invalid rate-eval rollback state"); + } + interval_flag_set(&ns->scheduled_rate_eval[m->port_id], m->interval_id, true); + } + break; + + default: + tw_error(TW_LOC, "switch reverse received unknown event type %d", m->event_type); + } +} + +static void switch_commit_event(switch_state* ns, tw_bf* b, fluid_msg* m, tw_lp* lp) { + (void)b; + (void)lp; + + if (!fluid_csv_commit_logs_enabled()) { + return; + } + + fluid_commit_logging_begin(); + + switch (m->event_type) { + case FLOWLET_ARRIVAL: + log_switch_arrival_event(ns, m); + break; + + case SWITCH_EGRESS_EARLY: + case SWITCH_EGRESS_LATE: + log_switch_egress_event(ns, m); + break; + + default: + break; + } + + fluid_commit_logging_end(); +} + +static void switch_finalize(switch_state* ns, tw_lp* lp) { + double queued = 0.0; + int any_pause_asserted = 0; + for (int i = 0; i < ns->num_ingress_links; ++i) { + any_pause_asserted |= ns->ingress_links[i].pause_asserted; + } + for (int p = 0; p < ns->num_ports; ++p) { + queued += queued_mbit_on_port(ns, p); + } + const char* unit = output_data_field_suffix(); + printf("fluid-flow-wan gid=%llu switch=%d name=%s ports=%d shared_buffer_%s=%.6f " + "received_fragments=%d sent_fragments=%d buffered_residual_%s=%.6f " + "sent_%s=%.6f local_delivery_%s=%.6f dropped_%s=%.6f ready_queue_%s=%.6f " + "shared_buffer_occupied_%s=%.6f pause_asserted=%d pause_updates_sent=%llu " + "pause_frames_sent=%llu resume_frames_sent=%llu pause_updates_received=%llu " + "pause_frames_received=%llu resume_frames_received=%llu paused_egress_intervals=%llu\n", + (unsigned long long)lp->gid, ns->switch_id, switches[ns->switch_id].name.c_str(), + ns->num_ports, unit, to_output_data_unit(ns->shared_buffer_mbit), ns->received_fragments, + ns->sent_fragments, unit, to_output_data_unit(ns->buffered_residual_mbit), unit, + to_output_data_unit(ns->sent_mbit), unit, to_output_data_unit(ns->delivered_local_mbit), + unit, to_output_data_unit(ns->dropped_mbit), unit, to_output_data_unit(queued), unit, + to_output_data_unit(queued), any_pause_asserted, ns->pause_updates_sent, + ns->pause_frames_sent, ns->resume_frames_sent, ns->pause_updates_received, + ns->pause_frames_received, ns->resume_frames_received, ns->paused_egress_intervals); +} + +const tw_optdef app_opt[] = {TWOPT_GROUP("interval-fluid switch/terminal workload"), TWOPT_END()}; + +static bool has_suffix(const char* value, const char* suffix) { + const size_t value_len = strlen(value); + const size_t suffix_len = strlen(suffix); + return value_len >= suffix_len && strcmp(value + value_len - suffix_len, suffix) == 0; +} + +static const char* find_config_arg(int argc, char** argv) { + for (int i = argc - 1; i >= 1; --i) { + if (argv[i] == NULL) { + continue; + } + + const char* arg = argv[i]; + if (has_suffix(arg, ".conf") || has_suffix(arg, ".yaml") || has_suffix(arg, ".yml") || + has_suffix(arg, ".json")) { + return arg; + } + } + return NULL; +} + +static void write_log_header_file(const char* path, const char* header) { + if (path[0] == '\0') { + return; + } + + std::remove(path); + + std::ofstream out(path, std::ios::out | std::ios::trunc); + if (!out) { + tw_error(TW_LOC, "could not open CSV log file '%s' for header", path); + } + out << header; +} + +static void append_merged_rows_to_file(const char* path, const char* data, int len) { + if (path[0] == '\0' || len <= 0) { + return; + } + + std::ofstream out(path, std::ios::app); + if (!out) { + tw_error(TW_LOC, "could not open CSV log file '%s' for merged append", path); + } + out.write(data, len); +} + +static void merge_log_buffer(const char* path, const std::ostringstream& buffer, MPI_Comm comm) { + if (path[0] == '\0') { + return; + } + + int rank = 0; + int size = 1; + MPI_Comm_rank(comm, &rank); + MPI_Comm_size(comm, &size); + + std::string local_rows = buffer.str(); + int local_len = (int)local_rows.size(); + + std::vector recv_counts; + std::vector displs; + if (rank == 0) { + recv_counts.resize(size, 0); + displs.resize(size, 0); + } + + MPI_Gather(&local_len, 1, MPI_INT, rank == 0 ? recv_counts.data() : NULL, 1, MPI_INT, 0, comm); + + int total_len = 0; + if (rank == 0) { + for (int i = 0; i < size; ++i) { + displs[i] = total_len; + total_len += recv_counts[i]; + } + } + + std::vector merged; + if (rank == 0 && total_len > 0) { + merged.resize(total_len); + } + + MPI_Gatherv(local_len > 0 ? local_rows.data() : NULL, local_len, MPI_CHAR, + rank == 0 && total_len > 0 ? merged.data() : NULL, + rank == 0 ? recv_counts.data() : NULL, rank == 0 ? displs.data() : NULL, MPI_CHAR, + 0, comm); + + if (rank == 0 && total_len > 0) { + append_merged_rows_to_file(path, merged.data(), total_len); + } +} + +static void merge_all_log_buffers(MPI_Comm comm) { + merge_log_buffer(cfg.terminal_log_path, terminal_log_buffer, comm); + merge_log_buffer(cfg.switch_log_path, switch_log_buffer, comm); + merge_log_buffer(cfg.flowlet_log_path, flowlet_log_buffer, comm); + merge_log_buffer(cfg.switch_training_log_path, switch_training_log_buffer, comm); +} + +static void write_log_headers(int rank) { + if (rank != 0) { + return; + } + + const char* unit = output_data_field_suffix(); + + std::ostringstream terminal_header; + terminal_header << "interval,event,terminal,terminal_name,attached_switch,peer_terminal," + << unit << '\n'; + write_log_header_file(cfg.terminal_log_path, terminal_header.str().c_str()); + + std::ostringstream switch_header; + switch_header << "interval,event,switch,switch_name,port,target_type,target_index," + << "capacity_" << unit << ",queued_before_" << unit << ",sent_" << unit + << ",queued_after_" << unit << ",shared_queued_before_" << unit + << ",shared_queued_after_" << unit << ",shared_buffer_" << unit << ",dropped_" + << unit << ",active_queue_entries\n"; + write_log_header_file(cfg.switch_log_path, switch_header.str().c_str()); + + std::ostringstream flowlet_header; + flowlet_header << "interval,event,switch,switch_name,port,target_type,target_index," + << "flowlet_id,source_terminal,destination_terminal,creation_interval," + << "age_intervals,capacity_" << unit << ",queued_before_" << unit << ",send_" + << unit << ",remaining_after_" << unit << ",dropped_" << unit << '\n'; + write_log_header_file(cfg.flowlet_log_path, flowlet_header.str().c_str()); + + std::ostringstream training_header; + training_header << "interval,switch,switch_name,port,target_type,target_index," + << "capacity_" << unit << ",queued_before_" << unit << ",sent_" << unit + << ",queued_after_" << unit << ",dropped_" << unit + << ",active_before,active_after,allocations\n"; + write_log_header_file(cfg.switch_training_log_path, training_header.str().c_str()); +} + +int main(int argc, char** argv) { + int rank = 0; + + g_tw_ts_end = seconds_to_ns(60.0 * 60.0 * 24.0 * 365.0); + + tw_opt_add(app_opt); + tw_init(&argc, &argv); + + const char* config_file = find_config_arg(argc, argv); + if (config_file == NULL) { + printf("Usage: mpirun -np %s --sync=1 -- \n", argv[0]); + MPI_Finalize(); + return 1; + } + + MPI_Comm_rank(MPI_COMM_CODES, &rank); + configuration_load(config_file, MPI_COMM_CODES, &config); + load_config(); + validate_ross_message_size_or_abort(rank); + + add_lp_types(); + codes_mapping_setup(); + + total_switch_lps = codes_mapping_get_lp_count(GROUP_NAME, 0, SWITCH_LP_NAME, NULL, 1); + total_terminal_lps = codes_mapping_get_lp_count(GROUP_NAME, 0, TERMINAL_LP_NAME, NULL, 1); + if (total_switch_lps != (int)switches.size()) { + tw_error(TW_LOC, "config defines %d switch LPs but topology YAML defines %zu switches", + total_switch_lps, switches.size()); + } + if (total_terminal_lps != (int)terminals.size()) { + tw_error(TW_LOC, "config defines %d terminal LPs but topology YAML defines %zu terminals", + total_terminal_lps, terminals.size()); + } + + write_log_headers(rank); + MPI_Barrier(MPI_COMM_CODES); + + if (rank == 0) { + printf("fluid-flow-wan config: switches=%zu terminals=%zu interval_seconds=%.6f " + "num_send_intervals=%d num_drain_intervals=%d " + "flow_generation_every_n_intervals=%d random_flow_min_%s=%.6f " + "random_flow_max_%s=%.6f output_data_unit=%s buffer_mode=shared csv_logs=%s " + "ross_message_size=%d fluid_msg_size=%zu\n", + switches.size(), terminals.size(), cfg.interval_seconds, cfg.num_send_intervals, + cfg.num_drain_intervals, cfg.flow_generation_every_n_intervals, + output_data_field_suffix(), to_output_data_unit(cfg.random_flow_min_mbit), + output_data_field_suffix(), to_output_data_unit(cfg.random_flow_max_mbit), + output_data_unit_symbol(), + fluid_csv_forward_logs_enabled() ? "buffered-forward" : "buffered-commit", + get_configured_message_size_bytes(), sizeof(fluid_msg)); + } + + tw_run(); + merge_all_log_buffers(MPI_COMM_CODES); + tw_end(); + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e7eb2b2d..f213abd0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,7 +62,7 @@ add_test(NAME mpi-multirank-sanity # REPEAT for "same command N times", VARIANTS when only a small fragment differs # (e.g. VARIANTS "--sync=1" "--sync=3" for a seq-vs-optimistic check). function(codes_add_equivalence_test) - cmake_parse_arguments(T "" "NAME;BINARY;NP;CONFIG;MARKER;REPEAT;SETUP;REQUIRE" "ARGS;VARIANTS" ${ARGN}) + cmake_parse_arguments(T "" "NAME;BINARY;NP;CONFIG;MARKER;REPEAT;SETUP" "ARGS;VARIANTS;REQUIRE" ${ARGN}) if(NOT T_NAME OR NOT T_BINARY OR NOT T_CONFIG) message(FATAL_ERROR "codes_add_equivalence_test: NAME, BINARY and CONFIG are required") @@ -107,9 +107,11 @@ function(codes_add_equivalence_test) set(_setup --setup "${CMAKE_CURRENT_SOURCE_DIR}/${T_SETUP}") endif() set(_require "") - if(T_REQUIRE) - set(_require --require "${T_REQUIRE}") - endif() + foreach(_req IN LISTS T_REQUIRE) + if(NOT _req STREQUAL "") + list(APPEND _require --require "${_req}") + endif() + endforeach() add_test(NAME ${T_NAME} COMMAND "${CMAKE_CURRENT_BINARY_DIR}/run-test.sh" @@ -141,7 +143,7 @@ endfunction() # [MARKER ] # ) function(codes_add_run_test) - cmake_parse_arguments(T "" "NAME;BINARY;NP;SETUP;MARKER" "ARGS" ${ARGN}) + cmake_parse_arguments(T "" "NAME;BINARY;NP;SETUP;MARKER" "ARGS;REQUIRE" ${ARGN}) if(NOT T_NAME OR NOT T_BINARY) message(FATAL_ERROR "codes_add_run_test: NAME and BINARY are required") endif() @@ -151,12 +153,17 @@ function(codes_add_run_test) set(_run ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${T_NP} ${MPIEXEC_PREFLAGS} "$" ${T_ARGS} ${MPIEXEC_POSTFLAGS}) - if(T_SETUP OR T_MARKER) - # SETUP must be sourced and MARKER checked -> go through the staged runner. + if(T_SETUP OR T_MARKER OR T_REQUIRE) + # SETUP must be sourced and MARKER/REQUIRE checked -> go through the staged runner. set(_opts "") if(T_MARKER) list(APPEND _opts --marker "${T_MARKER}") endif() + foreach(_req IN LISTS T_REQUIRE) + if(NOT _req STREQUAL "") + list(APPEND _opts --require "${_req}") + endif() + endforeach() if(T_SETUP) list(APPEND _opts --setup "${CMAKE_CURRENT_SOURCE_DIR}/${T_SETUP}") endif() @@ -400,15 +407,53 @@ if(USE_UNION) set_tests_properties(${union-shell-files} PROPERTIES LABELS "nightly" TIMEOUT 1200) endif() + +# Fluid-flow WAN smoke tests. The model has one deterministic max-min service +# rule; no FIFO or round-robin scheduler variants are configured. +foreach(_ffw_mode sequential optimistic) + if(_ffw_mode STREQUAL "sequential") + set(_ffw_synch 1) + set(_ffw_np 1) + else() + set(_ffw_synch 3) + set(_ffw_np 2) + endif() + + set(_ffw_test "fluid-flow-wan-${_ffw_mode}") + add_test(NAME ${_ffw_test} + COMMAND "${CMAKE_CURRENT_BINARY_DIR}/run-test.sh" + "${CMAKE_CURRENT_SOURCE_DIR}/fluid-flow-wan-ci.sh" + "${_ffw_synch}" + "${_ffw_np}" + "${_ffw_test}" + "${MPIEXEC_EXECUTABLE}" + "${MPIEXEC_NUMPROC_FLAG}" + WORKING_DIRECTORY "${CODES_BINARY_DIR}") + set_tests_properties(${_ffw_test} PROPERTIES TIMEOUT 120) +endforeach() + +# Cross-mode equivalence: the optimistic run must exercise rollback and still +# commit exactly the same switch, terminal, and event-count state as sequential. +add_test(NAME fluid-flow-wan-seq-opt-equivalence + COMMAND "${CMAKE_CURRENT_BINARY_DIR}/run-test.sh" + "${CMAKE_CURRENT_SOURCE_DIR}/fluid-flow-wan-equivalence.sh" + "fluid-flow-wan-seq-opt-equivalence" + "${MPIEXEC_EXECUTABLE}" + "${MPIEXEC_NUMPROC_FLAG}" + WORKING_DIRECTORY "${CODES_BINARY_DIR}") +set_tests_properties(fluid-flow-wan-seq-opt-equivalence + PROPERTIES TIMEOUT 240 PROCESSORS 2) + # Equivalence / determinism tests (replacing per-scenario shell scripts). # example-ping-pong-determinism.sh: run the optimistic sim twice, compare # committed-event counts (reproducibility). codes_add_equivalence_test( NAME example-ping-pong-determinism BINARY tutorial-synthetic-ping-pong - NP 3 - ARGS --sync=3 --num_messages=10 --payload_sz=8192 - CONFIG doc/example/tutorial-ping-pong.conf) + NP 3 + ARGS --sync=3 --num_messages=10 --payload_sz=8192 + CONFIG doc/example/tutorial-ping-pong.conf + REQUIRE "START PARALLEL OPTIMISTIC SIMULATION") # Surrogate-mode determinism: same surrogate config run twice (np 3, sync=3), # committed-event counts must match. The two variants differ only in network @@ -420,7 +465,7 @@ codes_add_equivalence_test( ARGS --sync=3 --num_messages=100 --payload_sz=8192 CONFIG tutorial-ping-pong-surrogate.conf SETUP surrogate-determinism-no-freeze-setup.sh - REQUIRE "Network switch completed") + REQUIRE "Network switch completed" "START PARALLEL OPTIMISTIC SIMULATION") codes_add_equivalence_test( NAME example-ping-pong-surrogate-determinism-freeze BINARY tutorial-synthetic-ping-pong @@ -428,7 +473,7 @@ codes_add_equivalence_test( ARGS --sync=3 --num_messages=100 --payload_sz=8192 CONFIG tutorial-ping-pong-surrogate.conf SETUP surrogate-determinism-freeze-setup.sh - REQUIRE "Network switch completed") + REQUIRE "Network switch completed" "START PARALLEL OPTIMISTIC SIMULATION") # Config-equivalence for the tutorial ping-pong: the legacy .conf against its YAML # twin. tutorial-synthetic-ping-pong writes its per-LP results to stdout and emits @@ -482,7 +527,8 @@ codes_add_run_test(NAME rc-stack-test BINARY rc-stack-test) codes_add_run_test(NAME modelnet-prio-sched-test-seq BINARY modelnet-prio-sched-test NP 1 ARGS --sync=1 -- "${_tconf}/modelnet-prio-sched-test.conf") codes_add_run_test(NAME modelnet-prio-sched-test-opt BINARY modelnet-prio-sched-test - NP 2 ARGS --sync=3 -- "${_tconf}/modelnet-prio-sched-test.conf") + NP 2 ARGS --sync=3 -- "${_tconf}/modelnet-prio-sched-test.conf" + REQUIRE "START PARALLEL OPTIMISTIC SIMULATION") codes_add_run_test(NAME jobmap-test BINARY jobmap-test ARGS "${_tconf}/jobmap-test-list.conf") codes_add_run_test(NAME map-ctx-test BINARY map-ctx-test ARGS "${_tconf}/map-ctx-test.conf") codes_add_run_test(NAME resource-test BINARY resource-test ARGS --sync=1 "--codes-config=${_tconf}/buffer_test.conf") diff --git a/tests/fluid-flow-wan-ci.sh b/tests/fluid-flow-wan-ci.sh new file mode 100755 index 00000000..88bd0c16 --- /dev/null +++ b/tests/fluid-flow-wan-ci.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -euo pipefail + +synch="${1:?synch mode required: 1 or 3}" +np="${2:?MPI rank count required}" +case_name="${3:-fluid-flow-wan-synch${synch}}" +mpi_exec="${4:-mpirun}" +mpi_np_flag="${5:--np}" + +if [[ -z "${bindir:-}" || -z "${srcdir:-}" ]]; then + echo "bindir/srcdir are not set; run through tests/run-test.sh" + exit 1 +fi + +binary="$bindir/src/model-net-fluid-flow-wan" +base_yaml="$bindir/doc/example/fluid-flow-wan.yaml" +topology="$bindir/doc/example/fluid-flow-wan-topology.yaml" +[[ -f "$topology" ]] || topology="$srcdir/doc/example/fluid-flow-wan-topology.yaml" + +[[ -x "$binary" ]] || { echo "missing executable: $binary"; exit 1; } +[[ -f "$base_yaml" ]] || { echo "missing generated config: $base_yaml"; exit 1; } +[[ -f "$topology" ]] || { echo "missing topology file"; exit 1; } + +rm -rf "$case_name" +mkdir -p "$case_name/logs" +cp "$topology" "$case_name/fluid-flow-wan-topology.yaml" + +cp "$base_yaml" "$case_name/fluid-flow-wan.yaml" + +if ! ( + cd "$case_name" + "$mpi_exec" "$mpi_np_flag" "$np" "$binary" --sync="$synch" -- fluid-flow-wan.yaml \ + > model-output.txt 2> model-output-error.txt +); then + echo "fluid-flow-wan model run failed" + cat "$case_name/model-output.txt" || true + cat "$case_name/model-output-error.txt" || true + exit 1 +fi + +out="$case_name/model-output.txt" +grep "fluid-flow-wan config:" "$out" +grep "Net Events Processed" "$out" +grep -Eq "source_backlog_(mbit|gbit)=" "$out" +grep -Eq "rate_updates_received=[1-9][0-9]*" "$out" +grep -Eq "rate_cache_entries=[1-9][0-9]*" "$out" +grep -q ',generate_flow,' "$case_name/logs/terminal-events.csv" +grep -q ',send,' "$case_name/logs/terminal-events.csv" + +if [[ "$synch" == "1" ]]; then + grep "csv_logs=buffered-forward" "$out" +else + grep "csv_logs=buffered-commit" "$out" +fi + +for csv in terminal-events.csv switch-events.csv flowlet-events.csv switch-training.csv; do + [[ -s "$case_name/logs/$csv" ]] || { echo "missing or empty CSV log: $csv"; exit 1; } +done diff --git a/tests/fluid-flow-wan-equivalence.sh b/tests/fluid-flow-wan-equivalence.sh new file mode 100755 index 00000000..065a73eb --- /dev/null +++ b/tests/fluid-flow-wan-equivalence.sh @@ -0,0 +1,119 @@ +#!/bin/bash +set -euo pipefail + +case_root="${1:-fluid-flow-wan-seq-opt-equivalence}" +mpi_exec="${2:-mpirun}" +mpi_np_flag="${3:--np}" + +if [[ -z "${bindir:-}" || -z "${srcdir:-}" ]]; then + echo "bindir/srcdir are not set; run through tests/run-test.sh" + exit 1 +fi + +smoke_test="$srcdir/tests/fluid-flow-wan-ci.sh" +[[ -x "$smoke_test" ]] || { echo "missing executable test script: $smoke_test"; exit 1; } + +seq_case="${case_root}-sequential" +opt_case="${case_root}-optimistic" +comparison_dir="${case_root}-comparison" + +rm -rf "$seq_case" "$opt_case" "$comparison_dir" +mkdir -p "$comparison_dir" + +"$smoke_test" 1 1 "$seq_case" "$mpi_exec" "$mpi_np_flag" +"$smoke_test" 3 2 "$opt_case" "$mpi_exec" "$mpi_np_flag" + +seq_output="$seq_case/model-output.txt" +opt_output="$opt_case/model-output.txt" + +extract_net_events() { + local output="$1" + local count + local value + + count="$(awk '$1 == "Net" && $2 == "Events" && $3 == "Processed" {count++} END {print count + 0}' "$output")" + value="$(awk '$1 == "Net" && $2 == "Events" && $3 == "Processed" {print $NF}' "$output")" + + if [[ "$count" != "1" || ! "$value" =~ ^[0-9]+$ ]]; then + echo "expected exactly one numeric Net Events Processed value in $output" >&2 + return 1 + fi + + printf '%s\n' "$value" +} + +canonicalize_csv() { + local input="$1" + local output="$2" + local header + local row_count + + [[ -s "$input" ]] || { + echo "missing or empty committed CSV log: $input" + return 1 + } + + IFS= read -r header < "$input" || { + echo "could not read CSV header from $input" + return 1 + } + header="${header%$'\r'}" + + { + printf '%s\n' "$header" + tail -n +2 "$input" | tr -d '\r' | LC_ALL=C sort + } > "$output" + + row_count="$(wc -l < "$output")" + if (( row_count < 2 )); then + echo "committed CSV log has no data rows: $input" + return 1 + fi +} + +csv_logs=( + terminal-events.csv + switch-events.csv + flowlet-events.csv + switch-training.csv +) + +for csv in "${csv_logs[@]}"; do + seq_csv="$seq_case/logs/$csv" + opt_csv="$opt_case/logs/$csv" + seq_canonical="$comparison_dir/sequential-$csv" + opt_canonical="$comparison_dir/optimistic-$csv" + + canonicalize_csv "$seq_csv" "$seq_canonical" + canonicalize_csv "$opt_csv" "$opt_canonical" + + if ! diff -u "$seq_canonical" "$opt_canonical"; then + echo "sequential and optimistic committed CSV logs differ: $csv" + exit 1 + fi +done + +seq_net_events="$(extract_net_events "$seq_output")" +opt_net_events="$(extract_net_events "$opt_output")" +if [[ "$seq_net_events" != "$opt_net_events" ]]; then + echo "Net Events Processed differs: sequential=$seq_net_events optimistic=$opt_net_events" + exit 1 +fi + +rolled_back="$(awk '$1 == "Events" && $2 == "Rolled" && $3 == "Back" {print $NF}' "$opt_output" | tail -n 1)" +rollbacks="$(awk '$1 == "Total" && $2 == "Roll" && $3 == "Backs" {print $NF}' "$opt_output" | tail -n 1)" + +if [[ ! "$rolled_back" =~ ^[0-9]+$ ]] || (( rolled_back == 0 )); then + echo "optimistic run did not exercise event rollback: Events Rolled Back=${rolled_back:-missing}" + exit 1 +fi + +if [[ ! "$rollbacks" =~ ^[0-9]+$ ]] || (( rollbacks == 0 )); then + echo "optimistic run did not exercise rollback handling: Total Roll Backs=${rollbacks:-missing}" + exit 1 +fi + +echo "fluid-flow-wan sequential/optimistic committed CSV logs match" +echo "Net Events Processed=$seq_net_events" +echo "Events Rolled Back=$rolled_back" +echo "Total Roll Backs=$rollbacks"