LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_station.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_LDES_LDES_STATION_H
6#define LINE_SOLVERS_LDES_LDES_STATION_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The scheduling disciplines of the native LDES engine.
12 *
13 * TWO FAMILIES, and they are structurally different simulators sharing one
14 * station:
15 *
16 * - THE BUFFERED disciplines (FCFS, LCFS, SIRO, the priority variants, SJF,
17 * LJF, SEPT, LEPT) hold waiting jobs in an ORDER and hand the head to a
18 * free server. The discipline IS the order, so it lives entirely in
19 * `WaitCmp` below; everything downstream is the same code.
20 * - THE SHARING disciplines (PS, DPS, GPS, their priority variants and LPS)
21 * have no order at all. Every job in service advances at its own share of
22 * the server, so the station has no head and no service-start event: the
23 * residual work of EVERY job must be integrated forward and EVERY departure
24 * rescheduled whenever the population changes. `PsStation` does that.
25 *
26 * THE SERVICE TIME IS SAMPLED AT ARRIVAL, not at service start. This is the
27 * reference's choice (`customer.serviceTime = generateServiceTime(...)` in the
28 * arrival handlers) and it is forced: SJF, LJF and the size-based orders
29 * compare service times of jobs that have not started, so the sample must
30 * exist before the job is ordered. It is invisible under FCFS, where arrival
31 * and service order coincide, and visible under LCFS with a MAP service
32 * process, whose phase then advances in ARRIVAL order.
33 *
34 * PRIORITY ORDERS ASCENDING: a lower value is more urgent, 0 highest. That is
35 * LINE's convention throughout (`SaveHandlers` inverts it when exporting to
36 * JMT, which orders the other way), and every comparator here follows it.
37 */
38
39#include <algorithm>
40#include <cmath>
41#include <cstddef>
42#include <cstdint>
43#include <limits>
44#include <set>
45#include <vector>
46
48
49namespace line {
50namespace ldes {
51namespace engine {
52
53/**
54 * A job held by a station.
55 *
56 * `service`, `remaining` and `elapsed` are the three quantities the size-based
57 * orders need and are NOT redundant. `service` is the ORIGINAL requirement
58 * PSJF orders by; `remaining` is the residual SRPT and LRPT order by, equal to
59 * `service` until the job is first preempted; `elapsed` is the attained
60 * service FB and SETF order by. The reference keeps them in a side table
61 * (`preemptedJobHistory`, keyed by the arrival instants) because its wait
62 * queue holds a plain `Customer`; carrying them on the job is the same
63 * information without the lookup, and without the key collision two jobs
64 * arriving at the same instant would produce.
65 */
66struct Job {
67 std::size_t cls = 0;
68 int priority = 0;
69 double t_arr = 0.0; ///< arrival instant at this station
70 double t_sys = 0.0; ///< instant the current passage started
71 double service = 0.0; ///< the sampled requirement, drawn at arrival
72 double remaining = 0.0; ///< residual work; equals `service` before any preemption
73 double elapsed = 0.0; ///< attained service across all its service intervals
74 double rank = 0.0; ///< SIRO's random key, drawn at arrival
75 double deadline = std::numeric_limits<double>::infinity(); ///< EDD/EDF key
76 double vft = 0.0; ///< FSP's virtual finish time, recomputed on demand
77 /**
78 * Identity of this job while it waits, so a reneging timer can find it.
79 *
80 * A timer names the job it was armed for, and the job may by then have
81 * entered service, been preempted back into the queue, or already left. An
82 * identity is the only thing that survives all three; the arrival instant
83 * cannot serve as one, because two jobs admitted at the same instant --
84 * routine on a closed model placed at t=0 -- would share it.
85 */
86 std::uint64_t id = 0;
87 /**
88 * The fork synchronization this job is a sibling of, 0 when it is not one.
89 *
90 * It rides on the JOB and not on the station because siblings of different
91 * parents interleave freely at every station between the Fork and the Join.
92 */
93 std::uint64_t parent = 0;
94 /**
95 * How many times this job has already retried from an orbit.
96 *
97 * It rides on the job because `maxAttempts` bounds the retries of ONE job,
98 * not of the station: a per-station counter would cap the orbit's total
99 * traffic instead and would let a job retry forever as long as others gave
100 * up.
101 */
102 int attempts = 0;
103 /**
104 * The SYNCHRONOUS CALL this job belongs to, 0 when it belongs to none.
105 *
106 * It rides on the job because the call is answered by a REPLY that comes
107 * back from somewhere else entirely: this identity is the only link between
108 * that reply and the server still held for it, and `id` cannot serve as one,
109 * being reassigned at every admission.
110 */
111 std::uint64_t call = 0;
112};
113
114/** True for the disciplines that interrupt a job already in service. */
137
138/**
139 * PREEMPTIVE RESUME, as against preemptive restart.
140 *
141 * A resumed job continues from its residual work; a restarted one draws a
142 * FRESH requirement and discards what it had done. That distinction is the
143 * whole of the PR/PI split and it is not cosmetic: under exponential service
144 * the two agree in distribution (memorylessness) and under any other law they
145 * do not, so a model that uses PI to represent lost work is silently served
146 * as PR on a Coxian. Every size-based policy is RESUME -- restarting a job
147 * would make its own scheduling key meaningless.
148 */
150 switch (s) {
155 return false;
156 default:
157 return is_preemptive(s);
158 }
159}
160
161/** True for the orders whose key is the job's size, residual or attained work. */
163 switch (s) {
171 return true;
172 default:
173 return false;
174 }
175}
176
177/** True for the disciplines that share the server instead of ordering a queue. */
179 switch (s) {
187 return true;
188 default:
189 return false;
190 }
191}
192
193/**
194 * The waiting-room order of a buffered discipline.
195 *
196 * Used as a `std::priority_queue` comparator, which pops the GREATEST element,
197 * so every rule below is inverted relative to the reference's `Comparator`
198 * (which orders ascending and polls the least). Getting that backwards turns
199 * FCFS into LCFS silently -- both run, both conserve jobs, and only the
200 * response-time variance tells them apart.
201 */
202struct WaitCmp {
204 const std::vector<double>* class_mean = 0; ///< mean service per class, for SEPT/LEPT
205
206 bool operator()(const Job& a, const Job& b) const {
207 switch (sched) {
211 return a.t_arr < b.t_arr; // latest first
213 return a.rank > b.rank;
219 if (a.priority != b.priority) return a.priority > b.priority;
220 return a.t_arr > b.t_arr;
224 if (a.priority != b.priority) return a.priority > b.priority;
225 return a.t_arr < b.t_arr;
227 if (a.service != b.service) return a.service > b.service;
228 return a.t_arr > b.t_arr;
230 if (a.service != b.service) return a.service < b.service;
231 return a.t_arr > b.t_arr;
233 const double ma = mean_of(a), mb = mean_of(b);
234 if (ma != mb) return ma > mb; // shortest expected first
235 return a.t_arr > b.t_arr;
236 }
238 const double ma = mean_of(a), mb = mean_of(b);
239 if (ma != mb) return ma < mb;
240 return a.t_arr > b.t_arr;
241 }
244 if (a.deadline != b.deadline) return a.deadline > b.deadline;
245 return a.t_arr > b.t_arr;
247 if (a.remaining != b.remaining) return a.remaining > b.remaining;
248 return a.t_arr > b.t_arr;
250 if (a.priority != b.priority) return a.priority > b.priority;
251 if (a.remaining != b.remaining) return a.remaining > b.remaining;
252 return a.t_arr > b.t_arr;
254 if (a.remaining != b.remaining) return a.remaining < b.remaining;
255 return a.service < b.service;
257 // The ORIGINAL requirement, not the residual: a job that has
258 // already been served keeps the rank its full size gave it.
259 if (a.service != b.service) return a.service > b.service;
260 return a.t_arr > b.t_arr;
263 // Least attained service first. FB and SETF share this order;
264 // they differ in WHEN the running job is interrupted, not in
265 // how the waiting ones are ranked.
266 if (a.elapsed != b.elapsed) return a.elapsed > b.elapsed;
267 return a.t_arr > b.t_arr;
269 if (a.vft != b.vft) return a.vft > b.vft;
270 return a.t_arr > b.t_arr;
271 default: // FCFS and anything ordered like it
272 return a.t_arr > b.t_arr;
273 }
274 }
275
276private:
277 double mean_of(const Job& j) const {
278 if (class_mean == 0 || j.cls >= class_mean->size()) return 0.0;
279 return (*class_mean)[j.cls];
280 }
281};
282
283/** A job being served by a sharing discipline, with the work it still owes. */
284struct PsJob {
285 std::size_t cls = 0;
286 int priority = 0;
287 double t_arr = 0.0;
288 double t_sys = 0.0;
289 double total = 0.0; ///< the sampled requirement
290 double remaining = 0.0; ///< residual work, in service-time units
291 std::uint64_t tag = 0; ///< identifies the departure event scheduled for it
292 /**
293 * The fork the job belongs to, carried across the sharing station.
294 *
295 * A buffered station keeps the whole `Job` in its server slot and hands it
296 * back at the completion, so the identity survives; a sharing station holds
297 * this reduced record instead and would hand back a DEFAULT-CONSTRUCTED one.
298 * A sibling leaving with parent 0 matches no fork at the Join and is
299 * discarded there, which drains a closed fork-join model after one pass.
300 */
301 std::uint64_t parent = 0;
302};
303
304/**
305 * The per-job shares of a sharing discipline, in units of one server.
306 *
307 * `c` is the station's effective server count: with n <= c every job runs at
308 * rate 1 and the station is an infinite server; above that the capacity is
309 * split. Each rule is capped at 1 because ONE job cannot consume more than ONE
310 * server however the weights fall -- dropping that cap is what makes a
311 * two-class DPS station report a utilization above one.
312 *
313 * Transcribed from `calculatePSRates` and the five rules under it.
314 */
315inline std::vector<double> ps_shares(lang::SchedStrategy sched, const std::vector<PsJob>& jobs,
316 double c, const std::vector<double>& weight,
317 std::size_t nclasses) {
318 const std::size_t n = jobs.size();
319 std::vector<double> rates(n, 0.0);
320 if (n == 0) return rates;
321
322 const bool prio = (sched == lang::SchedStrategy::PSPRIO ||
325 const bool weighted_by_job = (sched == lang::SchedStrategy::DPS ||
327 const bool weighted_by_class = (sched == lang::SchedStrategy::GPS ||
329
330 if (!prio) {
331 if (weighted_by_job) {
332 double tot = 0.0;
333 for (const PsJob& j : jobs) tot += weight[j.cls];
334 if (tot > 0.0) {
335 for (std::size_t i = 0; i < n; ++i)
336 rates[i] = std::min(1.0, weight[jobs[i].cls] * c / tot);
337 return rates;
338 }
339 } else if (weighted_by_class) {
340 std::vector<int> per_class(nclasses, 0);
341 for (const PsJob& j : jobs) ++per_class[j.cls];
342 double tot = 0.0;
343 for (std::size_t k = 0; k < nclasses; ++k)
344 if (per_class[k] > 0) tot += weight[k];
345 if (tot > 0.0) {
346 for (std::size_t i = 0; i < n; ++i) {
347 const std::size_t k = jobs[i].cls;
348 rates[i] = std::min(1.0, (weight[k] / tot / per_class[k]) * c);
349 }
350 return rates;
351 }
352 }
353 // PS, LPS, and the degenerate all-zero-weight case: equal shares.
354 const double share = std::min(1.0, c / static_cast<double>(n));
355 for (std::size_t i = 0; i < n; ++i) rates[i] = share;
356 return rates;
357 }
358
359 // The priority variants give the whole station to the most urgent class
360 // present, and only what is left over to the next -- so an uncongested
361 // station (n <= c) has nothing to arbitrate and every job runs at 1.
362 if (static_cast<double>(n) <= c) {
363 for (std::size_t i = 0; i < n; ++i) rates[i] = 1.0;
364 return rates;
365 }
366 std::set<int> prios;
367 for (const PsJob& j : jobs) prios.insert(j.priority);
368 double remaining = c;
369 for (std::set<int>::const_iterator it = prios.begin(); it != prios.end(); ++it) {
370 if (remaining <= 0.0) break;
371 const int p = *it;
372 int count = 0;
373 double tot_job_weight = 0.0;
374 std::vector<int> per_class(nclasses, 0);
375 for (const PsJob& j : jobs)
376 if (j.priority == p) {
377 ++count;
378 tot_job_weight += weight[j.cls];
379 ++per_class[j.cls];
380 }
381 const double allocated = std::min(remaining, static_cast<double>(count));
382 double tot_class_weight = 0.0;
383 for (std::size_t k = 0; k < nclasses; ++k)
384 if (per_class[k] > 0) tot_class_weight += weight[k];
385
386 for (std::size_t i = 0; i < n; ++i) {
387 if (jobs[i].priority != p) continue;
388 const std::size_t k = jobs[i].cls;
389 if (sched == lang::SchedStrategy::DPSPRIO && tot_job_weight > 0.0)
390 rates[i] = std::min(1.0, weight[k] * allocated / tot_job_weight);
391 else if (sched == lang::SchedStrategy::GPSPRIO && tot_class_weight > 0.0)
392 rates[i] = std::min(1.0, (weight[k] / tot_class_weight / per_class[k]) * allocated);
393 else
394 rates[i] = allocated / count;
395 }
396 remaining -= allocated;
397 }
398 return rates;
399}
400
401/**
402 * FSP's virtual finish time: when `target` would finish if the station ran
403 * processor sharing from now on.
404 *
405 * Sort every residual work at the station, then walk it: while n jobs remain
406 * each advances at min(1, c/n), so the wall time to clear the k-th smallest
407 * residual is the sum of (w_k - w_{k-1}) / rate over the ranks below it. FSP
408 * then serves in that order, which is what makes it dominate PS job by job.
409 * Transcribes `computeFSPVirtualFinishTime`.
410 */
411inline double fsp_virtual_finish(const std::vector<double>& residuals, double target_work,
412 double c, double now) {
413 std::vector<double> works = residuals;
414 std::sort(works.begin(), works.end());
415 const std::size_t n = works.size();
416 double cumulative = 0.0, prev = 0.0;
417 for (std::size_t rank = 0; rank < n; ++rank) {
418 const double w = works[rank];
419 const double rate = std::min(1.0, c / static_cast<double>(n - rank));
420 if (rate > 0.0) cumulative += (w - prev) / rate;
421 if (w >= target_work) return now + cumulative;
422 prev = w;
423 }
424 return now + cumulative;
425}
426
427/**
428 * Which job in service `arriving` displaces, or `in_service.size()` for none.
429 *
430 * ONE RULE PER FAMILY, transcribed from `shouldPreemptForLCFS`,
431 * `shouldPreemptForFCFS` and `shouldPreemptForSizeBasedPolicy` together with
432 * their `findJobToPreempt*` counterparts. The two halves are fused here because
433 * they ask the same question twice in the reference -- once to decide and once
434 * to choose -- and answering it once cannot disagree with itself.
435 *
436 * THE PRIORITY VARIANTS PREEMPT ONLY STRICTLY LOWER PRIORITY. `LCFSPR` without
437 * the PRIO suffix preempts unconditionally (it is last-come-first-served with
438 * no class ordering at all), while `LCFSPRPRIO` preempts a job whose priority
439 * value is at least the arrival's, and the FCFS family only one strictly
440 * greater. Collapsing those three tests into one turns a priority station into
441 * a thrashing one, or into a non-preemptive one, depending which way it falls.
442 */
443inline std::size_t preemption_victim(lang::SchedStrategy sched, const std::vector<Job>& in_service,
444 const Job& arriving, double c, double now) {
445 const std::size_t none = in_service.size();
446 if (in_service.empty()) return none;
447
448 switch (sched) {
451 // Displace the OLDEST job in service: the newcomer takes the head
452 // of a last-come-first-served station unconditionally.
453 std::size_t best = none;
454 double oldest = std::numeric_limits<double>::infinity();
455 for (std::size_t j = 0; j < in_service.size(); ++j)
456 if (in_service[j].t_arr < oldest) {
457 oldest = in_service[j].t_arr;
458 best = j;
459 }
460 return best;
461 }
464 std::size_t best = none;
465 double oldest = std::numeric_limits<double>::infinity();
466 for (std::size_t j = 0; j < in_service.size(); ++j)
467 if (in_service[j].priority >= arriving.priority && in_service[j].t_arr < oldest) {
468 oldest = in_service[j].t_arr;
469 best = j;
470 }
471 return best;
472 }
477 // The least urgent job in service, tie-broken by the LATEST service
478 // start: among equals, the one that has invested least is displaced.
479 std::size_t best = none;
480 for (std::size_t j = 0; j < in_service.size(); ++j) {
481 if (in_service[j].priority <= arriving.priority) continue;
482 if (best == none || in_service[j].priority > in_service[best].priority ||
483 (in_service[j].priority == in_service[best].priority &&
484 in_service[j].t_arr > in_service[best].t_arr))
485 best = j;
486 }
487 return best;
488 }
491 std::size_t worst = 0;
492 for (std::size_t j = 1; j < in_service.size(); ++j)
493 if (in_service[j].remaining > in_service[worst].remaining) worst = j;
494 if (sched == lang::SchedStrategy::SRPTPRIO &&
495 in_service[worst].priority < arriving.priority)
496 return none;
497 return (arriving.remaining < in_service[worst].remaining) ? worst : none;
498 }
500 std::size_t largest = 0;
501 for (std::size_t j = 1; j < in_service.size(); ++j)
502 if (in_service[j].service > in_service[largest].service) largest = j;
503 return (arriving.service < in_service[largest].service) ? largest : none;
504 }
507 // A fresh arrival has attained nothing, so it displaces whichever
508 // job has attained the most -- provided that is more than nothing.
509 std::size_t most = 0;
510 for (std::size_t j = 1; j < in_service.size(); ++j)
511 if (in_service[j].elapsed > in_service[most].elapsed) most = j;
512 return (in_service[most].elapsed > arriving.elapsed) ? most : none;
513 }
515 std::size_t least = 0;
516 for (std::size_t j = 1; j < in_service.size(); ++j)
517 if (in_service[j].remaining < in_service[least].remaining) least = j;
518 return (arriving.remaining > in_service[least].remaining) ? least : none;
519 }
521 std::vector<double> works;
522 for (const Job& j : in_service) works.push_back(j.remaining);
523 works.push_back(arriving.remaining);
524 const double arrival_vft = fsp_virtual_finish(works, arriving.remaining, c, now);
525 std::size_t worst = 0;
526 double worst_vft = -std::numeric_limits<double>::infinity();
527 for (std::size_t j = 0; j < in_service.size(); ++j) {
528 const double v = fsp_virtual_finish(works, in_service[j].remaining, c, now);
529 if (v > worst_vft) {
530 worst_vft = v;
531 worst = j;
532 }
533 }
534 return (arrival_vft < worst_vft) ? worst : none;
535 }
536 default:
537 return none;
538 }
539}
540
541} // namespace engine
542} // namespace ldes
543} // namespace line
544
545#endif // LINE_SOLVERS_LDES_LDES_STATION_H
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
bool is_preemptive(lang::SchedStrategy s)
True for the disciplines that interrupt a job already in service.
double fsp_virtual_finish(const std::vector< double > &residuals, double target_work, double c, double now)
FSP's virtual finish time: when target would finish if the station ran processor sharing from now on.
std::vector< double > ps_shares(lang::SchedStrategy sched, const std::vector< PsJob > &jobs, double c, const std::vector< double > &weight, std::size_t nclasses)
The per-job shares of a sharing discipline, in units of one server.
bool is_preemptive_resume(lang::SchedStrategy s)
PREEMPTIVE RESUME, as against preemptive restart.
bool is_ps_family(lang::SchedStrategy s)
True for the disciplines that share the server instead of ordering a queue.
bool is_size_based(lang::SchedStrategy s)
True for the orders whose key is the job's size, residual or attained work.
std::size_t preemption_victim(lang::SchedStrategy sched, const std::vector< Job > &in_service, const Job &arriving, double c, double now)
Which job in service arriving displaces, or in_service.size() for none.
A job held by a station.
double deadline
EDD/EDF key.
double t_arr
arrival instant at this station
std::uint64_t call
The SYNCHRONOUS CALL this job belongs to, 0 when it belongs to none.
double rank
SIRO's random key, drawn at arrival.
double vft
FSP's virtual finish time, recomputed on demand.
int attempts
How many times this job has already retried from an orbit.
double t_sys
instant the current passage started
double elapsed
attained service across all its service intervals
double service
the sampled requirement, drawn at arrival
double remaining
residual work; equals service before any preemption
std::uint64_t parent
The fork synchronization this job is a sibling of, 0 when it is not one.
A job being served by a sharing discipline, with the work it still owes.
double total
the sampled requirement
std::uint64_t tag
identifies the departure event scheduled for it
double remaining
residual work, in service-time units
std::uint64_t parent
The fork the job belongs to, carried across the sharing station.
The waiting-room order of a buffered discipline.
lang::SchedStrategy sched
bool operator()(const Job &a, const Job &b) const
const std::vector< double > * class_mean
mean service per class, for SEPT/LEPT