Aggie Student Toolkit — Load Balancer CSCE 412 Project 4
Arnav Fotedar · Fall 2026 · C++ load balancer simulation
Loading...
Searching...
No Matches
Simulation.cpp
Go to the documentation of this file.
1
6
7#include "Simulation.h"
8
9#include <algorithm>
10#include <chrono>
11#include <cmath>
12#include <ctime>
13#include <iomanip>
14#include <ostream>
15#include <sstream>
16#include <string>
17
18namespace {
19
20const char* const kRule = "==============================================================================";
21const char* const kThinRule = "------------------------------------------------------------------------------";
22
23std::string fixed(double value, int precision) {
24 std::ostringstream text;
25 text << std::fixed << std::setprecision(precision) << value;
26 return text.str();
27}
28
29void heading(std::ostream& out, const std::string& title) {
30 out << '\n' << kThinRule << '\n' << title << '\n' << kThinRule << '\n';
31}
32
33void row(std::ostream& out, const std::string& label, const std::string& value) {
34 out << " " << std::left << std::setw(46) << label << value << '\n';
35}
36
37const char* kindName(ScaleEvent::Kind kind) {
38 switch (kind) {
39 case ScaleEvent::Kind::Added: return "+ added";
40 case ScaleEvent::Kind::Reactivated: return "+ reactivated";
41 case ScaleEvent::Kind::Removed: return "- removed idle";
42 case ScaleEvent::Kind::Retiring: return "- retiring busy";
43 case ScaleEvent::Kind::Retired: return " retired (done)";
44 }
45 return "";
46}
47
48Firewall buildFirewall(const Config& config) {
49 Firewall firewall;
50 for (const std::string& range : config.blockedRanges) {
51 firewall.addRange(range);
52 }
53 return firewall;
54}
55
56ScalingPolicy buildPolicy(const Config& config) {
57 ScalingPolicy policy;
58 policy.upperFactor = config.upperFactor;
59 policy.lowerFactor = config.lowerFactor;
60 policy.cooldownCycles = config.cooldown;
61 policy.minServers = config.minServers;
62 policy.maxServers = config.maxServers;
63 return policy;
64}
65
66GeneratorSettings buildGeneratorSettings(const Config& config) {
67 GeneratorSettings settings;
68 settings.minTaskCycles = config.minTask;
69 settings.maxTaskCycles = config.maxTask;
70 settings.arrivalRate = config.arrivalRate;
71 settings.waveAmplitude = config.waveAmplitude;
72 settings.wavePeriod = config.autoWavePeriod ? std::max<unsigned long>(4, config.cycles / 4) : config.wavePeriod;
73 settings.streamingShare = config.streamingShare;
74 return settings;
75}
76
77} // namespace
78
79Simulation::Simulation(const Config& config, Logger& logger)
80 : config_(config),
81 logger_(logger),
82 generator_(buildGeneratorSettings(config), config.seed),
83 statusEvery_(config.statusEvery),
84 snapshotEvery_(std::max<unsigned long>(1, config.cycles / 20)) {
85 if (statusEvery_ == 0) {
86 statusEvery_ = config.verbose ? 100 : std::max<unsigned long>(1, config.cycles / 20);
87 }
88 const Firewall firewall = buildFirewall(config);
89 const ScalingPolicy policy = buildPolicy(config);
90 if (config.useSwitch) {
91 // Split servers in proportion to the expected job mix; each side keeps at least one.
92 std::size_t streamingServers = static_cast<std::size_t>(std::lround(config.servers * config.streamingShare));
93 streamingServers = std::clamp<std::size_t>(streamingServers, 1, config.servers - 1);
94 const std::size_t processingServers = config.servers - streamingServers;
95 switch_ = std::make_unique<Switch>(
96 std::make_unique<LoadBalancer>("LB-P", processingServers, policy, firewall, config.maxQueue, logger),
97 std::make_unique<LoadBalancer>("LB-S", streamingServers, policy, firewall, config.maxQueue, logger));
98 } else {
99 single_ = std::make_unique<LoadBalancer>("LB", config.servers, policy, firewall, config.maxQueue, logger);
100 }
101}
102
104 const auto started = std::chrono::steady_clock::now();
105 const std::size_t initialCount = static_cast<std::size_t>(config_.servers) * config_.queuePerServer;
106 logger_.log(LogCategory::Info, 0, "SIM",
107 "generating initial queue of " + std::to_string(initialCount) + " requests (seed " +
108 std::to_string(config_.seed) + ")");
109 for (const Request& request : generator_.makeInitialBatch(initialCount, 0)) {
110 if (switch_) {
111 switch_->route(request, 0);
112 } else {
113 single_->admit(request, 0);
114 }
115 }
116 if (switch_) {
117 switch_->start();
118 } else {
119 single_->start();
120 }
121 for (LoadBalancer* balancer : mutableLoadBalancers()) {
122 balancer->recordSnapshot(0);
123 logger_.log(LogCategory::Status, 0, balancer->name(), balancer->statusLine(0));
124 }
125
126 for (unsigned long cycle = 1; cycle <= config_.cycles; ++cycle) {
127 const std::vector<Request> arrivals = generator_.arrivalsFor(cycle);
128 if (switch_) {
129 switch_->processCycle(cycle, arrivals);
130 } else {
131 single_->processCycle(cycle, arrivals);
132 }
133 const bool snapshotDue = cycle % snapshotEvery_ == 0 || cycle == config_.cycles;
134 const bool statusDue = cycle % statusEvery_ == 0 || cycle == config_.cycles;
135 for (LoadBalancer* balancer : mutableLoadBalancers()) {
136 if (snapshotDue) {
137 balancer->recordSnapshot(cycle);
138 }
139 if (statusDue) {
140 logger_.log(LogCategory::Status, cycle, balancer->name(), balancer->statusLine(cycle));
141 }
142 }
143 }
144 runSeconds_ = std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
145}
146
147std::vector<const LoadBalancer*> Simulation::loadBalancers() const {
148 if (switch_) {
149 return {&switch_->processing(), &switch_->streaming()};
150 }
151 return {single_.get()};
152}
153
154std::vector<LoadBalancer*> Simulation::mutableLoadBalancers() {
155 if (switch_) {
156 return {&switch_->processing(), &switch_->streaming()};
157 }
158 return {single_.get()};
159}
160
162 return generator_;
163}
164
165void Simulation::writeSettings(std::ostream& out) const {
166 heading(out, "1. RUN SETTINGS (user input)");
167 row(out, "Initial web servers", std::to_string(config_.servers));
168 row(out, "Clock cycles simulated", std::to_string(config_.cycles));
169 row(out, "Output mode", config_.verbose ? "verbose" : "quiet");
170 row(out, "Routing", config_.useSwitch ? "Switch: P jobs -> LB-P, S jobs -> LB-S (bonus)" : "single load balancer");
171 row(out, "Random seed", std::to_string(config_.seed));
172 row(out, "Task time range (uniform)", std::to_string(config_.minTask) + " to " + std::to_string(config_.maxTask) + " cycles");
173 row(out, "Arrivals per cycle", "Poisson, average " + fixed(config_.arrivalRate, 2));
174 const unsigned long wavePeriod = generator_.settings().wavePeriod;
175 if (wavePeriod > 0 && config_.waveAmplitude > 0.0) {
176 row(out, "Traffic wave", "rate " + fixed(config_.arrivalRate * (1.0 - config_.waveAmplitude), 2) + " to " +
177 fixed(config_.arrivalRate * (1.0 + config_.waveAmplitude), 2) + " per cycle, period " +
178 std::to_string(wavePeriod) + " cycles");
179 } else {
180 row(out, "Traffic wave", "off (steady rate)");
181 }
182 row(out, "Streaming (S) job share", fixed(config_.streamingShare * 100.0, 0) + "%");
183 row(out, "Initial queue", std::to_string(config_.queuePerServer) + " x servers = " +
184 std::to_string(static_cast<unsigned long>(config_.servers) * config_.queuePerServer) +
185 " requests");
186 row(out, "Scale up when", "queue > " + fixed(config_.upperFactor, 0) + " x active servers");
187 row(out, "Scale down when", "queue < " + fixed(config_.lowerFactor, 0) + " x active servers");
188 row(out, "Scaling check interval", "every " + std::to_string(config_.cooldown) + " cycles, one server at a time");
189 row(out, "Server limits", std::to_string(config_.minServers) + " to " + std::to_string(config_.maxServers));
190 row(out, "Queue capacity", config_.maxQueue == 0 ? "unlimited" : std::to_string(config_.maxQueue));
191 std::string ranges;
192 for (const std::string& range : config_.blockedRanges) {
193 ranges += (ranges.empty() ? "" : ", ") + range;
194 }
195 row(out, "Firewall blocked source ranges", ranges.empty() ? "none" : ranges);
196}
197
198void Simulation::writeRandomness(std::ostream& out) const {
199 const GeneratorStats& stats = generator_.stats();
200 heading(out, "3. RANDOMNESS IN REQUEST GENERATION");
201 row(out, "Requests generated", std::to_string(stats.total) + " (" + std::to_string(stats.initial) +
202 " initial + " + std::to_string(stats.total - stats.initial) + " during run)");
203 const double total = stats.total == 0 ? 1.0 : static_cast<double>(stats.total);
204 row(out, "Processing (P) / Streaming (S) jobs",
205 std::to_string(stats.processing) + " (" + fixed(100.0 * stats.processing / total, 1) + "%) / " +
206 std::to_string(stats.streaming) + " (" + fixed(100.0 * stats.streaming / total, 1) + "%)");
207 row(out, "Observed task time min / mean / max",
208 std::to_string(stats.minTaskSeen) + " / " + fixed(static_cast<double>(stats.taskCycleSum) / total, 1) + " / " +
209 std::to_string(stats.maxTaskSeen) + " cycles");
210 const double runCycles = config_.cycles == 0 ? 1.0 : static_cast<double>(config_.cycles);
211 row(out, "Observed arrivals per cycle (mean / max)",
212 fixed((stats.total - stats.initial) / runCycles, 3) + " / " + std::to_string(stats.maxArrivalsInCycle));
213
214 out << "\n Task time histogram (cycles)\n";
215 const GeneratorSettings& settings = generator_.settings();
216 const unsigned long span = settings.maxTaskCycles - settings.minTaskCycles + 1UL;
217 const unsigned long largest = *std::max_element(stats.taskHistogram.begin(), stats.taskHistogram.end());
218 for (std::size_t bin = 0; bin < GeneratorStats::kTaskBins; ++bin) {
219 const unsigned long low = settings.minTaskCycles + bin * span / GeneratorStats::kTaskBins;
220 const unsigned long high = settings.minTaskCycles + (bin + 1) * span / GeneratorStats::kTaskBins - 1;
221 if (high < low) {
222 continue; // empty bin when the range is narrower than the bin count
223 }
224 const unsigned long count = stats.taskHistogram[bin];
225 const std::size_t bar = largest == 0 ? 0 : static_cast<std::size_t>(30.0 * count / largest + 0.5);
226 std::ostringstream label;
227 label << std::setw(4) << low << "-" << std::left << std::setw(4) << high;
228 out << " " << label.str() << " " << std::right << std::setw(7) << count << " " << std::string(bar, '#') << '\n';
229 }
230
231 out << "\n New arrivals per cycle (Poisson)\n";
232 const char* labels[GeneratorStats::kArrivalBins] = {"0", "1", "2", "3", "4+"};
233 for (std::size_t bin = 0; bin < GeneratorStats::kArrivalBins; ++bin) {
234 const unsigned long count = stats.arrivalHistogram[bin];
235 out << " " << std::left << std::setw(3) << labels[bin] << std::right << std::setw(8) << count
236 << " cycles (" << fixed(100.0 * count / runCycles, 2) << "%)\n";
237 }
238
239 out << "\n First generated requests (sample)\n";
240 out << " " << std::left << std::setw(6) << "id" << std::setw(17) << "source IP" << std::setw(17)
241 << "destination IP" << std::setw(6) << "type" << "task cycles\n";
242 for (const Request& request : generator_.samples()) {
243 out << " " << std::left << std::setw(6) << request.id << std::setw(17) << request.sourceIp
244 << std::setw(17) << request.destinationIp << std::setw(6) << jobTypeCode(request.jobType)
245 << request.taskCycles << '\n';
246 }
247}
248
249void Simulation::writeSnapshots(std::ostream& out, const LoadBalancer& balancer) const {
250 out << "\n " << balancer.name() << " status every " << snapshotEvery_ << " cycles\n";
251 out << " " << std::right << std::setw(7) << "cycle" << std::setw(8) << "queue" << std::setw(8) << "live"
252 << std::setw(7) << "busy" << std::setw(7) << "idle" << std::setw(7) << "retire" << std::setw(11)
253 << "completed" << std::setw(10) << "rejected" << '\n';
254 for (const Snapshot& snapshot : balancer.snapshots()) {
255 out << " " << std::setw(7) << snapshot.cycle << std::setw(8) << snapshot.queueSize << std::setw(8)
256 << snapshot.liveServers << std::setw(7) << snapshot.busyServers << std::setw(7) << snapshot.idleServers
257 << std::setw(7) << snapshot.retiringServers << std::setw(11) << snapshot.completed << std::setw(10)
258 << snapshot.rejected << '\n';
259 }
260}
261
262void Simulation::writeScaleTimeline(std::ostream& out, const LoadBalancer& balancer) const {
263 const std::vector<ScaleEvent>& events = balancer.scaleEvents();
264 out << "\n " << balancer.name() << ": " << events.size() << " server pool changes\n";
265 if (events.empty()) {
266 return;
267 }
268 // Print every change for normal runs; very long runs keep the first and last changes.
269 const std::size_t head = events.size() <= 500 ? events.size() : 300;
270 const std::size_t tail = events.size() <= 500 ? 0 : 100;
271 const auto print = [&out](const ScaleEvent& event) {
272 out << " t=" << std::left << std::setw(8) << event.cycle << std::setw(17) << kindName(event.kind)
273 << "server " << std::setw(5) << event.serverId << "queue " << std::setw(7) << event.queueSize
274 << "active servers now " << event.activeServers << '\n';
275 };
276 for (std::size_t i = 0; i < events.size() && i < head; ++i) {
277 print(events[i]);
278 }
279 if (events.size() > head + tail) {
280 out << " ... " << (events.size() - head - tail)
281 << " more changes omitted (see the status table above or run with --verbose) ...\n";
282 }
283 for (std::size_t i = std::max(head, events.size() > tail ? events.size() - tail : 0); i < events.size(); ++i) {
284 print(events[i]);
285 }
286}
287
288void Simulation::writeReport(std::ostream& out) const {
289 const std::time_t now = std::time(nullptr);
290 char stamp[32];
291 std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", std::localtime(&now));
292
293 out << kRule << '\n'
294 << " AGGIE STUDENT TOOLKIT - LOAD BALANCER LOG SUMMARY\n"
295 << " CSCE 412 Project 4 | Arnav Fotedar | generated " << stamp << '\n'
296 << kRule << '\n';
297
298 writeSettings(out);
299
300 const std::vector<const LoadBalancer*> balancers = loadBalancers();
301 std::size_t startingQueue = 0;
302 std::size_t endingQueue = 0;
303 for (const LoadBalancer* balancer : balancers) {
304 startingQueue += balancer->stats().startingQueue;
305 endingQueue += balancer->queueSize();
306 }
307 heading(out, "2. REQUIRED SUMMARY");
308 row(out, "Starting queue size", std::to_string(startingQueue) + " requests (t=0, after firewall, before dispatch)");
309 row(out, "Ending queue size", std::to_string(endingQueue) + " requests (t=" + std::to_string(config_.cycles) + ")");
310 row(out, "Range for task times", std::to_string(config_.minTask) + " to " + std::to_string(config_.maxTask) +
311 " clock cycles");
312
313 writeRandomness(out);
314
315 heading(out, "4. END STATUS AND STATISTICS");
316 if (switch_) {
317 row(out, "Requests routed to LB-P / LB-S",
318 std::to_string(switch_->routedProcessing()) + " / " + std::to_string(switch_->routedStreaming()));
319 out << '\n';
320 }
321 for (const LoadBalancer* balancer : balancers) {
322 balancer->summarize(out);
323 out << '\n';
324 }
325 row(out, "Simulation wall-clock time", fixed(runSeconds_, 2) + " s");
326
327 heading(out, "5. SERVERS AT THE END OF THE RUN");
328 for (const LoadBalancer* balancer : balancers) {
329 out << " " << balancer->name() << '\n';
330 out << " " << std::left << std::setw(8) << "server" << std::setw(10) << "state" << std::setw(26)
331 << "current request" << std::setw(11) << "remaining" << std::setw(11) << "completed"
332 << "online since\n";
333 for (const WebServer& server : balancer->servers()) {
334 std::string state = server.isIdle() ? "idle" : (server.isRetiring() ? "retiring" : "busy");
335 std::string current = "-";
336 if (server.current()) {
337 current = "req#" + std::to_string(server.current()->id) + " (" +
338 std::string(1, jobTypeCode(server.current()->jobType)) + ", " +
339 std::to_string(server.current()->taskCycles) + " cyc)";
340 }
341 out << " " << std::left << std::setw(8) << server.id() << std::setw(10) << state << std::setw(26)
342 << current << std::setw(11) << server.remainingCycles() << std::setw(11) << server.completedCount()
343 << "t=" << server.createdCycle() << '\n';
344 }
345 out << '\n';
346 }
347
348 heading(out, "6. DYNAMIC SERVER COUNT AND QUEUE OVER TIME");
349 for (const LoadBalancer* balancer : balancers) {
350 writeSnapshots(out, *balancer);
351 }
352
353 heading(out, "7. SCALING TIMELINE (server pool changes)");
354 for (const LoadBalancer* balancer : balancers) {
355 writeScaleTimeline(out, *balancer);
356 }
357 out << '\n' << kRule << "\n END OF LOG\n" << kRule << '\n';
358}
@ Info
General information such as start-up messages.
Definition Logger.h:25
@ Status
Periodic queue and server status line.
Definition Logger.h:24
char jobTypeCode(JobType type)
Returns the one-letter code for a job type ('P' or 'S').
Definition Request.h:29
Declares the Simulation class, which drives the clock and writes the run log.
Rejects requests whose source address falls in a blocked IPv4 range.
Definition Firewall.h:22
bool addRange(const std::string &cidr)
Adds a blocked range.
Definition Firewall.cpp:44
Distributes queued requests across a dynamically sized pool of WebServer objects.
std::size_t queueSize() const
const std::string & name() const
const LoadBalancerStats & stats() const
void summarize(std::ostream &out) const
Writes the end-of-run summary for this load balancer.
const std::vector< Snapshot > & snapshots() const
const std::vector< ScaleEvent > & scaleEvents() const
const std::vector< WebServer > & servers() const
Writes simulation events to the console and, optionally, to a file.
Definition Logger.h:40
Produces random requests from a single seeded Mersenne Twister engine.
const GeneratorSettings & settings() const
void writeReport(std::ostream &out) const
Writes the full log summary: settings, randomness, results, snapshots and scaling timeline.
std::vector< const LoadBalancer * > loadBalancers() const
Simulation(const Config &config, Logger &logger)
Builds the components described by a validated configuration.
const RequestGenerator & generator() const
void run()
Runs every clock cycle of the simulation.
A simulated web server that processes at most one request at a time.
Definition WebServer.h:26
Every user-adjustable setting for one simulation run.
Definition Config.h:21
double arrivalRate
Average new requests per cycle.
Definition Config.h:26
double lowerFactor
Scale down when queue < lowerFactor x servers.
Definition Config.h:33
unsigned int servers
Initial number of web servers.
Definition Config.h:22
bool useSwitch
Bonus: route P and S jobs to two load balancers.
Definition Config.h:42
unsigned long cycles
Clock cycles to simulate.
Definition Config.h:23
unsigned int maxTask
Longest task time, in cycles.
Definition Config.h:25
bool verbose
Print every event instead of only status lines.
Definition Config.h:40
double waveAmplitude
Traffic wave size, 0 (steady) to 1.
Definition Config.h:27
double streamingShare
Fraction of requests that are streaming ('S') jobs.
Definition Config.h:30
std::uint32_t seed
Random seed (logged so a run can be repeated).
Definition Config.h:38
unsigned int cooldown
Cycles between scaling checks.
Definition Config.h:34
std::size_t maxQueue
Queue capacity; 0 means unlimited.
Definition Config.h:37
unsigned int minServers
Lower bound on active servers.
Definition Config.h:35
unsigned int minTask
Shortest task time, in cycles.
Definition Config.h:24
double upperFactor
Scale up when queue > upperFactor x servers.
Definition Config.h:32
std::vector< std::string > blockedRanges
Firewall CIDR ranges.
Definition Config.h:39
bool autoWavePeriod
Use cycles / 4 (four busy/quiet waves) unless –wave-period is given.
Definition Config.h:29
unsigned int maxServers
Upper bound on live servers.
Definition Config.h:36
Tunable parameters for random request generation.
unsigned int minTaskCycles
Shortest possible task, in clock cycles.
double waveAmplitude
Traffic wave size: the rate swings between (1 - a) and (1 + a) x arrivalRate.
unsigned long wavePeriod
Cycles in one busy-to-quiet traffic wave; 0 disables the wave.
double arrivalRate
Average new requests per clock cycle (Poisson mean).
double streamingShare
Probability that a request is a streaming ('S') job.
unsigned int maxTaskCycles
Longest possible task, in clock cycles.
static constexpr std::size_t kArrivalBins
Arrival-count buckets: 0, 1, 2, 3, 4+.
unsigned long streaming
Generated requests with JobType::Streaming.
unsigned int minTaskSeen
Shortest task actually generated.
std::array< unsigned long, kTaskBins > taskHistogram
Task lengths split into equal-width bins.
unsigned long initial
Requests generated for the initial queue.
unsigned int maxTaskSeen
Longest task actually generated.
std::array< unsigned long, kArrivalBins > arrivalHistogram
Cycles with 0, 1, 2, 3 and 4+ arrivals.
static constexpr std::size_t kTaskBins
Number of task-length histogram bins.
unsigned long maxArrivalsInCycle
Largest burst generated in a single cycle.
unsigned long processing
Generated requests with JobType::Processing.
unsigned long total
Requests generated, including the initial queue.
unsigned long long taskCycleSum
Sum of all task lengths, for the mean.
std::size_t startingQueue
Queue size at time 0, before the first dispatch.
A single web request waiting for, or receiving, service.
Definition Request.h:41
Kind
What happened to the server.
@ Retiring
A busy server was marked to be removed after its current request.
@ Removed
An idle server was removed immediately.
@ Reactivated
A retiring server was returned to service instead of creating one.
@ Retired
A retiring server finished its request and was removed.
@ Added
A new server was created.
Rules for growing and shrinking the server pool.
unsigned int maxServers
Never more live servers (active + retiring) than this.
double lowerFactor
Scale down when queue < lowerFactor x active servers.
unsigned int cooldownCycles
Cycles between scaling evaluations.
unsigned int minServers
Never fewer active servers than this.
double upperFactor
Scale up when queue > upperFactor x active servers.