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 /**
71 * Time spent parked outside a WAITQ region before entering this station. It is part of the
72 * visit's response time (JMT and Solver_ssj keep the original arrival instant) but not of
73 * `t_arr`, which orders the buffer by the RELEASE instant, as the Java engine's orderTime does.
74 */
75 double region_wait = 0.0;
76 double t_sys = 0.0; ///< instant the current passage started
77 double service = 0.0; ///< the sampled requirement, drawn at arrival
78 double remaining = 0.0; ///< residual work; equals `service` before any preemption
79 double elapsed = 0.0; ///< attained service across all its service intervals
80 double rank = 0.0; ///< SIRO's random key, drawn at arrival
81 double deadline = std::numeric_limits<double>::infinity(); ///< EDD/EDF key
82 double vft = 0.0; ///< FSP's virtual finish time, recomputed on demand
83 /**
84 * Identity of this job while it waits, so a reneging timer can find it.
85 *
86 * A timer names the job it was armed for, and the job may by then have
87 * entered service, been preempted back into the queue, or already left. An
88 * identity is the only thing that survives all three; the arrival instant
89 * cannot serve as one, because two jobs admitted at the same instant --
90 * routine on a closed model placed at t=0 -- would share it.
91 */
92 std::uint64_t id = 0;
93 /**
94 * The fork synchronization this job is a sibling of, 0 when it is not one.
95 *
96 * It rides on the JOB and not on the station because siblings of different
97 * parents interleave freely at every station between the Fork and the Join.
98 */
99 std::uint64_t parent = 0;
100 /**
101 * How many times this job has already retried from an orbit.
102 *
103 * It rides on the job because `maxAttempts` bounds the retries of ONE job,
104 * not of the station: a per-station counter would cap the orbit's total
105 * traffic instead and would let a job retry forever as long as others gave
106 * up.
107 */
108 int attempts = 0;
109 /**
110 * The SYNCHRONOUS CALL this job belongs to, 0 when it belongs to none.
111 *
112 * It rides on the job because the call is answered by a REPLY that comes
113 * back from somewhere else entirely: this identity is the only link between
114 * that reply and the server still held for it, and `id` cannot serve as one,
115 * being reassigned at every admission.
116 */
117 std::uint64_t call = 0;
118 /**
119 * The generation its currently scheduled completion was pushed under.
120 *
121 * Only an INFINITE SERVER needs it: a buffered station keeps the generation
122 * on the slot (`server_tag`) and a sharing one on its `PsJob`, but a Delay
123 * holds neither, so a think time re-timed by a global dependence has no
124 * other way to invalidate the departure already in the event list.
125 */
126 std::uint64_t tag = 0;
127};
128
129/** True for the disciplines that interrupt a job already in service. */
152
153/**
154 * PREEMPTIVE RESUME, as against preemptive restart.
155 *
156 * A resumed job continues from its residual work; a restarted one draws a
157 * FRESH requirement and discards what it had done. That distinction is the
158 * whole of the PR/PI split and it is not cosmetic: under exponential service
159 * the two agree in distribution (memorylessness) and under any other law they
160 * do not, so a model that uses PI to represent lost work is silently served
161 * as PR on a Coxian. Every size-based policy is RESUME -- restarting a job
162 * would make its own scheduling key meaningless.
163 */
165 switch (s) {
170 return false;
171 default:
172 return is_preemptive(s);
173 }
174}
175
176/** True for the orders whose key is the job's size, residual or attained work. */
178 switch (s) {
186 return true;
187 default:
188 return false;
189 }
190}
191
192/** True for the disciplines that share the server instead of ordering a queue. */
194 switch (s) {
202 return true;
203 default:
204 return false;
205 }
206}
207
208/**
209 * The waiting-room order of a buffered discipline.
210 *
211 * Used as a `std::priority_queue` comparator, which pops the GREATEST element,
212 * so every rule below is inverted relative to the reference's `Comparator`
213 * (which orders ascending and polls the least). Getting that backwards turns
214 * FCFS into LCFS silently -- both run, both conserve jobs, and only the
215 * response-time variance tells them apart.
216 */
217struct WaitCmp {
219 const std::vector<double>* class_mean = 0; ///< mean service per class, for SEPT/LEPT
220
221 bool operator()(const Job& a, const Job& b) const {
222 switch (sched) {
226 return a.t_arr < b.t_arr; // latest first
228 return a.rank > b.rank;
232 // Plain FCFSPR/FCFSPI are DELIBERATELY absent: a policy that did
233 // not declare itself priority-aware must not read `priority`, so
234 // they fall to the default arm and queue in arrival order.
235 if (a.priority != b.priority) return a.priority > b.priority;
236 return a.t_arr > b.t_arr;
240 if (a.priority != b.priority) return a.priority > b.priority;
241 return a.t_arr < b.t_arr;
243 if (a.service != b.service) return a.service > b.service;
244 return a.t_arr > b.t_arr;
246 if (a.service != b.service) return a.service < b.service;
247 return a.t_arr > b.t_arr;
249 const double ma = mean_of(a), mb = mean_of(b);
250 if (ma != mb) return ma > mb; // shortest expected first
251 return a.t_arr > b.t_arr;
252 }
254 const double ma = mean_of(a), mb = mean_of(b);
255 if (ma != mb) return ma < mb;
256 return a.t_arr > b.t_arr;
257 }
260 if (a.deadline != b.deadline) return a.deadline > b.deadline;
261 return a.t_arr > b.t_arr;
263 if (a.remaining != b.remaining) return a.remaining > b.remaining;
264 return a.t_arr > b.t_arr;
266 if (a.priority != b.priority) return a.priority > b.priority;
267 if (a.remaining != b.remaining) return a.remaining > b.remaining;
268 return a.t_arr > b.t_arr;
270 if (a.remaining != b.remaining) return a.remaining < b.remaining;
271 return a.service < b.service;
273 // The ORIGINAL requirement, not the residual: a job that has
274 // already been served keeps the rank its full size gave it.
275 if (a.service != b.service) return a.service > b.service;
276 return a.t_arr > b.t_arr;
279 // Least attained service first. FB and SETF share this order;
280 // they differ in WHEN the running job is interrupted, not in
281 // how the waiting ones are ranked.
282 if (a.elapsed != b.elapsed) return a.elapsed > b.elapsed;
283 return a.t_arr > b.t_arr;
285 if (a.vft != b.vft) return a.vft > b.vft;
286 return a.t_arr > b.t_arr;
287 default: // FCFS and anything ordered like it
288 return a.t_arr > b.t_arr;
289 }
290 }
291
292private:
293 double mean_of(const Job& j) const {
294 if (class_mean == 0 || j.cls >= class_mean->size()) return 0.0;
295 return (*class_mean)[j.cls];
296 }
297};
298
299/** A job being served by a sharing discipline, with the work it still owes. */
300struct PsJob {
301 std::size_t cls = 0;
302 int priority = 0;
303 double t_arr = 0.0;
304 double region_wait = 0.0; ///< Job::region_wait, carried across the sharing station
305 double t_sys = 0.0;
306 double total = 0.0; ///< the sampled requirement
307 double remaining = 0.0; ///< residual work, in service-time units
308 std::uint64_t tag = 0; ///< identifies the departure event scheduled for it
309 /**
310 * The fork the job belongs to, carried across the sharing station.
311 *
312 * A buffered station keeps the whole `Job` in its server slot and hands it
313 * back at the completion, so the identity survives; a sharing station holds
314 * this reduced record instead and would hand back a DEFAULT-CONSTRUCTED one.
315 * A sibling leaving with parent 0 matches no fork at the Join and is
316 * discarded there, which drains a closed fork-join model after one pass.
317 */
318 std::uint64_t parent = 0;
319};
320
321/**
322 * The per-job shares of a sharing discipline, in units of one server.
323 *
324 * `c` is the station's effective server count: with n <= c every job runs at
325 * rate 1 and the station is an infinite server; above that the capacity is
326 * split. Each rule is capped at 1 because ONE job cannot consume more than ONE
327 * server however the weights fall -- dropping that cap is what makes a
328 * two-class DPS station report a utilization above one.
329 *
330 * Transcribed from `calculatePSRates` and the five rules under it.
331 */
332inline std::vector<double> ps_shares(lang::SchedStrategy sched, const std::vector<PsJob>& jobs,
333 double c, const std::vector<double>& weight,
334 std::size_t nclasses) {
335 const std::size_t n = jobs.size();
336 std::vector<double> rates(n, 0.0);
337 if (n == 0) return rates;
338
339 const bool prio = (sched == lang::SchedStrategy::PSPRIO ||
342 const bool weighted_by_job = (sched == lang::SchedStrategy::DPS ||
344 const bool weighted_by_class = (sched == lang::SchedStrategy::GPS ||
346
347 if (!prio) {
348 if (weighted_by_job) {
349 double tot = 0.0;
350 for (const PsJob& j : jobs) tot += weight[j.cls];
351 if (tot > 0.0) {
352 for (std::size_t i = 0; i < n; ++i)
353 rates[i] = std::min(1.0, weight[jobs[i].cls] * c / tot);
354 return rates;
355 }
356 } else if (weighted_by_class) {
357 std::vector<int> per_class(nclasses, 0);
358 for (const PsJob& j : jobs) ++per_class[j.cls];
359 double tot = 0.0;
360 for (std::size_t k = 0; k < nclasses; ++k)
361 if (per_class[k] > 0) tot += weight[k];
362 if (tot > 0.0) {
363 for (std::size_t i = 0; i < n; ++i) {
364 const std::size_t k = jobs[i].cls;
365 rates[i] = std::min(1.0, (weight[k] / tot / per_class[k]) * c);
366 }
367 return rates;
368 }
369 }
370 // PS, LPS, and the degenerate all-zero-weight case: equal shares.
371 const double share = std::min(1.0, c / static_cast<double>(n));
372 for (std::size_t i = 0; i < n; ++i) rates[i] = share;
373 return rates;
374 }
375
376 // The priority variants give the whole station to the most urgent class
377 // present, and only what is left over to the next -- so an uncongested
378 // station (n <= c) has nothing to arbitrate and every job runs at 1.
379 if (static_cast<double>(n) <= c) {
380 for (std::size_t i = 0; i < n; ++i) rates[i] = 1.0;
381 return rates;
382 }
383 std::set<int> prios;
384 for (const PsJob& j : jobs) prios.insert(j.priority);
385 double remaining = c;
386 for (std::set<int>::const_iterator it = prios.begin(); it != prios.end(); ++it) {
387 if (remaining <= 0.0) break;
388 const int p = *it;
389 int count = 0;
390 double tot_job_weight = 0.0;
391 std::vector<int> per_class(nclasses, 0);
392 for (const PsJob& j : jobs)
393 if (j.priority == p) {
394 ++count;
395 tot_job_weight += weight[j.cls];
396 ++per_class[j.cls];
397 }
398 const double allocated = std::min(remaining, static_cast<double>(count));
399 double tot_class_weight = 0.0;
400 for (std::size_t k = 0; k < nclasses; ++k)
401 if (per_class[k] > 0) tot_class_weight += weight[k];
402
403 for (std::size_t i = 0; i < n; ++i) {
404 if (jobs[i].priority != p) continue;
405 const std::size_t k = jobs[i].cls;
406 if (sched == lang::SchedStrategy::DPSPRIO && tot_job_weight > 0.0)
407 rates[i] = std::min(1.0, weight[k] * allocated / tot_job_weight);
408 else if (sched == lang::SchedStrategy::GPSPRIO && tot_class_weight > 0.0)
409 rates[i] = std::min(1.0, (weight[k] / tot_class_weight / per_class[k]) * allocated);
410 else
411 rates[i] = allocated / count;
412 }
413 remaining -= allocated;
414 }
415 return rates;
416}
417
418/**
419 * FSP's virtual finish time: when `target` would finish if the station ran
420 * processor sharing from now on.
421 *
422 * Sort every residual work at the station, then walk it: while n jobs remain
423 * each advances at min(1, c/n), so the wall time to clear the k-th smallest
424 * residual is the sum of (w_k - w_{k-1}) / rate over the ranks below it. FSP
425 * then serves in that order, which is what makes it dominate PS job by job.
426 * Transcribes `computeFSPVirtualFinishTime`.
427 */
428inline double fsp_virtual_finish(const std::vector<double>& residuals, double target_work,
429 double c, double now) {
430 std::vector<double> works = residuals;
431 std::sort(works.begin(), works.end());
432 const std::size_t n = works.size();
433 double cumulative = 0.0, prev = 0.0;
434 for (std::size_t rank = 0; rank < n; ++rank) {
435 const double w = works[rank];
436 const double rate = std::min(1.0, c / static_cast<double>(n - rank));
437 if (rate > 0.0) cumulative += (w - prev) / rate;
438 if (w >= target_work) return now + cumulative;
439 prev = w;
440 }
441 return now + cumulative;
442}
443
444/**
445 * Which job in service `arriving` displaces, or `in_service.size()` for none.
446 *
447 * ONE RULE PER FAMILY, transcribed from `shouldPreemptForLCFS`,
448 * `shouldPreemptForFCFS` and `shouldPreemptForSizeBasedPolicy` together with
449 * their `findJobToPreempt*` counterparts. The two halves are fused here because
450 * they ask the same question twice in the reference -- once to decide and once
451 * to choose -- and answering it once cannot disagree with itself.
452 *
453 * THE PRIORITY VARIANTS PREEMPT ONLY STRICTLY LOWER PRIORITY, AND ONLY THEY DO.
454 * There are FOUR distinct tests here, not one:
455 * - `LCFSPR`/`LCFSPI` preempt UNCONDITIONALLY. Last-come-first-served keeps the
456 * NEWEST job in service, so the newcomer takes the head with no class ordering.
457 * - `LCFSPRPRIO`/`LCFSPIPRIO` preempt a job whose priority value is AT LEAST the
458 * arrival's.
459 * - `FCFSPRPRIO`/`FCFSPIPRIO` preempt only one STRICTLY GREATER.
460 * - `FCFSPR`/`FCFSPI` preempt NOTHING AT ALL. Preemption under the FCFS families
461 * is a PRIORITY act, so it belongs to the PRIO names alone and only ACROSS
462 * priority groups: the base rule serves in ARRIVAL ORDER, and a plain variant
463 * is absent from the model's priority-aware set (the user is warned that
464 * "Priorities will be ignored"), so every class sits in ONE group and the
465 * arrival can only WAIT. Reading `priority` here anyway -- which this function
466 * did until 2026-09-12, sharing an arm with the PRIO pair -- infers a policy
467 * the model never declared; it is invisible for flat priorities and silently
468 * turns a declared-FCFS station into a preemptive-priority one otherwise.
469 * Collapsing those four tests into fewer turns a priority station into a
470 * thrashing one, or into a non-preemptive one, depending which way it falls.
471 */
472inline std::size_t preemption_victim(lang::SchedStrategy sched, const std::vector<Job>& in_service,
473 const Job& arriving, double c, double now) {
474 const std::size_t none = in_service.size();
475 if (in_service.empty()) return none;
476
477 switch (sched) {
480 // Displace the OLDEST job in service: the newcomer takes the head
481 // of a last-come-first-served station unconditionally.
482 std::size_t best = none;
483 double oldest = std::numeric_limits<double>::infinity();
484 for (std::size_t j = 0; j < in_service.size(); ++j)
485 if (in_service[j].t_arr < oldest) {
486 oldest = in_service[j].t_arr;
487 best = j;
488 }
489 return best;
490 }
493 std::size_t best = none;
494 double oldest = std::numeric_limits<double>::infinity();
495 for (std::size_t j = 0; j < in_service.size(); ++j)
496 if (in_service[j].priority >= arriving.priority && in_service[j].t_arr < oldest) {
497 oldest = in_service[j].t_arr;
498 best = j;
499 }
500 return best;
501 }
504 // NOTHING. An FCFS-PR/PI arrival never preempts: the base rule serves
505 // in ARRIVAL ORDER and the plain variants declare no priorities, so
506 // every class is in one group and the arrival can only wait.
507 return none;
510 // The least urgent job in service, tie-broken by the LATEST service
511 // start: among equals, the one that has invested least is displaced.
512 std::size_t best = none;
513 for (std::size_t j = 0; j < in_service.size(); ++j) {
514 if (in_service[j].priority <= arriving.priority) continue;
515 if (best == none || in_service[j].priority > in_service[best].priority ||
516 (in_service[j].priority == in_service[best].priority &&
517 in_service[j].t_arr > in_service[best].t_arr))
518 best = j;
519 }
520 return best;
521 }
524 std::size_t worst = 0;
525 for (std::size_t j = 1; j < in_service.size(); ++j)
526 if (in_service[j].remaining > in_service[worst].remaining) worst = j;
527 if (sched == lang::SchedStrategy::SRPTPRIO &&
528 in_service[worst].priority < arriving.priority)
529 return none;
530 return (arriving.remaining < in_service[worst].remaining) ? worst : none;
531 }
533 std::size_t largest = 0;
534 for (std::size_t j = 1; j < in_service.size(); ++j)
535 if (in_service[j].service > in_service[largest].service) largest = j;
536 return (arriving.service < in_service[largest].service) ? largest : none;
537 }
540 // A fresh arrival has attained nothing, so it displaces whichever
541 // job has attained the most -- provided that is more than nothing.
542 std::size_t most = 0;
543 for (std::size_t j = 1; j < in_service.size(); ++j)
544 if (in_service[j].elapsed > in_service[most].elapsed) most = j;
545 return (in_service[most].elapsed > arriving.elapsed) ? most : none;
546 }
548 std::size_t least = 0;
549 for (std::size_t j = 1; j < in_service.size(); ++j)
550 if (in_service[j].remaining < in_service[least].remaining) least = j;
551 return (arriving.remaining > in_service[least].remaining) ? least : none;
552 }
554 std::vector<double> works;
555 for (const Job& j : in_service) works.push_back(j.remaining);
556 works.push_back(arriving.remaining);
557 const double arrival_vft = fsp_virtual_finish(works, arriving.remaining, c, now);
558 std::size_t worst = 0;
559 double worst_vft = -std::numeric_limits<double>::infinity();
560 for (std::size_t j = 0; j < in_service.size(); ++j) {
561 const double v = fsp_virtual_finish(works, in_service[j].remaining, c, now);
562 if (v > worst_vft) {
563 worst_vft = v;
564 worst = j;
565 }
566 }
567 return (arrival_vft < worst_vft) ? worst : none;
568 }
569 default:
570 return none;
571 }
572}
573
574} // namespace engine
575} // namespace ldes
576} // namespace line
577
578#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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A job held by a station.
double deadline
EDD/EDF key.
double region_wait
Time spent parked outside a WAITQ region before entering this station.
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
std::uint64_t tag
The generation its currently scheduled completion was pushed under.
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 region_wait
Job::region_wait, carried across the sharing station.
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