LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_ln_engine.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_LN_ENGINE_H
6#define LINE_SOLVERS_LDES_LDES_LN_ENGINE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The NATIVE LDES engine for LAYERED (LQN) models, the C++ twin of
12 * `jline/solvers/ldes/handlers/Solver_ssj_ln.java`.
13 *
14 * IT IS A DIFFERENT SIMULATOR FROM THE FLAT ONE, not a wrapper around it. A
15 * layered model has no jobs circulating over a routing matrix: it has
16 * REFERENCE TASKS that think and then invoke an entry, ACTIVITIES that consume
17 * host demand, and CALLS that suspend the caller until the callee replies. The
18 * decomposition into layers that `SolverLN` performs is an APPROXIMATION; this
19 * engine simulates the layered semantics directly and is therefore the
20 * reference the layered solvers are checked against.
21 *
22 * THE TWO RESOURCES ARE HELD AT ONCE, and that is the whole content of a
23 * layered model. An activity needs a THREAD of its task and a SERVER of its
24 * host, and it holds the thread across a synchronous call while the callee runs
25 * on a different host entirely. Releasing the thread during the call would turn
26 * every synchronous call into an asynchronous one and remove the layered
27 * contention the model exists to represent -- the answer stays plausible and
28 * every utilization falls.
29 *
30 * WHAT IT COVERS: reference tasks with think time and multiplicity; task
31 * multiplicity as a thread semaphore, with setup/delay-off power cycling;
32 * host demand on c-server processors under FCFS, LCFS, SIRO, HOL, FCFSPRIO,
33 * LCFSPRIO, FCFSPRPRIO, FCFSPIPRIO, LCFSPRPRIO, LCFSPIPRIO, PS, PSPRIO and INF,
34 * and task queueing under FCFS, LCFS, SIRO, HOL, FCFSPRIO, LCFSPRIO and INF,
35 * each served as Solver_ssj_ln serves it. The priority disciplines order by
36 * `lsn.prio` on the lqns scale, a LARGER number first and the default 0 lowest;
37 * HOL and FCFSPRIO are FIFO within a level, the LCFS variants LIFO; the PR/PI
38 * variants preempt, resuming the residual work (PR) or drawing a fresh demand
39 * (PI); PS is an egalitarian share min(1, c/n) of the c servers and PSPRIO
40 * gives the servers to the levels from the top down; INF never queues, whatever
41 * the multiplicity. Also synchronous calls with a mean call multiplicity,
42 * asynchronous calls and open arrivals (each a request nobody waits on),
43 * activity think times (after the demand, before the calls, holding the thread
44 * but not the processor), REPLICATION of a processor or a task, one queue per
45 * copy, and, as `Solver_ssj_ln` runs them:
46 *
47 * - ACTIVITY GRAPHS walked as graphs, not as a list. An entry starts at its
48 * bound activity and follows `graph`: one successor continues, an OR fork
49 * or a loop exit draws one branch, an AND fork splits the invocation into
50 * concurrent LINES sharing its thread, and an AND join fires on the root
51 * line once every PRE_AND input has arrived. A phase-1 activity followed by
52 * phase 2 replies early, and the thread is held through phase 2.
53 * - CACHE TASKS: one content per task replica, an ItemEntry read drawing an
54 * item from its pmf and continuing on the hit or the miss branch, the
55 * replacement rules RR/FIFO/SFIFO/LRU/HLRU/CLIMB/QLRU over the level lists,
56 * and delayed-hit retrieval (a read of an in-flight item is parked until
57 * the fetcher replies, then continues on the hit branch).
58 * - ADMISSION CONSTRAINTS A n <= b: a host row blocks a host request outside
59 * the processor (thread kept, residence running), a task row blocks a
60 * request before the task (counted nowhere), FIFO release on a departure,
61 * and a run that stops with a blocked request is reported as a deadlock.
62 * - HETEROGENEOUS SERVER POOLS on a processor, served as OI rate sharing: each
63 * pool puts its whole capacity count*rate on the first held job it serves.
64 *
65 * An activity runs its host demand FIRST, then its think time, then its calls,
66 * as LQN2QN builds it and Solver_ssj_ln runs it. An entry's response time is
67 * its service time, from taking a thread of its task to its reply: the wait for
68 * the thread belongs to the caller's call. Its occupancy runs from the same
69 * instant to the end of its last phase.
70 *
71 * WHAT IT REFUSES by name: every discipline the Java engine refuses (PS or
72 * preemption on a task, which holds threads and does not divide or preempt
73 * them, and any weighted discipline anywhere), a setup on an infinite-thread
74 * task, server pools on a task, a partial AND-join quorum (the Java engine waits
75 * for every input), and a cache read whose ItemEntry has no popularity or no
76 * hit/miss pair.
77 *
78 * WHAT IT STILL DOES NOT SIMULATE, silently, as before: forwarding.
79 */
80
81#include <algorithm>
82#include <cctype>
83#include <cmath>
84#include <cstddef>
85#include <cstdint>
86#include <deque>
87#include <functional>
88#include <limits>
89#include <list>
90#include <map>
91#include <queue>
92#include <set>
93#include <string>
94#include <vector>
95
99#include "line/util/error.h"
100#include "line/util/matrix.h"
101
102namespace line {
103namespace ldes {
104namespace engine {
105
106/** One scheduled event of the layered engine. */
107struct LnEvent {
108 double t = 0.0;
109 /**
110 * 0 = host completion (`who` = the LINE), 1 = think completion (`who` = the
111 * reference customer), 2 = a task thread finished its cold start, 3 = an
112 * idle task thread's delay-off countdown expired, 4 = the next completion
113 * on a pooled host replica (`who` = the host slot), 5 = the next completion
114 * on a PS host replica (`who` = the host slot), 6 = line `who` finished the
115 * think time of its activity, 7 = an open arrival at entry `who` on replica
116 * `aux` of its task. A kind-0 event carries the line's host generation, so a
117 * preemption makes it stale.
118 *
119 * For kinds 2 and 3 `who` is the task SLOT and `aux` the thread within it,
120 * and `gen` is the generation the timer was armed with: a countdown caught
121 * by an arriving request is cancelled by bumping the generation, so the
122 * event still fires but finds itself stale and does nothing. Kinds 4 and 5 use the
123 * same device per host slot, since every change of the job set moves the
124 * next completion. The event queue has no removal.
125 */
126 int kind = 0;
127 std::size_t who = 0;
128 std::size_t aux = 0;
129 std::uint64_t gen = 0;
130 std::uint64_t seq = 0;
131};
132
134 bool operator()(const LnEvent& a, const LnEvent& b) const {
135 if (a.t != b.t) return a.t > b.t;
136 if (a.kind != b.kind) return a.kind > b.kind;
137 return a.seq > b.seq;
138 }
139};
140
141/** Index meaning "none" for a line, an invocation or a customer. */
142static const std::size_t LN_NONE = static_cast<std::size_t>(-1);
143
144/**
145 * One LINE of execution: a point in the activity graph of one invocation.
146 *
147 * A sequential invocation has one line, its root. An AND fork gives it one line
148 * per branch, all holding the SAME thread of the invocation, and the join
149 * resumes the root; that is `LNRequest.copyWith` of `Solver_ssj_ln`.
150 */
151struct LnLine {
152 std::size_t inv = LN_NONE; ///< the invocation this line belongs to
153 std::size_t act = 0; ///< current activity, 0 for an entry with none
154 /**
155 * When the current activity began, -1 before it is reached. Its span runs
156 * to its host completion, so it CARRIES THE NESTED CALLS it makes.
157 */
158 double act_t0 = -1.0;
159 std::size_t call_pos = 0; ///< next call index of the current activity
160 /**
161 * Repetitions of the current call still to be made; `NOTDRAWN` until drawn.
162 * `calls-mean` is drawn once when the call is first reached, so a call of
163 * mean 3 blocks three times per execution of the activity.
164 */
165 std::size_t calls_left = static_cast<std::size_t>(-1);
166 double host_t0 = 0.0; ///< when the line asked its processor, queueing included
167 bool branch = false; ///< a line an AND fork created (never the root)
168 int hcol = -1; ///< host admission column held, -1 = none
169 int pcol = -1; ///< operand column at a pooled host
170 double remaining = 0.0; ///< work left at a pooled or PS host
171 double siro_key = 0.0; ///< uniform key in a SIRO host queue; the smallest is served next
172 /**
173 * How far the current activity is before its calls: 0 nothing started, 1 host demand started
174 * or none, 2 think time started or none.
175 */
176 int dem_done = 0;
177 /**
178 * Host service in progress at a queueing host. The completion event carries `host_gen`, drawn
179 * from one engine-wide counter because lines are recycled, so a preemption cancels it by
180 * drawing a new one. `host_rem` is the work left when service (re)starts, negative meaning
181 * "draw the demand": a fresh request or a PI victim.
182 */
183 std::uint64_t host_gen = 0;
184 double host_rem = -1.0;
185 double host_start = 0.0;
186 double host_end = 0.0;
187};
188
189/** One invocation of an entry: what a call or a reference cycle starts. */
190struct LnInv {
191 std::size_t entry = 0, task = 0;
192 std::size_t trep = 0; ///< replica of the task serving it
193 std::size_t ts = 0; ///< task slot of that replica
194 std::size_t th = static_cast<std::size_t>(-1); ///< thread held, NOTHR on an INF task
195 bool has_thread = false;
196 bool started = false; ///< the entry's service has begun (a thread is held)
197 double entry_t0 = 0.0; ///< when the entry took its thread, the start of its service
198 std::size_t caller = LN_NONE; ///< the line blocked on this call, if synchronous
199 std::size_t cust = LN_NONE; ///< the reference customer, at the top level only
200 std::size_t root = LN_NONE; ///< root line
201 bool replied = false;
202 bool early_reply = false; ///< replied before completing (phase 2 follows)
203 long pending = -1; ///< live lines after a fork, -1 = never forked
204 std::map<std::size_t, std::set<std::size_t> > joins; ///< join activity -> inputs arrived
205 int tcol = -1; ///< task admission column held, -1 = none
206 bool fetching = false; ///< this read is fetching an item (retrieval)
207 std::size_t fetch_cache = 0, fetch_item = 0;
208 double siro_key = 0.0; ///< uniform key in a SIRO thread queue
209};
210
211/** A reference-task customer: it thinks, then starts an invocation. */
213 std::size_t ref_task = 0;
214 std::size_t repl = 0; ///< replica of the reference task it belongs to
215 double t_start = 0.0; ///< when the current cycle began
216};
217
218/** Live content of one replica of a cache task, `LNCacheState` of the Java engine. */
220 std::size_t nitems = 0;
221 std::vector<int> cap; ///< per level list
223 std::vector<std::list<std::size_t> > levels; ///< per level, most recent first
224 bool retrieval = false;
225 std::vector<char> inflight; ///< [item] a fetch is in progress
226 std::vector<std::vector<std::pair<std::size_t, std::size_t> > > held; ///< (line, access)
227 long long reads = 0, hits = 0, misses = 0, delayed = 0;
228};
229
230/** One ItemEntry read: its driver activity, its item law and its two branches. */
232 std::vector<std::size_t> caches; ///< cache state per replica of the cache task
233 std::vector<double> cdf;
234 std::size_t hit = 0, miss = 0, entry = 0;
235};
236
237/** Thread index meaning "no thread held", also used for an infinite-thread task. */
238static const std::size_t NOTHR = static_cast<std::size_t>(-1);
239
240/** `LnJob::calls_left` before the repetition count of a call has been drawn. */
241static const std::size_t NOTDRAWN = static_cast<std::size_t>(-1);
242
243/** Power state of one task thread, mirroring Solver_ssj_ln's THREAD_* constants. */
245
246/** The layered result: per element, the mean measures. */
247struct LnResult {
248 std::size_t nidx = 0;
250 /**
251 * Residence time per element, the ResidT column: the time an activity holds
252 * ITS HOST PROCESSOR per visit of the request stream driving its task, and
253 * for a task the sum over its activities.
254 *
255 * NOT the response time RLN, which also carries the nested synchronous
256 * calls the activity makes. NaN at the element kinds that have no residence
257 * -- processors and entries -- because a zero residence is a claim (nothing
258 * is held there) and this is the absence of one, the same convention
259 * SolverLN and LQNS report.
260 */
262 /**
263 * Every per-request ENTRY response time observed, one vector per entry in
264 * LOCAL index space (0..nentries-1). `RLN` at the entry indices is their
265 * mean; these are the observations themselves, which is what an empirical
266 * CDF has to be built from -- a law fitted to the mean says nothing about
267 * the tail, and the tail is the reason to ask a simulator at all.
268 */
269 std::vector<std::vector<double> > entry_resp_samples;
270 double simulated_time = 0.0;
271 long long completions = 0;
272 /**
273 * Cache measures, one row per ItemEntry that was read, pooled over the
274 * replicas of its cache task as `Solver_ssj_ln` pools them: element indices
275 * of the task and of the entry, the hit, miss and delayed-hit probabilities
276 * per read, and the read rate. Hit + miss + delayed = 1.
277 */
278 std::vector<std::size_t> cache_task, cache_entry;
280};
281
282/** One entry's empirical response-time CDF: F[j] = P(R <= t[j]). */
284 std::vector<double> t;
285 std::vector<double> F;
286};
287
288/**
289 * The empirical response time CDF of every ENTRY, the `getCdfRespTLN` of the
290 * other codebases: one [F(t), t] table per entry in the LOCAL index space,
291 * empty where the run observed nothing.
292 *
293 * The ecdf of each entry's observations: sort, step by 1/n, and collapse ties
294 * keeping the LARGEST value at each distinct time, so an interpolating reader
295 * cannot land on a multivalued point.
296 */
297inline std::vector<LnEntryCdf> ldes_ln_cdf_respt(const LnResult& r, std::size_t nentries) {
298 std::vector<LnEntryCdf> out(nentries);
299 for (std::size_t e = 0; e < nentries; ++e) {
300 if (e >= r.entry_resp_samples.size()) continue;
301 std::vector<double> x = r.entry_resp_samples[e];
302 if (x.empty()) continue;
303 std::sort(x.begin(), x.end());
304 const double n = static_cast<double>(x.size());
305 for (std::size_t i = 0; i < x.size(); ++i) {
306 if (i + 1 < x.size() && x[i + 1] == x[i]) continue;
307 out[e].t.push_back(x[i]);
308 out[e].F.push_back(static_cast<double>(i + 1) / n);
309 }
310 }
311 return out;
312}
313
314/** A column of the shared layered average table, as `line-cli` prints it. */
315enum class LnColumn { QLen, Util, RespT, ResidT, Tput };
316
317/**
318 * Does the element at `i` HAVE the quantity in column `c`?
319 *
320 * THE MASK BELONGS TO THE TABLE, not to whichever engine filled it. A NaN there
321 * is not a failed computation: it says the quantity is not defined for that
322 * element kind, and SolverLN and LQNS report the same one --
323 *
324 * Processor QLen no, Util yes, RespT no, ResidT no, Tput no
325 * Task QLen yes, Util yes, RespT no, ResidT yes, Tput yes
326 * Entry QLen yes, Util yes, RespT yes, ResidT no, Tput yes
327 * Activity all five
328 *
329 * -- so a reader can diff the arms row by row. This engine MEASURES more than
330 * that: a processor's completion rate is sitting in `TLN` and an entry's
331 * occupancy in `QLN`, both perfectly well defined as sample-path quantities.
332 * Reporting them where every other solver says NaN would make one column mean
333 * different things depending on who filled it, which is the divergence the JAR
334 * removed from `getLNAvgTable` on 2026-08-21. Nothing is discarded: the caller
335 * still has the whole `LnResult`; only the SHARED TABLE is masked.
336 *
337 * ResidT needs no rule of its own here because the engine already applies it:
338 * `WLN` starts at NaN and only activities and their tasks are written, so a
339 * task with no activity at all keeps its NaN rather than claiming a residence
340 * of zero (which would say something is held there for no time, not that
341 * nothing is held).
342 *
343 * Twin of the mask `jline.solvers.ldes.SolverLDES.getLNAvgTable` applies and of
344 * the `defined_*` flags of `ln::LnSolution`; the three are pinned against each
345 * other by `cpp/tests/test_lqn_nan_mask_parity.cpp`.
346 */
347template <class T>
348inline bool ln_defined(const lqn::LqnStruct<T>& lsn, const LnResult& r, std::size_t i,
349 LnColumn c) {
350 if (i < 1 || i > r.nidx || i >= lsn.type.size()) return false;
351 const bool host = lsn.type[i] == line::lang::LqnElement::HOST;
352 switch (c) {
353 case LnColumn::QLen: return !host;
354 case LnColumn::Util: return true;
355 case LnColumn::RespT:
356 return lsn.type[i] == line::lang::LqnElement::ENTRY ||
358 case LnColumn::ResidT: return !std::isnan(r.WLN(i, 0));
359 case LnColumn::Tput: return !host;
360 }
361 return false;
362}
363
364} // namespace engine
365
366/**
367 * Simulate a layered model in process.
368 *
369 * The budget is ENTRY COMPLETIONS, mirroring the flat engine's service
370 * completions: a layered model has no single notion of "a job leaving", so the
371 * count of entry invocations that returned is what a horizon can be set on.
372 */
373template <class T>
375 using namespace engine;
377
378 const std::size_t nidx = lsn.nidx;
379 if (nidx == 0) throw InputError("SolverLDES (native LN engine): empty layered model");
380
381 // ---- refuse what the engine does not simulate ---------------------------
382 // The disciplines Solver_ssj_ln.assertSchedulingIsSupported admits, refused by name otherwise:
383 // a discipline quietly served as another returns plausible numbers for a model nobody described.
384 for (std::size_t k = 1; k <= nidx && k < lsn.sched.size(); ++k) {
385 const bool is_host = k > lsn.hshift && k <= lsn.hshift + lsn.nhosts;
386 const bool is_task = k > lsn.tshift && k <= lsn.tshift + lsn.ntasks;
387 if (!is_host && !is_task) continue;
388 const SchedStrategy sc = lsn.sched[k];
389 const bool ok = sc == SchedStrategy::FCFS || sc == SchedStrategy::INF ||
390 sc == SchedStrategy::LCFS || sc == SchedStrategy::SIRO ||
391 sc == SchedStrategy::HOL || sc == SchedStrategy::REF ||
392 sc == SchedStrategy::LCFSPRIO ||
393 (is_host && (sc == SchedStrategy::PS || sc == SchedStrategy::PSPRIO ||
394 sc == SchedStrategy::FCFSPRPRIO ||
395 sc == SchedStrategy::FCFSPIPRIO ||
396 sc == SchedStrategy::LCFSPRPRIO ||
397 sc == SchedStrategy::LCFSPIPRIO));
398 if (ok) continue;
399 std::string scname = lang::sched_to_text(sc);
400 for (std::size_t c = 0; c < scname.size(); ++c)
401 scname[c] = static_cast<char>(std::toupper(static_cast<unsigned char>(scname[c])));
402 throw UnsupportedError(
403 "SolverLDES (native LN engine) does not simulate " + scname + " scheduling at " +
404 (is_host ? "processor" : "task") + " '" + lsn.names[k] +
405 "'. The layered engine implements " +
406 (is_host ? "FCFS, LCFS, SIRO, HOL, FCFSPRIO, LCFSPRIO, FCFSPRPRIO, FCFSPIPRIO, "
407 "LCFSPRPRIO, LCFSPIPRIO, PS, PSPRIO and INF"
408 : "FCFS, LCFS, SIRO, HOL, FCFSPRIO, LCFSPRIO and INF (a task holds threads, "
409 "it does not divide or preempt them)") +
410 "; a weighted discipline would additionally need the per-task shares the layered "
411 "struct does not carry.");
412 }
413 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
414 // A setup IS simulated, except on an infinite-thread task, which holds
415 // no thread to power down. Same rule as the Java engine.
416 if (t < lsn.hassetup.size() && lsn.hassetup[t]) {
417 const double m = (t < lsn.mult.size()) ? lsn.mult[t] : 1.0;
418 if (!std::isfinite(m) || (t < lsn.sched.size() && lsn.sched[t] == lang::SchedStrategy::INF))
419 throw UnsupportedError("SolverLDES (native LN engine): task '" + lsn.names[t] +
420 "' declares a setup time on an infinite-server task, which "
421 "holds no thread to power down; give it a finite "
422 "multiplicity");
423 }
424 // Server pools on a TASK: the Java engine holds pools on processors only
425 // and throws here, so this does too rather than pick a reading of its own.
426 if (t < lsn.pools.size() && lsn.pools[t].npools() > 0)
427 throw UnsupportedError("SolverLDES (native LN engine): task '" + lsn.names[t] +
428 "' declares heterogeneous server pools; LDES holds server "
429 "pools on processors only");
430 }
431
432 const std::uint64_t max_events =
433 static_cast<std::uint64_t>(o.events > 0 ? o.events : o.samples);
434 const std::uint64_t base =
435 (o.seed >= 0) ? static_cast<std::uint64_t>(o.seed) : std::random_device{}();
436 /**
437 * ONE STREAM PER ACTIVITY, as the flat engine now does per (node, class).
438 *
439 * The layered reference indexes its generators by the LQN entity rather
440 * than by a station, so the offsets here are built the same way from the
441 * activity index: host demands in the service band (+1000) and think times
442 * in the arrival band, which is what those two are. Sharing one stream
443 * across every activity interleaves draws that the reference keeps apart,
444 * and the divergence is immediate rather than statistical.
445 */
446 const long long ln_seed = static_cast<long long>(base);
447 std::vector<Rng> g_host, g_think;
448 g_host.reserve(nidx + 1);
449 g_think.reserve(nidx + 1);
450 for (std::size_t k = 0; k <= nidx; ++k) {
451 g_host.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 1000));
452 g_think.push_back(Rng(ln_seed, static_cast<long long>(k) * 10));
453 }
454 Rng g_call(ln_seed, 900000);
455
456 // ---- resolved model -----------------------------------------------------
457 std::vector<Sampler> hostdem(nidx + 1), think(nidx + 1), actthink(nidx + 1);
458 std::vector<bool> has_hostdem(nidx + 1, false), has_think(nidx + 1, false),
459 has_actthink(nidx + 1, false);
460 // ACTIVITY think time (`Activity.setThinkTime`, .lqnx `think-time` on an activity): an
461 // off-processor delay after the demand and before the calls, on its own offset block (6000).
462 std::vector<Rng> g_actthink;
463 g_actthink.reserve(nidx + 1);
464 for (std::size_t k = 0; k <= nidx; ++k)
465 g_actthink.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 6000));
466 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a < lsn.actthink.size(); ++a) {
467 if (!lsn.actthink[a].disabled && num_traits<T>::to_double(lsn.actthink[a].mean) > 0.0) {
468 actthink[a] = Sampler(lsn.actthink[a], "the think time of activity '" + lsn.names[a] + "'");
469 has_actthink[a] = true;
470 }
471 }
472 for (std::size_t k = 1; k <= nidx; ++k) {
473 if (k < lsn.hostdem.size() && !lsn.hostdem[k].disabled &&
474 num_traits<T>::to_double(lsn.hostdem[k].mean) > 0.0) {
475 hostdem[k] = Sampler(lsn.hostdem[k], "the host demand of '" + lsn.names[k] + "'");
476 has_hostdem[k] = true;
477 }
478 if (k < lsn.think.size() && !lsn.think[k].disabled &&
479 num_traits<T>::to_double(lsn.think[k].mean) > 0.0) {
480 think[k] = Sampler(lsn.think[k], "the think time of '" + lsn.names[k] + "'");
481 has_think[k] = true;
482 }
483 }
484
485 // Host servers and task threads: the two resources an activity holds.
486 std::vector<std::size_t> host_servers(nidx + 1, 1), task_threads(nidx + 1, 1);
487 std::vector<SchedStrategy> host_sched(nidx + 1, SchedStrategy::FCFS);
488 std::vector<SchedStrategy> task_sched(nidx + 1, SchedStrategy::FCFS);
489 // An INF host or task never queues, whatever multiplicity it declares, which is how
490 // Solver_ssj_ln serves it (startHostService / enterTask branch on the discipline alone).
491 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h) {
492 const double m = (h < lsn.mult.size()) ? lsn.mult[h] : 1.0;
493 if (h < lsn.sched.size()) host_sched[h] = lsn.sched[h];
494 host_servers[h] = (std::isfinite(m) && host_sched[h] != SchedStrategy::INF)
495 ? std::max<std::size_t>(1, static_cast<std::size_t>(m + 0.5))
496 : std::numeric_limits<std::size_t>::max();
497 }
498 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
499 const double m = (t < lsn.mult.size()) ? lsn.mult[t] : 1.0;
500 if (t < lsn.sched.size()) task_sched[t] = lsn.sched[t];
501 task_threads[t] = (std::isfinite(m) && task_sched[t] != SchedStrategy::INF)
502 ? static_cast<std::size_t>(m + 0.5)
503 : std::numeric_limits<std::size_t>::max();
504 }
505
506 /**
507 * REPLICATION. A processor or task declared with replication r is r
508 * identical copies of itself, and a copy is a server of its own: pooling
509 * them into one server of r times the capacity would let one queue absorb
510 * what r separate queues cannot. So the host state below is indexed by a
511 * SLOT -- the processor plus its replica -- while what the copies share by
512 * definition (servers per copy, scheduling) stays indexed by the element.
513 * The reported measures are summed over the copies, which keeps throughput
514 * conserved across a call and utilization a fraction of the total capacity.
515 */
516 std::vector<std::size_t> repl(nidx + 1, 1);
517 for (std::size_t k = 1; k < lsn.repl.size() && k <= nidx; ++k) {
518 const double r = lsn.repl[k];
519 repl[k] = (r > 1.0) ? static_cast<std::size_t>(r + 0.5) : 1;
520 }
521 std::vector<std::size_t> host_slot0(nidx + 1, 0);
522 std::size_t nhost_slots = 0;
523 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h) {
524 host_slot0[h] = nhost_slots;
525 nhost_slots += repl[h];
526 }
527 if (nhost_slots == 0) nhost_slots = 1;
528
529 /** Slot of the processor replica running replica trep of one of its tasks. */
530 auto host_slot = [&](std::size_t host, std::size_t trep) {
531 return host_slot0[host] + (trep % repl[host]);
532 };
533
534 // Task replicas get their own slots for exactly the reason processors do: r
535 // copies are r thread pools, not one pool of r times the size.
536 std::vector<std::size_t> task_slot0(nidx + 1, 0);
537 std::size_t ntask_slots = 0;
538 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
539 task_slot0[t] = ntask_slots;
540 ntask_slots += repl[t];
541 }
542 if (ntask_slots == 0) ntask_slots = 1;
543 auto task_slot = [&](std::size_t task, std::size_t trep) {
544 return task_slot0[task] + (trep % repl[task]);
545 };
546
547 /**
548 * Replica of the callee reached by one call of replica `crep` of the caller.
549 * An unset fan-out is the smallest value consistent with
550 * repl(caller)*fanout = repl(callee)*fanin, and the caller reaches the block
551 * {(i*f+k) mod r}. A call is one indivisible unit of work, so it goes to one
552 * member of that block drawn uniformly: over many calls each member carries
553 * the 1/f share that LQN2QN splits the call mean into. This is deterministic
554 * pairing at f=1 and a uniform spread over every replica at f=r.
555 */
556 auto callee_replica = [&](std::size_t caller_task, std::size_t crep,
557 std::size_t callee_task) -> std::size_t {
558 const std::size_t rb = repl[callee_task];
559 if (rb <= 1) return 0;
560 std::size_t f = static_cast<std::size_t>(lsn.fanout_at(caller_task, callee_task) + 0.5);
561 if (f == 0) {
562 const std::size_t ra = repl[caller_task];
563 f = (rb > ra) ? std::max<std::size_t>(1, rb / ra) : 1;
564 }
565 f = std::min(std::max<std::size_t>(1, f), rb);
566 const std::size_t k =
567 (f > 1) ? static_cast<std::size_t>(uniform01(g_call) * static_cast<double>(f)) : 0;
568 return ((crep * f) + std::min(k, f - 1)) % rb;
569 };
570
571 // ---- the activity graph, as buildActivityGraph builds it -----------------
572 const std::size_t a_lo = lsn.ashift + 1, a_hi = lsn.ashift + lsn.nacts;
573 auto is_act = [&](std::size_t k) { return k >= a_lo && k <= a_hi; };
574 auto wgt = [&](std::size_t i, std::size_t j) {
575 return num_traits<T>::to_double(lsn.graph.get(i, j));
576 };
577 auto pretype = [&](std::size_t a) {
578 return a < lsn.actpretype.size() ? lsn.actpretype[a] : lang::PrecedenceType::NONE;
579 };
580 auto posttype = [&](std::size_t a) {
581 return a < lsn.actposttype.size() ? lsn.actposttype[a] : lang::PrecedenceType::NONE;
582 };
583 auto phase_of = [&](std::size_t a) -> int {
584 const std::size_t k = a - lsn.ashift;
585 return (k < lsn.actphase.size() && lsn.actphase[k] > 0) ? lsn.actphase[k] : 1;
586 };
587 // Successors of an activity are the ACTIVITIES it precedes, ascending: the
588 // element graph also carries its call edges, which are not precedence.
589 std::vector<std::vector<std::size_t> > succ_act(nidx + 1);
590 for (std::size_t a = a_lo; a <= a_hi; ++a)
591 for (std::size_t s : lsn.graph.succ(a))
592 if (is_act(s)) succ_act[a].push_back(s);
593 /**
594 * A multi-successor activity CHOOSES one branch when it is an OR fork or a
595 * loop decision (its own post type, the first test of `buildActivityGraph`)
596 * or when any branch weight is a probability (w != 1, the lazy test of
597 * `completeActivity`); otherwise it FORKS every branch. The weights are
598 * renormalised when they do not sum to one, and the draw is `u <= cum`.
599 */
600 std::vector<std::vector<double> > or_prob(nidx + 1);
601 for (std::size_t a = a_lo; a <= a_hi; ++a) {
602 const std::vector<std::size_t>& sc = succ_act[a];
603 if (sc.size() < 2) continue;
604 bool prob = posttype(a) == lang::PrecedenceType::POST_OR ||
605 posttype(a) == lang::PrecedenceType::POST_LOOP;
606 for (std::size_t s : sc) {
607 const double w = wgt(a, s);
608 if (w != 1.0 && w > 0.0) prob = true;
609 }
610 if (!prob) continue;
611 double tot = 0.0;
612 for (std::size_t s : sc) tot += wgt(a, s);
613 for (std::size_t s : sc) {
614 const double w = wgt(a, s);
615 or_prob[a].push_back((tot > 0.0 && tot != 1.0) ? w / tot : w);
616 }
617 }
618 // An AND join waits for as many inputs as there are PRE_AND activities whose
619 // first successor it is, joinRequiredCount of the Java engine.
620 std::vector<std::size_t> join_required(nidx + 1, 0);
621 for (std::size_t a = a_lo; a <= a_hi; ++a)
622 if (pretype(a) == lang::PrecedenceType::PRE_AND && !succ_act[a].empty())
623 ++join_required[succ_act[a][0]];
624 // The Java engine ignores a quorum and waits for every input. Waiting for
625 // all of them here would run a different system from the declared one, and
626 // running a quorum would diverge from the reference, so it is refused.
627 for (std::size_t a = a_lo; a <= a_hi; ++a) {
628 const std::size_t q = a < lsn.actquorum.size() ? lsn.actquorum[a] : 0;
629 if (q > 0 && join_required[a] > 0 && q < join_required[a])
630 throw UnsupportedError("SolverLDES (native LN engine): the AND join into '" +
631 lsn.names[a] + "' declares a quorum of " + std::to_string(q) +
632 " of " + std::to_string(join_required[a]) +
633 " inputs; LDES joins wait for every input");
634 }
635 /**
636 * The activity an entry starts at: the one the entry's own graph edge names,
637 * else the first predecessor-free sequential activity of its task, else the
638 * first activity, which is `findBoundActivityForEntry`. 0 means none.
639 */
640 std::vector<std::size_t> bound(nidx + 1, 0);
641 for (std::size_t e = lsn.eshift + 1; e <= lsn.eshift + lsn.nentries; ++e) {
642 const std::size_t t = lsn.parent[e];
643 const std::vector<std::size_t>& acts =
644 (t < lsn.actsof.size()) ? lsn.actsof[t] : std::vector<std::size_t>();
645 for (std::size_t a : acts)
646 if (wgt(e, a) > 0.0) {
647 bound[e] = a;
648 break;
649 }
650 if (bound[e] != 0 || acts.empty()) continue;
651 for (std::size_t a : acts) {
652 const lang::PrecedenceType pt = pretype(a);
654 bool haspred = false;
655 for (std::size_t o2 : acts)
656 if (o2 != a && wgt(o2, a) > 0.0) {
657 haspred = true;
658 break;
659 }
660 if (!haspred) {
661 bound[e] = a;
662 break;
663 }
664 }
665 if (bound[e] == 0) bound[e] = acts[0];
666 }
667
668 // ---- cache tasks: one content per replica, one access per ItemEntry -------
669 std::vector<LnCacheState> caches;
670 std::vector<std::size_t> cache_of_slot(ntask_slots, LN_NONE);
671 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
672 if (t >= lsn.iscache.size() || !lsn.iscache[t]) continue;
673 const std::size_t n = t < lsn.nitems.size() ? lsn.nitems[t] : 0;
674 if (n == 0) continue;
675 std::vector<int> cap = (t < lsn.itemcap.size()) ? lsn.itemcap[t] : std::vector<int>();
676 if (cap.empty()) cap.assign(1, 1);
677 for (std::size_t m = 0; m < repl[t]; ++m) {
678 LnCacheState cs;
679 cs.nitems = n;
680 cs.cap = cap;
681 cs.rs = (t < lsn.replacestrat.size()) ? lsn.replacestrat[t]
683 cs.levels.assign(cap.size(), std::list<std::size_t>());
684 cs.retrieval = t < lsn.hasretrieval.size() && lsn.hasretrieval[t];
685 if (cs.retrieval) {
686 cs.inflight.assign(n, 0);
687 cs.held.assign(n, {});
688 }
689 cache_of_slot[task_slot(t, m)] = caches.size();
690 caches.push_back(cs);
691 }
692 }
693 std::vector<LnCacheAccess> accesses;
694 std::vector<std::size_t> access_of_driver(nidx + 1, LN_NONE);
695 std::vector<std::size_t> branch_reply(nidx + 1, 0); ///< hit/miss activity -> its ItemEntry
696 for (std::size_t e = lsn.eshift + 1; e <= lsn.eshift + lsn.nentries; ++e) {
697 const std::size_t t = lsn.parent[e];
698 if (t <= lsn.tshift || t > lsn.tshift + lsn.ntasks) continue;
699 if (cache_of_slot[task_slot(t, 0)] == LN_NONE) continue;
700 const std::size_t drv = bound[e];
701 const bool is_item = e < lsn.itemproc.size() && !lsn.itemproc[e].empty();
702 // An entry of a cache task that is not an ItemEntry runs as an ordinary
703 // entry, as it does in the Java engine.
704 if (!is_item) continue;
705 if (drv == 0 || succ_act[drv].size() < 2)
706 throw UnsupportedError("SolverLDES (native LN engine): ItemEntry '" + lsn.names[e] +
707 "' has no cache access with a hit and a miss branch");
708 LnCacheAccess ca;
709 for (std::size_t m = 0; m < repl[t]; ++m) ca.caches.push_back(cache_of_slot[task_slot(t, m)]);
710 ca.hit = succ_act[drv][0];
711 ca.miss = succ_act[drv][1];
712 ca.entry = e;
713 std::size_t card = e < lsn.nitems.size() ? lsn.nitems[e] : 0;
714 if (card == 0) card = caches[ca.caches[0]].nitems;
715 double sum = 0.0;
716 for (std::size_t i = 0; i < card; ++i) {
717 const double v = (i < lsn.itemproc[e].size())
718 ? num_traits<T>::to_double(lsn.itemproc[e][i]) : 0.0;
719 if (std::isnan(v) || v < 0.0)
720 throw InputError("SolverLDES (native LN engine): ItemEntry '" + lsn.names[e] +
721 "' has a popularity that is not a pmf");
722 sum += v;
723 ca.cdf.push_back(sum);
724 }
725 if (!(sum > 0.0))
726 throw InputError("SolverLDES (native LN engine): ItemEntry '" + lsn.names[e] +
727 "' has a popularity of zero total mass");
728 for (double& c : ca.cdf) c /= sum;
729 access_of_driver[drv] = accesses.size();
730 branch_reply[ca.hit] = e;
731 branch_reply[ca.miss] = e;
732 accesses.push_back(ca);
733 }
734 // A POST_CACHE branch with no resolvable read would otherwise run as an AND
735 // fork of the hit AND the miss branch, which is not a cache at all.
736 for (std::size_t a = a_lo; a <= a_hi; ++a) {
737 if (access_of_driver[a] != LN_NONE) continue;
738 for (std::size_t s : succ_act[a])
739 if (posttype(s) == lang::PrecedenceType::POST_CACHE)
740 throw UnsupportedError("SolverLDES (native LN engine): activity '" + lsn.names[a] +
741 "' reads a cache, but no ItemEntry of a cache task with "
742 "a popularity is bound to it");
743 }
744
745 // ---- admission constraints A n <= b --------------------------------------
746 /**
747 * The rows are declared once per element and hold separately inside each
748 * replica, so A and b are per element and the occupancy per slot. A host's
749 * columns are its tasks (`tasksof`), a task's its entries (`entriesof`).
750 */
751 struct Lincon {
752 std::vector<std::vector<double> > A;
753 std::vector<double> b;
754 std::map<std::size_t, int> col;
755 };
756 std::vector<Lincon> lincon(nidx + 1);
757 std::vector<bool> has_lincon(nidx + 1, false);
758 for (std::size_t k = 1; k < lsn.lincon_A.size() && k <= nidx; ++k) {
759 const Matrix<T>& A = lsn.lincon_A[k];
760 if (A.rows() == 0) continue;
761 const bool host = k > lsn.hshift && k <= lsn.hshift + lsn.nhosts;
762 const std::vector<std::size_t>* ops = nullptr;
763 if (host && k < lsn.tasksof.size()) ops = &lsn.tasksof[k];
764 if (!host && k < lsn.entriesof.size()) ops = &lsn.entriesof[k];
765 const std::size_t ncols = ops ? ops->size() : 0;
766 if (A.cols() != ncols)
767 throw InputError("SolverLDES (native LN engine): the admission constraint on '" +
768 lsn.names[k] + "' has " + std::to_string(A.cols()) +
769 " columns but the server has " + std::to_string(ncols) + " operands");
770 if (ncols == 0) continue;
771 Lincon lc;
772 lc.A.assign(A.rows(), std::vector<double>(ncols, 0.0));
773 for (std::size_t r = 0; r < A.rows(); ++r)
774 for (std::size_t c = 0; c < ncols; ++c) lc.A[r][c] = num_traits<T>::to_double(A(r, c));
775 for (std::size_t r = 0; r < A.rows(); ++r)
776 lc.b.push_back((k < lsn.lincon_b.size() && r < lsn.lincon_b[k].size())
777 ? num_traits<T>::to_double(lsn.lincon_b[k][r])
778 : std::numeric_limits<double>::infinity());
779 for (std::size_t c = 0; c < ncols; ++c) lc.col[(*ops)[c]] = static_cast<int>(c);
780 lincon[k] = lc;
781 has_lincon[k] = true;
782 }
783 auto lincon_col = [&](std::size_t elem, std::size_t operand) -> int {
784 if (!has_lincon[elem]) return -1;
785 const std::map<std::size_t, int>::const_iterator it = lincon[elem].col.find(operand);
786 return it == lincon[elem].col.end() ? -1 : it->second;
787 };
788 /** True when one more job in column `col` still satisfies every row. */
789 auto admits = [&](std::size_t elem, const std::vector<int>& occ, int col) {
790 const Lincon& lc = lincon[elem];
791 for (std::size_t r = 0; r < lc.A.size(); ++r) {
792 double lhs = lc.A[r][static_cast<std::size_t>(col)];
793 for (std::size_t k = 0; k < occ.size(); ++k) lhs += lc.A[r][k] * occ[k];
794 if (lhs > lc.b[r] + 1e-9) return false;
795 }
796 return true;
797 };
798
799 // ---- heterogeneous server pools on processors ----------------------------
800 /**
801 * The pools ARE the servers of the host, so their counts must add up to its
802 * multiplicity. An INF host ignores them, as the Java engine's service path
803 * does. The operand column of a job is its task's position in `tasksof`.
804 */
805 std::vector<bool> pooled(nidx + 1, false);
806 std::vector<std::vector<double> > pool_capacity(nidx + 1);
807 std::vector<std::vector<std::vector<bool> > > pool_serves(nidx + 1);
808 std::vector<std::map<std::size_t, int> > pool_col(nidx + 1);
809 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h) {
810 if (h >= lsn.pools.size() || lsn.pools[h].npools() == 0) continue;
811 if (host_sched[h] == SchedStrategy::INF) continue; // the INF path ignores pools
812 const lqn::ServerPools<T>& P = lsn.pools[h];
813 const std::size_t np = P.npools(), nc = P.compat.cols();
814 std::size_t declared = 0;
815 for (std::size_t p = 0; p < np; ++p) {
816 const double cnt = (p < P.counts.size()) ? P.counts[p] : 0.0;
817 const std::size_t c = cnt > 0.0 ? static_cast<std::size_t>(cnt) : 0;
818 const double r = (p < P.rates.size()) ? num_traits<T>::to_double(P.rates[p]) : 1.0;
819 pool_capacity[h].push_back(static_cast<double>(c) * (r > 0.0 ? r : 1.0));
820 declared += c;
821 std::vector<bool> sv(nc, false);
822 for (std::size_t j = 0; j < nc; ++j)
823 sv[j] = num_traits<T>::to_double(P.compat(p, j)) != 0.0;
824 pool_serves[h].push_back(sv);
825 }
826 if (declared != host_servers[h])
827 throw InputError("SolverLDES (native LN engine): processor '" + lsn.names[h] +
828 "' declares server pools holding " + std::to_string(declared) +
829 " servers but has multiplicity " +
830 (host_servers[h] == std::numeric_limits<std::size_t>::max()
831 ? std::string("Inf") : std::to_string(host_servers[h])) +
832 "; the pools must partition the servers");
833 const std::vector<std::size_t>& ops =
834 (h < lsn.tasksof.size()) ? lsn.tasksof[h] : std::vector<std::size_t>();
835 for (std::size_t j = 0; j < ops.size() && j < nc; ++j) pool_col[h][ops[j]] = static_cast<int>(j);
836 pooled[h] = true;
837 }
838
839 // ---- live state ---------------------------------------------------------
840 double now = 0.0;
841 std::priority_queue<LnEvent, std::vector<LnEvent>, LnEventLater> evq;
842 std::uint64_t seq = 0;
843 // Lines and invocations live in deques, whose elements do not move when
844 // another is added, and are recycled through free lists.
845 std::deque<LnLine> lines;
846 std::deque<LnInv> invs;
847 std::vector<std::size_t> free_lines, free_invs;
848 std::vector<LnCustomer> custs;
849
850 // Busy servers and waiting lines of ONE processor replica, addressed by slot.
851 std::vector<std::size_t> host_busy(nhost_slots, 0);
852 std::vector<std::deque<std::size_t>> host_queue(nhost_slots);
853 // A pooled replica holds its lines in arrival order, the order the pools scan.
854 std::vector<std::vector<std::size_t> > pool_jobs(nhost_slots);
855 std::vector<double> pool_last(nhost_slots, 0.0), pool_rate(nhost_slots, 0.0);
856 std::vector<std::uint64_t> pool_gen(nhost_slots, 0);
857 std::vector<std::size_t> host_of_slot(nhost_slots, 0);
858 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h)
859 for (std::size_t m = 0; m < repl[h]; ++m) host_of_slot[host_slot0[h] + m] = h;
860 // Admission occupancy and blocked requests, per slot.
861 std::vector<std::vector<int> > hocc(nhost_slots), tocc(ntask_slots);
862 std::vector<std::deque<std::size_t> > hadm(nhost_slots), tadm(ntask_slots);
863 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h)
864 if (has_lincon[h])
865 for (std::size_t m = 0; m < repl[h]; ++m)
866 hocc[host_slot0[h] + m].assign(lincon[h].A[0].size(), 0);
867
868 // Task threads of ONE task replica, addressed by slot. An invocation that
869 // finds every thread taken waits in thr_queue, holding no resource meanwhile.
870 std::vector<std::vector<bool>> thr_held(ntask_slots);
871 std::vector<std::vector<ThreadState>> thr_state(ntask_slots);
872 std::vector<std::vector<double>> thr_setup_t0(ntask_slots);
873 std::vector<std::vector<std::uint64_t>> thr_gen(ntask_slots);
874 std::vector<std::deque<std::size_t>> thr_queue(ntask_slots);
875 std::vector<std::size_t> thr_owner(ntask_slots, 0); ///< task index of each slot
876 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
877 const std::size_t n = (task_threads[t] == std::numeric_limits<std::size_t>::max())
878 ? 0 : task_threads[t];
879 for (std::size_t m = 0; m < repl[t]; ++m) {
880 const std::size_t ts = task_slot(t, m);
881 thr_owner[ts] = t;
882 thr_held[ts].assign(n, false);
883 thr_state[ts].assign(n, ThreadState::ACTIVE);
884 thr_setup_t0[ts].assign(n, 0.0);
885 thr_gen[ts].assign(n, 0);
886 if (has_lincon[t]) tocc[ts].assign(lincon[t].A[0].size(), 0);
887 }
888 }
889
890 // A SetupTask's two clocks, per task. Both must be positive for the power
891 // cycle to exist: a setup with no delay-off would never fire, because nothing
892 // would ever power a thread down.
893 std::vector<Sampler> setupd(nidx + 1), delayoffd(nidx + 1);
894 std::vector<bool> has_setup(nidx + 1, false);
895 std::vector<Rng> g_setup, g_doff;
896 g_setup.reserve(nidx + 1);
897 g_doff.reserve(nidx + 1);
898 for (std::size_t k = 0; k <= nidx; ++k) {
899 g_setup.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 2000));
900 g_doff.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 3000));
901 }
902 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
903 if (t >= lsn.hassetup.size() || !lsn.hassetup[t]) continue;
904 const bool su = t < lsn.setuptime.size() && !lsn.setuptime[t].disabled &&
905 num_traits<T>::to_double(lsn.setuptime[t].mean) > 0.0;
906 const bool df = t < lsn.delayofftime.size() && !lsn.delayofftime[t].disabled &&
907 num_traits<T>::to_double(lsn.delayofftime[t].mean) > 0.0;
908 if (!su || !df) continue;
909 setupd[t] = Sampler(lsn.setuptime[t], "the setup time of '" + lsn.names[t] + "'");
910 delayoffd[t] = Sampler(lsn.delayofftime[t], "the delay-off time of '" + lsn.names[t] + "'");
911 has_setup[t] = true;
912 }
913
914 /**
915 * The new features draw on streams of their own, in blocks above every
916 * existing offset, so that a model without them keeps its seeded sample
917 * path: branch choices per activity (940000), item draws per cache replica
918 * (950000) and random replacement per cache replica (960000). 910000 and
919 * 920000 are the SIRO keys of hosts and tasks.
920 */
921 std::vector<Rng> g_route, g_item, g_rr;
922 g_route.reserve(nidx + 1);
923 for (std::size_t k = 0; k <= nidx; ++k)
924 g_route.push_back(Rng(ln_seed, 940000 + static_cast<long long>(k) * 10));
925 for (std::size_t c = 0; c < caches.size(); ++c) {
926 g_item.push_back(Rng(ln_seed, 950000 + static_cast<long long>(c) * 10));
927 g_rr.push_back(Rng(ln_seed, 960000 + static_cast<long long>(c) * 10));
928 }
929
930 /// Lines woken by a release, continued after the current walk returns.
931 /// Waking them inside the walk would re-enter it on another line.
932 std::deque<std::size_t> runnable;
933
934 /**
935 * PRIORITY ORDER, as Solver_ssj_ln orders it: a LARGER `lsn.prio` first (the lqns convention),
936 * FIFO within one level under HOL/FCFSPRIO and the FCFS preemptive variants, LIFO under the
937 * LCFS ones. A host request carries the priority of the task whose activity it is; a task
938 * request that of its CALLER, since every request at one task belongs to that task, and one
939 * with no caller (a reference task's own cycle, an open arrival, an asynchronous call) takes
940 * 0, the lowest.
941 */
942 auto task_prio = [&](std::size_t task) -> int {
943 return (task < lsn.prio.size()) ? lsn.prio[task] : 0;
944 };
945 auto host_req_prio = [&](std::size_t lid) -> int { return task_prio(lsn.parent[lines[lid].act]); };
946 auto task_req_prio = [&](std::size_t vi) -> int {
947 const std::size_t c = invs[vi].caller;
948 return c == LN_NONE ? 0 : task_prio(invs[lines[c].inv].task);
949 };
950 // SIRO keys, one stream per resource kind on the engine's own offsets, so a SIRO queue does
951 // not shift the demand, think or call variates of any other element.
952 Rng g_siro_host(ln_seed, 910000), g_siro_task(ln_seed, 920000);
953 auto fcfs_prio = [](SchedStrategy sc) {
954 // FCFSPRIO has no enumerator of its own here: it is MATLAB's alias for HOL.
955 return sc == SchedStrategy::HOL || sc == SchedStrategy::FCFSPRPRIO ||
956 sc == SchedStrategy::FCFSPIPRIO;
957 };
958 auto lcfs_prio = [](SchedStrategy sc) {
959 return sc == SchedStrategy::LCFSPRIO || sc == SchedStrategy::LCFSPRPRIO ||
960 sc == SchedStrategy::LCFSPIPRIO;
961 };
962 auto preemptive = [](SchedStrategy sc) {
963 return sc == SchedStrategy::FCFSPRPRIO || sc == SchedStrategy::FCFSPIPRIO ||
964 sc == SchedStrategy::LCFSPRPRIO || sc == SchedStrategy::LCFSPIPRIO;
965 };
966 auto is_ps = [](SchedStrategy sc) {
967 return sc == SchedStrategy::PS || sc == SchedStrategy::PSPRIO;
968 };
969 /**
970 * Queue `id` behind every request of a higher priority and, within its own level, behind the
971 * ones FIFO (or, `lifo`, LIFO) puts first. `key_of` is the arrival instant a level is ordered
972 * by; a task queue passes none and relies on requests joining it in arrival order.
973 */
974 auto prio_enqueue = [](std::deque<std::size_t>& q, std::size_t id,
975 const std::function<int(std::size_t)>& prio_of, bool lifo,
976 const std::function<double(std::size_t)>* key_of) {
977 const int prio = prio_of(id);
978 const double key = key_of ? (*key_of)(id) : 0.0;
979 std::deque<std::size_t>::iterator it = q.end();
980 while (it != q.begin()) {
981 const std::size_t o = *(it - 1);
982 const int po = prio_of(o);
983 if (po > prio) break;
984 if (po == prio) {
985 if (!lifo && (!key_of || (*key_of)(o) <= key)) break;
986 if (lifo && key_of && (*key_of)(o) > key) break;
987 }
988 --it;
989 }
990 q.insert(it, id);
991 };
992 /**
993 * Queue `id` under `sc`, the order Solver_ssj_ln's comparators impose: FCFS by arrival, LCFS
994 * newest first, SIRO by a uniform key drawn now, the priority disciplines by `prio` then
995 * arrival (`key_of`, the host residence start, when given).
996 */
997 auto enqueue = [&](std::deque<std::size_t>& q, std::size_t id, SchedStrategy sc,
998 const std::function<int(std::size_t)>& prio_of, double& key, Rng& g_siro,
999 const std::function<double(std::size_t)>* key_of) {
1000 if (fcfs_prio(sc) || lcfs_prio(sc)) {
1001 prio_enqueue(q, id, prio_of, lcfs_prio(sc), key_of);
1002 } else if (sc == SchedStrategy::LCFS) {
1003 q.push_front(id);
1004 } else {
1005 if (sc == SchedStrategy::SIRO) key = uniform01(g_siro);
1006 q.push_back(id);
1007 }
1008 };
1009 /** Take the request `sc` serves next: the head, or under SIRO the smallest key. */
1010 auto dequeue = [&](std::deque<std::size_t>& q, SchedStrategy sc,
1011 const std::function<double(std::size_t)>& key_of) -> std::size_t {
1012 std::deque<std::size_t>::iterator best = q.begin();
1013 if (sc == SchedStrategy::SIRO)
1014 for (std::deque<std::size_t>::iterator it = q.begin(); it != q.end(); ++it)
1015 if (key_of(*it) < key_of(*best)) best = it;
1016 const std::size_t id = *best;
1017 q.erase(best);
1018 return id;
1019 };
1020 const std::function<double(std::size_t)> line_key = [&](std::size_t lid) {
1021 return lines[lid].siro_key;
1022 };
1023 const std::function<double(std::size_t)> inv_key = [&](std::size_t vi) {
1024 return invs[vi].siro_key;
1025 };
1026 const std::function<double(std::size_t)> host_key = [&](std::size_t lid) {
1027 return lines[lid].host_t0;
1028 };
1029
1030 /**
1031 * PROCESSOR SHARING on a host replica, Solver_ssj_ln's startPSJob / advancePSService: every line
1032 * present is served at min(1, c/n) of a server, and the next completion is re-read whenever the
1033 * set changes. The event queue has no removal, so a stale completion is recognised by its
1034 * generation (event kind 5).
1035 */
1036 std::vector<std::vector<std::size_t> > ps_jobs(nhost_slots);
1037 std::vector<double> ps_last(nhost_slots, 0.0);
1038 std::vector<std::uint64_t> ps_gen(nhost_slots, 0);
1039 /**
1040 * The share of each line of slot `hs`: min(1, c/n) under PS; under PSPRIO, Solver_ssj_ln's
1041 * psprioJobRates, the levels served from the largest task priority down, each taking
1042 * min(servers left, its lines) servers split evenly. Either way the shares sum to min(n, c).
1043 */
1044 auto ps_rates = [&](std::size_t hs) -> std::vector<double> {
1045 const std::vector<std::size_t>& pj = ps_jobs[hs];
1046 const std::size_t n = pj.size();
1047 const std::size_t host = host_of_slot[hs];
1048 const double c = static_cast<double>(host_servers[host]);
1049 if (host_sched[host] != SchedStrategy::PSPRIO || static_cast<double>(n) <= c)
1050 return std::vector<double>(n, n == 0 ? 0.0 : std::min(1.0, c / static_cast<double>(n)));
1051 std::vector<double> rate(n, 0.0);
1052 std::vector<int> pr(n);
1053 for (std::size_t i = 0; i < n; ++i) pr[i] = task_prio(lsn.parent[lines[pj[i]].act]);
1054 std::vector<int> levels(pr);
1055 std::sort(levels.begin(), levels.end(), std::greater<int>());
1056 levels.erase(std::unique(levels.begin(), levels.end()), levels.end());
1057 double left = c;
1058 for (std::size_t l = 0; l < levels.size() && left > 0.0; ++l) {
1059 std::size_t cnt = 0;
1060 for (std::size_t i = 0; i < n; ++i) cnt += (pr[i] == levels[l]) ? 1 : 0;
1061 const double alloc = std::min(left, static_cast<double>(cnt));
1062 for (std::size_t i = 0; i < n; ++i)
1063 if (pr[i] == levels[l]) rate[i] = alloc / static_cast<double>(cnt);
1064 left -= alloc;
1065 }
1066 return rate;
1067 };
1068 /** Serve every line of slot `hs` up to now at the shares in force since the last change. */
1069 auto ps_advance = [&](std::size_t hs) {
1070 const double dt = now - ps_last[hs];
1071 if (dt > 0.0) {
1072 const std::vector<double> rate = ps_rates(hs);
1073 for (std::size_t i = 0; i < ps_jobs[hs].size(); ++i) {
1074 LnLine& L = lines[ps_jobs[hs][i]];
1075 L.remaining = std::max(0.0, L.remaining - dt * rate[i]);
1076 }
1077 }
1078 ps_last[hs] = now;
1079 };
1080 /** Busy servers of a PS slot holding n lines, the min(n, c) Solver_ssj_ln.updateHostStats counts. */
1081 auto ps_busy = [&](std::size_t hs, std::size_t n) -> double {
1082 return static_cast<double>(std::min(n, host_servers[host_of_slot[hs]]));
1083 };
1084
1085 // Time integrals per element.
1086 std::vector<double> tot_q(nidx + 1, 0.0), tot_u(nidx + 1, 0.0);
1087 std::vector<double> cur_q(nidx + 1, 0.0), cur_u(nidx + 1, 0.0);
1088 std::vector<double> last_upd(nidx + 1, 0.0);
1089 std::vector<double> completions(nidx + 1, 0.0), resp_sum(nidx + 1, 0.0),
1090 resp_cnt(nidx + 1, 0.0);
1091 // The observations behind `resp_sum` at the ENTRY indices, kept for
1092 // get_cdf_respt. Only entries: an activity's and a task's response times are
1093 // means the layered table reports, with no distributional getter on them.
1094 std::vector<std::vector<double> > entry_resp_samples(lsn.nentries);
1095 // Total time the executions of an activity spend AT ITS HOST PROCESSOR --
1096 // queueing there plus running. Over the elapsed time this is the mean
1097 // occupancy at the host, which is what ResidT is built from below.
1098 // MEASURED rather than derived from the nominal `hostdem` mean: that mean is
1099 // the SERVICE time, and on a contended processor the residence is that plus
1100 // the wait, so a derived figure would report an uncontended residence at
1101 // exactly the models where it matters.
1102 std::vector<double> act_host_resid(nidx + 1, 0.0);
1103
1104 // ONE STREAM PER CALL for the repetition count, on the engine's own offset
1105 // block (4000), so that adding a call does not shift the variates of the
1106 // think times or the host demands and re-baseline every seeded result.
1107 std::vector<Rng> g_callmult;
1108 g_callmult.reserve(lsn.ncalls + 1);
1109 for (std::size_t c = 0; c <= lsn.ncalls; ++c)
1110 g_callmult.push_back(Rng(ln_seed, static_cast<long long>(c) * 10 + 4000));
1111
1112 /**
1113 * How many times one execution of the caller makes call `cidx`.
1114 *
1115 * `floor(mean)` calls plus one more with probability equal to the fraction,
1116 * which is `Solver_ssj_ln.sampleCallCount` exactly. It is DETERMINISTIC at
1117 * an integer mean, which is what `calls-mean="3"` asks for: three calls
1118 * every time, not three on average.
1119 */
1120 auto sample_call_count = [&](std::size_t cidx) -> std::size_t {
1121 const double m = (cidx < lsn.callproc_mean.size())
1122 ? num_traits<T>::to_double(lsn.callproc_mean[cidx])
1123 : 1.0;
1124 if (!(m > 0.0)) return 0;
1125 std::size_t n = static_cast<std::size_t>(std::floor(m));
1126 const double frac = m - static_cast<double>(n);
1127 if (frac > 0.0 && cidx < g_callmult.size() && uniform01(g_callmult[cidx]) < frac) ++n;
1128 return n;
1129 };
1130
1131 auto touch = [&](std::size_t k) {
1132 const double dt = now - last_upd[k];
1133 if (dt > 0.0) {
1134 tot_q[k] += cur_q[k] * dt;
1135 tot_u[k] += cur_u[k] * dt;
1136 }
1137 last_upd[k] = now;
1138 };
1139 auto push_ev = [&](LnEvent e) {
1140 e.seq = seq++;
1141 evq.push(e);
1142 };
1143
1144 std::uint64_t done = 0;
1145
1146 // ---- lines and invocations ------------------------------------------------
1147 auto new_line = [&](std::size_t inv, std::size_t act, bool branch) -> std::size_t {
1148 std::size_t id;
1149 if (!free_lines.empty()) {
1150 id = free_lines.back();
1151 free_lines.pop_back();
1152 lines[id] = LnLine();
1153 } else {
1154 id = lines.size();
1155 lines.push_back(LnLine());
1156 }
1157 lines[id].inv = inv;
1158 lines[id].act = act;
1159 lines[id].calls_left = NOTDRAWN;
1160 lines[id].branch = branch;
1161 return id;
1162 };
1163 auto free_line = [&](std::size_t id) { free_lines.push_back(id); };
1164 /** Move a line to activity `a`, not yet begun. */
1165 auto set_act = [&](std::size_t lid, std::size_t a) {
1166 LnLine& L = lines[lid];
1167 L.act = a;
1168 L.act_t0 = -1.0;
1169 L.call_pos = 0;
1170 L.calls_left = NOTDRAWN;
1171 L.dem_done = 0;
1172 };
1173 auto new_inv =[&](std::size_t entry, std::size_t trep, std::size_t caller,
1174 std::size_t cust) -> std::size_t {
1175 std::size_t id;
1176 if (!free_invs.empty()) {
1177 id = free_invs.back();
1178 free_invs.pop_back();
1179 invs[id] = LnInv();
1180 } else {
1181 id = invs.size();
1182 invs.push_back(LnInv());
1183 }
1184 LnInv& v = invs[id];
1185 v.entry = entry;
1186 v.task = lsn.parent[entry];
1187 v.trep = trep;
1188 v.ts = task_slot(v.task, trep);
1189 v.th = NOTHR;
1190 v.caller = caller;
1191 v.cust = cust;
1192 const std::size_t root = new_line(id, bound[entry], false);
1193 invs[id].root = root;
1194 return id;
1195 };
1196
1197 /**
1198 * Enter the task: the invocation now counts at the task. It counts at its ENTRY only once it
1199 * holds a thread (see `advance`), since the entry's service starts there.
1200 */
1201 auto enter_task = [&](std::size_t vi) {
1202 LnInv& v = invs[vi];
1203 touch(v.task);
1204 cur_q[v.task] += 1.0;
1205 };
1206 /**
1207 * Arrive at the task, through its admission constraint when it has one. A
1208 * blocked request holds neither a thread nor a place in the task's queue,
1209 * so it is counted nowhere while it waits, as `arriveAtTask` has it.
1210 */
1211 auto arrive_at_task = [&](std::size_t vi) -> bool {
1212 LnInv& v = invs[vi];
1213 const int col = lincon_col(v.task, v.entry);
1214 if (col >= 0) {
1215 v.tcol = col;
1216 if (!admits(v.task, tocc[v.ts], col)) {
1217 tadm[v.ts].push_back(vi);
1218 return false;
1219 }
1220 ++tocc[v.ts][static_cast<std::size_t>(col)];
1221 }
1222 enter_task(vi);
1223 return true;
1224 };
1225
1226 // ---- the activity walk --------------------------------------------------
1227 std::function<void(std::size_t)> advance;
1228 /** Continue every line a release woke, and every line those wake. */
1229 auto drain = [&]() {
1230 while (!runnable.empty()) {
1231 const std::size_t id = runnable.front();
1232 runnable.pop_front();
1233 advance(id);
1234 }
1235 };
1236
1237 /** Put line `lid` into service at its host replica. */
1238 std::function<void(std::size_t)> advance_pool;
1239 auto pool_rates = [&](std::size_t hs) {
1240 const std::size_t h = host_of_slot[hs];
1241 const std::vector<std::size_t>& jobs = pool_jobs[hs];
1242 std::vector<double> rate(jobs.size(), 0.0);
1243 for (std::size_t p = 0; p < pool_capacity[h].size(); ++p)
1244 for (std::size_t i = 0; i < jobs.size(); ++i) {
1245 const int j = lines[jobs[i]].pcol;
1246 if (j >= 0 && static_cast<std::size_t>(j) < pool_serves[h][p].size() &&
1247 pool_serves[h][p][static_cast<std::size_t>(j)]) {
1248 rate[i] += pool_capacity[h][p];
1249 break;
1250 }
1251 }
1252 return rate;
1253 };
1254 /** Serve every job of a pooled replica up to now at the rates in force. */
1255 advance_pool = [&](std::size_t hs) {
1256 const std::vector<double> r = pool_rates(hs);
1257 const double dt = now - pool_last[hs];
1258 for (std::size_t i = 0; i < pool_jobs[hs].size(); ++i) {
1259 LnLine& L = lines[pool_jobs[hs][i]];
1260 L.remaining = std::max(0.0, L.remaining - dt * r[i]);
1261 }
1262 pool_last[hs] = now;
1263 };
1264 /**
1265 * Re-read the delivered rate after the job set changed, and arm the next
1266 * completion. A pooled host's Util integrates the DELIVERED rate, which is
1267 * `hostBusyTime += delivered*dt` of `advancePSService`.
1268 */
1269 auto rearm_pool = [&](std::size_t hs) {
1270 const std::size_t h = host_of_slot[hs];
1271 const std::vector<double> r = pool_rates(hs);
1272 double tot = 0.0, tmin = std::numeric_limits<double>::infinity();
1273 for (std::size_t i = 0; i < r.size(); ++i) {
1274 tot += r[i];
1275 if (r[i] > 0.0) tmin = std::min(tmin, lines[pool_jobs[hs][i]].remaining / r[i]);
1276 }
1277 touch(h);
1278 pool_rate[hs] = tot;
1279 double u = 0.0;
1280 for (std::size_t m = 0; m < repl[h]; ++m) u += pool_rate[host_slot0[h] + m];
1281 cur_u[h] = u;
1282 const std::uint64_t g = ++pool_gen[hs];
1283 if (std::isfinite(tmin)) {
1284 LnEvent e;
1285 e.t = now + tmin;
1286 e.kind = 4;
1287 e.who = hs;
1288 e.gen = g;
1289 push_ev(e);
1290 }
1291 };
1292 /** Arm the next completion of PS slot `hs` under the shares that hold from now on. */
1293 auto ps_schedule = [&](std::size_t hs) {
1294 ++ps_gen[hs];
1295 if (ps_jobs[hs].empty()) return;
1296 const std::vector<double> rate = ps_rates(hs);
1297 double tmin = std::numeric_limits<double>::infinity();
1298 for (std::size_t i = 0; i < ps_jobs[hs].size(); ++i)
1299 if (rate[i] > 0.0) tmin = std::min(tmin, lines[ps_jobs[hs][i]].remaining / rate[i]);
1300 LnEvent e;
1301 e.t = now + tmin;
1302 e.kind = 5;
1303 e.who = hs;
1304 e.gen = ps_gen[hs];
1305 push_ev(e);
1306 };
1307
1308 /// Lines in service on a host slot under a preemptive discipline, the candidates to preempt.
1309 std::vector<std::vector<std::size_t> > in_service(nhost_slots);
1310 std::uint64_t host_gen_ctr = 0;
1311 /**
1312 * Put line `lid` on a server of queueing host slot `hs` for the work it has left, drawing the
1313 * demand when it has none yet (a fresh request, or a PI victim resuming).
1314 */
1315 auto begin_host = [&](std::size_t lid, std::size_t hs) {
1316 LnLine& L = lines[lid];
1317 if (L.host_rem < 0.0) L.host_rem = hostdem[L.act].next(g_host[L.act]);
1318 L.host_start = now;
1319 L.host_end = now + L.host_rem;
1320 if (preemptive(host_sched[host_of_slot[hs]])) in_service[hs].push_back(lid);
1321 L.host_gen = ++host_gen_ctr;
1322 LnEvent e;
1323 e.t = L.host_end;
1324 e.kind = 0;
1325 e.who = lid;
1326 e.gen = L.host_gen;
1327 push_ev(e);
1328 };
1329 /**
1330 * Preempt a line in service on full slot `hs` for `lid`, Solver_ssj_ln.preemptFor: FCFSPR/PIPRIO
1331 * take only a STRICTLY lower priority, the lowest in service and the latest started among ties;
1332 * LCFSPR/PIPRIO take the lowest strictly lower one (earliest arrival among ties), else the equal
1333 * one that started earliest. The victim rejoins the queue at its original arrival, keeping its
1334 * residual work under PR and drawing afresh under PI. Returns whether `lid` took a server.
1335 */
1336 auto preempt_for = [&](std::size_t lid, std::size_t hs) -> bool {
1337 const SchedStrategy sc = host_sched[host_of_slot[hs]];
1338 const bool lifo = lcfs_prio(sc);
1339 const int pa = host_req_prio(lid);
1340 std::vector<std::size_t>& ins = in_service[hs];
1341 std::size_t vi = ins.size();
1342 int pv = 0;
1343 for (std::size_t i = 0; i < ins.size(); ++i) {
1344 const int pr = host_req_prio(ins[i]);
1345 if (pr >= pa) continue;
1346 bool better = vi == ins.size() || pr < pv;
1347 if (!better && pr == pv) {
1348 const LnLine& r = lines[ins[i]];
1349 const LnLine& v = lines[ins[vi]];
1350 better = lifo ? r.host_t0 < v.host_t0 : r.host_start > v.host_start;
1351 }
1352 if (better) {
1353 vi = i;
1354 pv = pr;
1355 }
1356 }
1357 if (vi == ins.size() && lifo)
1358 for (std::size_t i = 0; i < ins.size(); ++i)
1359 if (host_req_prio(ins[i]) == pa &&
1360 (vi == ins.size() || lines[ins[i]].host_start < lines[ins[vi]].host_start))
1361 vi = i;
1362 if (vi == ins.size()) return false;
1363 const std::size_t victim = ins[vi];
1364 ins.erase(ins.begin() + static_cast<std::ptrdiff_t>(vi));
1365 LnLine& v = lines[victim];
1366 v.host_gen = ++host_gen_ctr; // its pending completion is now stale
1367 const bool resume = sc == SchedStrategy::FCFSPRPRIO || sc == SchedStrategy::LCFSPRPRIO;
1368 v.host_rem = resume ? std::max(0.0, v.host_end - now) : -1.0;
1369 enqueue(host_queue[hs], victim, sc, host_req_prio, v.siro_key, g_siro_host, &host_key);
1370 begin_host(lid, hs);
1371 return true;
1372 };
1373
1374 auto start_host_service = [&](std::size_t lid) {
1375 LnLine& L = lines[lid];
1376 const std::size_t act = L.act;
1377 const std::size_t host = lsn.host_of(act);
1378 const std::size_t hs = host_slot(host, invs[L.inv].trep);
1379 touch(host);
1380 cur_q[host] += 1.0;
1381 if (pooled[host]) {
1382 advance_pool(hs);
1383 const std::size_t t = lsn.parent[act];
1384 const std::map<std::size_t, int>::const_iterator it = pool_col[host].find(t);
1385 if (it == pool_col[host].end())
1386 throw InputError("SolverLDES (native LN engine): activity '" + lsn.names[act] +
1387 "' runs on a host whose pool declaration does not name its "
1388 "task, so no server can be assigned");
1389 lines[lid].pcol = it->second;
1390 lines[lid].remaining = hostdem[act].next(g_host[act]);
1391 pool_jobs[hs].push_back(lid);
1392 rearm_pool(hs);
1393 return;
1394 }
1395 lines[lid].host_rem = -1.0;
1396 if (is_ps(host_sched[host])) {
1397 // Charge the interval that just ended at the share in force during it, then admit.
1398 ps_advance(hs);
1399 const std::size_t n0 = ps_jobs[hs].size();
1400 lines[lid].remaining = hostdem[act].next(g_host[act]);
1401 ps_jobs[hs].push_back(lid);
1402 ++host_busy[hs];
1403 cur_u[host] += ps_busy(hs, n0 + 1) - ps_busy(hs, n0);
1404 ps_schedule(hs);
1405 return;
1406 }
1407 if (host_busy[hs] < host_servers[host]) {
1408 ++host_busy[hs];
1409 cur_u[host] += 1.0;
1410 begin_host(lid, hs);
1411 } else if (!(preemptive(host_sched[host]) && preempt_for(lid, hs))) {
1412 enqueue(host_queue[hs], lid, host_sched[host], host_req_prio, lines[lid].siro_key,
1413 g_siro_host, &host_key);
1414 }
1415 };
1416 /**
1417 * Ask the host for service. The residence starts HERE, when the line asks,
1418 * whether it then queues at the processor or waits outside it for a host
1419 * admission token; a blocked line keeps its thread, being still inside
1420 * its activity, but holds no server.
1421 */
1422 auto request_host = [&](std::size_t lid) {
1423 LnLine& L = lines[lid];
1424 const std::size_t act = L.act;
1425 const std::size_t host = lsn.host_of(act);
1426 const std::size_t hs = host_slot(host, invs[L.inv].trep);
1427 L.host_t0 = now;
1428 const int col = lincon_col(host, lsn.parent[act]);
1429 if (col >= 0) {
1430 L.hcol = col;
1431 if (!admits(host, hocc[hs], col)) {
1432 hadm[hs].push_back(lid);
1433 return;
1434 }
1435 ++hocc[hs][static_cast<std::size_t>(col)];
1436 }
1437 start_host_service(lid);
1438 };
1439 /** Give back a host admission token and admit the blocked head while it fits. */
1440 auto release_host_admission = [&](std::size_t lid) {
1441 LnLine& L = lines[lid];
1442 if (L.hcol < 0) return;
1443 const std::size_t host = lsn.host_of(L.act);
1444 const std::size_t hs = host_slot(host, invs[L.inv].trep);
1445 --hocc[hs][static_cast<std::size_t>(L.hcol)];
1446 L.hcol = -1;
1447 while (!hadm[hs].empty()) {
1448 const std::size_t head = hadm[hs].front();
1449 const int c = lines[head].hcol;
1450 if (!admits(host, hocc[hs], c)) return;
1451 hadm[hs].pop_front();
1452 ++hocc[hs][static_cast<std::size_t>(c)];
1453 start_host_service(head);
1454 }
1455 };
1456
1457 /** Arm THREAD's cold start; the request that woke it waits in thr_queue. */
1458 auto start_setup = [&](std::size_t ts, std::size_t th) {
1459 const std::size_t task = thr_owner[ts];
1460 thr_state[ts][th] = ThreadState::SETUP;
1461 thr_setup_t0[ts][th] = now;
1462 LnEvent e;
1463 e.t = now + setupd[task].next(g_setup[task]);
1464 e.kind = 2;
1465 e.who = ts;
1466 e.aux = th;
1467 e.gen = ++thr_gen[ts][th];
1468 push_ev(e);
1469 };
1470
1471 /** Arm THREAD's idle countdown; when it expires the thread is OFF. */
1472 auto start_delayoff = [&](std::size_t ts, std::size_t th) {
1473 const std::size_t task = thr_owner[ts];
1474 thr_state[ts][th] = ThreadState::DELAYOFF;
1475 LnEvent e;
1476 e.t = now + delayoffd[task].next(g_doff[task]);
1477 e.kind = 3;
1478 e.who = ts;
1479 e.aux = th;
1480 e.gen = ++thr_gen[ts][th];
1481 push_ev(e);
1482 };
1483
1484 /**
1485 * Give invocation `vi` a thread of its task.
1486 *
1487 * Returns false when it could not proceed: either every thread is taken, or
1488 * the only thread available was powered off and is now warming up. Either
1489 * way it is parked in thr_queue and continued by whoever frees or finishes
1490 * warming a thread. An infinite-thread task never blocks and holds no
1491 * identified thread.
1492 */
1493 auto acquire_thread = [&](std::size_t vi) -> bool {
1494 LnInv& v = invs[vi];
1495 const std::size_t task = v.task;
1496 const std::size_t ts = v.ts;
1497 touch(task);
1498 if (task_threads[task] == std::numeric_limits<std::size_t>::max()) {
1499 cur_u[task] += 1.0;
1500 v.has_thread = true;
1501 v.th = NOTHR;
1502 return true;
1503 }
1504 std::vector<bool>& held = thr_held[ts];
1505 std::vector<ThreadState>& st = thr_state[ts];
1506 // An ACTIVE idle thread first, then one still counting down -- caught
1507 // before it powers off, it serves having paid nothing -- and only then a
1508 // thread that is already OFF, which must warm up first.
1509 for (std::size_t th = 0; th < held.size(); ++th) {
1510 if (!held[th] && st[th] == ThreadState::ACTIVE) {
1511 held[th] = true;
1512 cur_u[task] += 1.0;
1513 v.has_thread = true;
1514 v.th = th;
1515 return true;
1516 }
1517 }
1518 for (std::size_t th = 0; th < held.size(); ++th) {
1519 if (!held[th] && st[th] == ThreadState::DELAYOFF) {
1520 ++thr_gen[ts][th]; // cancel the countdown
1521 st[th] = ThreadState::ACTIVE;
1522 held[th] = true;
1523 cur_u[task] += 1.0;
1524 v.has_thread = true;
1525 v.th = th;
1526 return true;
1527 }
1528 }
1529 enqueue(thr_queue[ts], vi, task_sched[task], task_req_prio, invs[vi].siro_key, g_siro_task,
1530 nullptr);
1531 for (std::size_t th = 0; th < held.size(); ++th) {
1532 if (!held[th] && st[th] == ThreadState::OFF) {
1533 start_setup(ts, th);
1534 break;
1535 }
1536 }
1537 return false;
1538 };
1539
1540 /** Free the thread invocation `vi` holds and hand it to the next one waiting. */
1541 auto release_thread = [&](std::size_t vi) {
1542 LnInv& v = invs[vi];
1543 const std::size_t ts = v.ts;
1544 const std::size_t th = v.th;
1545 const std::size_t task = thr_owner[ts];
1546 touch(task);
1547 cur_u[task] -= 1.0;
1548 v.has_thread = false;
1549 v.th = NOTHR;
1550 if (th == NOTHR) return; // an infinite-thread task holds nothing
1551 thr_held[ts][th] = false;
1552 if (!thr_queue[ts].empty()) {
1553 const std::size_t nxt = dequeue(thr_queue[ts], task_sched[task], inv_key);
1554 thr_held[ts][th] = true;
1555 cur_u[task] += 1.0;
1556 invs[nxt].has_thread = true;
1557 invs[nxt].th = th;
1558 runnable.push_back(invs[nxt].root);
1559 } else if (has_setup[task]) {
1560 start_delayoff(ts, th);
1561 }
1562 };
1563
1564 /**
1565 * One execution of the line's activity has finished.
1566 *
1567 * ITS RESPONSE TIME CARRIES THE CALLS IT MADE, which is what separates it
1568 * from the residence `act_host_resid` accumulates: on an activity that calls
1569 * a server three times, the response is its own demand plus the three
1570 * replies and the residence is its own demand alone. The occupancy is closed
1571 * off through `cur_q` at the same instant, so QLen, RespT and Tput satisfy
1572 * Little's law by construction rather than by three separate estimators
1573 * happening to agree.
1574 */
1575 auto finish_act = [&](LnLine& L) {
1576 const std::size_t act = L.act;
1577 completions[act] += 1.0;
1578 if (L.act_t0 >= 0.0) {
1579 resp_sum[act] += now - L.act_t0;
1580 resp_cnt[act] += 1.0;
1581 touch(act);
1582 cur_q[act] -= 1.0;
1583 }
1584 L.act_t0 = -1.0;
1585 };
1586
1587 /**
1588 * The entry replies: its throughput and response time are recorded, and the
1589 * synchronous caller is released. `direct` is a reply at completion, whose
1590 * caller the walk continues itself; an early reply (phase 2 follows, or a
1591 * cache branch replied) wakes the caller through `runnable`.
1592 *
1593 * AN ENTRY'S RESPONSE TIME IS THE LQN ENTRY SERVICE TIME, from the instant it
1594 * took a thread of its task to the reply, and so carries every nested call its
1595 * activities made. The wait for the thread belongs to the caller's call, as
1596 * lqns, lqsim, SolverLN and Solver_ssj_ln.sendEntryReply all count it.
1597 */
1598 auto send_reply = [&](std::size_t vi, bool direct) {
1599 LnInv& v = invs[vi];
1600 if (v.replied) return;
1601 v.replied = true;
1602 v.early_reply = !direct;
1603 completions[v.entry] += 1.0;
1604 const double r = now - v.entry_t0;
1605 resp_sum[v.entry] += r;
1606 resp_cnt[v.entry] += 1.0;
1607 // 1-BASED element indices in this engine, unlike the JAR and python
1608 // ports: entries are eshift+1 .. eshift+nentries, so the local index is
1609 // one less than the difference.
1610 if (v.entry > lsn.eshift && v.entry <= lsn.eshift + lsn.nentries)
1611 entry_resp_samples[v.entry - lsn.eshift - 1].push_back(r);
1612 if (!direct && v.caller != LN_NONE) runnable.push_back(v.caller);
1613 };
1614
1615 // ---- cache content -------------------------------------------------------
1616 /** `cacheMiss`: insert into the entry list, evicting by the policy's rule. */
1617 auto cache_insert = [&](std::size_t ci, std::size_t item) {
1618 LnCacheState& cs = caches[ci];
1619 std::list<std::size_t>& lst = cs.levels[0];
1620 const std::size_t cap = static_cast<std::size_t>(std::max(0, cs.cap[0]));
1621 if (cs.rs == lang::ReplacementStrategy::RR) {
1622 if (lst.size() >= cap && !lst.empty()) {
1623 std::size_t r = static_cast<std::size_t>(uniform01(g_rr[ci]) *
1624 static_cast<double>(lst.size()));
1625 r = std::min(r, lst.size() - 1);
1626 std::list<std::size_t>::iterator it = lst.begin();
1627 std::advance(it, static_cast<long>(r));
1628 *it = item;
1629 } else {
1630 lst.push_front(item);
1631 }
1632 } else {
1633 lst.push_front(item);
1634 if (lst.size() > cap) lst.pop_back();
1635 }
1636 };
1637 /** `cacheHit`: refresh or promote the item one list up, demoting the displaced one. */
1638 auto cache_promote = [&](std::size_t ci, std::size_t item, std::size_t level) {
1639 LnCacheState& cs = caches[ci];
1640 const lang::ReplacementStrategy rs = cs.rs;
1641 const std::size_t inew = std::min(level + 1, cs.levels.size() - 1);
1642 const bool front_rule = rs == lang::ReplacementStrategy::LRU ||
1645 if (inew <= level) {
1646 if (front_rule) {
1647 cs.levels[level].remove(item);
1648 cs.levels[level].push_front(item);
1649 }
1650 return;
1651 }
1652 std::list<std::size_t>& lo = cs.levels[level];
1653 std::list<std::size_t>& hi = cs.levels[inew];
1654 std::size_t kpos = 0;
1655 for (std::list<std::size_t>::iterator it = lo.begin(); it != lo.end(); ++it, ++kpos)
1656 if (*it == item) break;
1657 lo.remove(item);
1658 if (hi.size() < static_cast<std::size_t>(std::max(0, cs.cap[inew]))) {
1659 hi.push_front(item);
1660 return;
1661 }
1662 std::size_t demoted;
1664 std::size_t r = static_cast<std::size_t>(uniform01(g_rr[ci]) *
1665 static_cast<double>(hi.size()));
1666 r = std::min(r, hi.size() - 1);
1667 std::list<std::size_t>::iterator it = hi.begin();
1668 std::advance(it, static_cast<long>(r));
1669 demoted = *it;
1670 *it = item;
1671 } else {
1672 demoted = hi.back();
1673 hi.pop_back();
1674 hi.push_front(item);
1675 }
1676 if (front_rule || rs == lang::ReplacementStrategy::SFIFO) {
1677 lo.push_front(demoted);
1678 } else {
1679 std::list<std::size_t>::iterator it = lo.begin();
1680 std::advance(it, static_cast<long>(std::min(kpos, lo.size())));
1681 lo.insert(it, demoted);
1682 }
1683 };
1684 static const std::size_t HELD = static_cast<std::size_t>(-2);
1685 /**
1686 * One read of an ItemEntry, `accessCache`: draw the item, look it up in the
1687 * live content of the replica serving the request, update the content, and
1688 * return the branch to continue on -- or HELD, when retrieval parks a read
1689 * of an item that is already being fetched.
1690 */
1691 auto access_cache = [&](std::size_t ai, std::size_t lid) -> std::size_t {
1692 const LnCacheAccess& ca = accesses[ai];
1693 LnInv& v = invs[lines[lid].inv];
1694 const std::size_t ci = ca.caches[std::min(v.trep, ca.caches.size() - 1)];
1695 LnCacheState& cs = caches[ci];
1696 ++cs.reads;
1697 const double u = uniform01(g_item[ci]);
1698 std::size_t item = ca.cdf.size() - 1;
1699 for (std::size_t i = 0; i < ca.cdf.size(); ++i)
1700 if (u <= ca.cdf[i]) {
1701 item = i;
1702 break;
1703 }
1704 for (std::size_t l = 0; l < cs.levels.size(); ++l)
1705 if (std::find(cs.levels[l].begin(), cs.levels[l].end(), item) != cs.levels[l].end()) {
1706 ++cs.hits;
1707 cache_promote(ci, item, l);
1708 return ca.hit;
1709 }
1710 if (cs.retrieval) {
1711 if (item < cs.inflight.size() && cs.inflight[item]) {
1712 cs.held[item].push_back(std::make_pair(lid, ai));
1713 return HELD;
1714 }
1715 ++cs.misses;
1716 cs.inflight[item] = 1;
1717 v.fetching = true;
1718 v.fetch_cache = ci;
1719 v.fetch_item = item;
1720 return ca.miss;
1721 }
1722 ++cs.misses;
1723 cache_insert(ci, item);
1724 return ca.miss;
1725 };
1726 /** The fetch completed: insert the item and let every parked read hit it. */
1727 auto release_fetch = [&](std::size_t vi) {
1728 LnInv& v = invs[vi];
1729 if (!v.fetching) return;
1730 v.fetching = false;
1731 LnCacheState& cs = caches[v.fetch_cache];
1732 const std::size_t item = v.fetch_item;
1733 cs.inflight[item] = 0;
1734 cache_insert(v.fetch_cache, item);
1735 std::vector<std::pair<std::size_t, std::size_t> > held;
1736 held.swap(cs.held[item]);
1737 for (std::size_t k = 0; k < held.size(); ++k) {
1738 ++caches[v.fetch_cache].delayed;
1739 set_act(held[k].first, accesses[held[k].second].hit);
1740 runnable.push_back(held[k].first);
1741 }
1742 };
1743
1744 /**
1745 * The invocation is complete, `completeRequest`: reply if it has not, leave
1746 * the entry and the task, free the thread and the task admission token, and
1747 * return the line to continue -- the synchronous caller, when the reply was
1748 * made just now -- or LN_NONE. A top-level invocation sends its customer
1749 * back to think.
1750 */
1751 auto complete_request = [&](std::size_t vi) -> std::size_t {
1752 send_reply(vi, true);
1753 release_fetch(vi);
1754 LnInv& v = invs[vi];
1755 touch(v.entry);
1756 cur_q[v.entry] -= 1.0;
1757 completions[v.task] += 1.0;
1758 touch(v.task);
1759 cur_q[v.task] -= 1.0;
1760 release_thread(vi);
1761 if (v.tcol >= 0) {
1762 const std::size_t ts = v.ts, task = v.task;
1763 --tocc[ts][static_cast<std::size_t>(v.tcol)];
1764 v.tcol = -1;
1765 while (!tadm[ts].empty()) {
1766 const std::size_t head = tadm[ts].front();
1767 const int c = invs[head].tcol;
1768 if (!admits(task, tocc[ts], c)) break;
1769 tadm[ts].pop_front();
1770 ++tocc[ts][static_cast<std::size_t>(c)];
1771 enter_task(head);
1772 runnable.push_back(invs[head].root);
1773 }
1774 }
1775 LnInv& w = invs[vi];
1776 const std::size_t caller = w.caller, cust = w.cust;
1777 const bool early = w.early_reply;
1778 free_line(w.root);
1779 free_invs.push_back(vi);
1780 if (cust != LN_NONE) {
1781 // THE COMPLETION IS NOT COUNTED HERE: the reference task's own entry
1782 // just replied and `completions[task]` above is this task already.
1783 // `resp_sum` is the CYCLE response time and is the task's own.
1784 const std::size_t rt = custs[cust].ref_task;
1785 resp_sum[rt] += now - custs[cust].t_start;
1786 resp_cnt[rt] += 1.0;
1787 ++done;
1788 LnEvent e;
1789 e.t = now + (has_think[rt] ? think[rt].next(g_think[rt]) : 0.0);
1790 e.kind = 1;
1791 e.who = cust;
1792 push_ev(e);
1793 return LN_NONE;
1794 }
1795 return (caller != LN_NONE && !early) ? caller : LN_NONE;
1796 };
1797
1798 /** A line reached an activity with no successor, `finishLine`. */
1799 auto finish_line = [&](std::size_t lid) -> std::size_t {
1800 const std::size_t vi = lines[lid].inv;
1801 LnInv& v = invs[vi];
1802 const bool br = lines[lid].branch;
1803 if (v.pending < 0) return complete_request(vi);
1804 const long rem = v.pending - 1;
1805 if (br) free_line(lid);
1806 if (rem <= 0) {
1807 invs[vi].pending = -1;
1808 return complete_request(vi);
1809 }
1810 invs[vi].pending = rem;
1811 return LN_NONE;
1812 };
1813
1814 /**
1815 * The line's activity is complete, `completeActivity`: a cache read picks
1816 * its branch; an AND-join input is recorded and the join fires on the ROOT
1817 * once all have arrived; a phase-1 activity entering phase 2, or a cache
1818 * branch of the entry, replies; then one successor continues, an OR or loop
1819 * decision draws one, and an AND fork starts every branch. Returns the line
1820 * to continue, or LN_NONE.
1821 */
1822 auto complete_activity = [&](std::size_t lid) -> std::size_t {
1823 finish_act(lines[lid]);
1824 const std::size_t act = lines[lid].act;
1825 const std::size_t vi = lines[lid].inv;
1826 if (access_of_driver[act] != LN_NONE) {
1827 const std::size_t nx = access_cache(access_of_driver[act], lid);
1828 if (nx == HELD) return LN_NONE;
1829 set_act(lid, nx);
1830 return lid;
1831 }
1832 const std::vector<std::size_t>& sc = succ_act[act];
1833 const bool br = lines[lid].branch;
1834 if ((br || invs[vi].pending >= 0) && pretype(act) == lang::PrecedenceType::PRE_AND &&
1835 !sc.empty()) {
1836 const std::size_t j = sc[0];
1837 std::set<std::size_t>& got = invs[vi].joins[j];
1838 got.insert(act);
1839 if (br) free_line(lid);
1840 const std::size_t req = join_required[j] > 0 ? join_required[j] : 1;
1841 if (got.size() >= req) {
1842 invs[vi].joins.erase(j);
1843 if (invs[vi].pending >= 0) invs[vi].pending -= static_cast<long>(req - 1);
1844 set_act(invs[vi].root, j);
1845 return invs[vi].root;
1846 }
1847 return LN_NONE;
1848 }
1849 if (!invs[vi].replied && !br) {
1850 bool reply = branch_reply[act] != 0 && branch_reply[act] == invs[vi].entry;
1851 if (!reply && phase_of(act) == 1)
1852 for (std::size_t s : sc)
1853 if (phase_of(s) == 2) {
1854 reply = true;
1855 break;
1856 }
1857 if (reply) send_reply(vi, false);
1858 }
1859 if (invs[vi].replied && invs[vi].fetching) release_fetch(vi);
1860 if (sc.empty()) return finish_line(lid);
1861 if (sc.size() == 1) {
1862 set_act(lid, sc[0]);
1863 return lid;
1864 }
1865 if (!or_prob[act].empty()) {
1866 const double u = uniform01(g_route[act]);
1867 double cum = 0.0;
1868 std::size_t pick = sc.back();
1869 for (std::size_t k = 0; k < sc.size(); ++k) {
1870 cum += or_prob[act][k];
1871 if (u <= cum) {
1872 pick = sc[k];
1873 break;
1874 }
1875 }
1876 set_act(lid, pick);
1877 return lid;
1878 }
1879 // AND fork: one line becomes |sc| concurrent lines. The forking line goes
1880 // dormant (the root) or retires (a branch); the join resumes the root.
1881 invs[vi].pending = (invs[vi].pending < 0 ? 1 : invs[vi].pending) +
1882 static_cast<long>(sc.size()) - 1;
1883 if (br) free_line(lid);
1884 std::vector<std::size_t> kids;
1885 for (std::size_t k = 0; k < sc.size(); ++k) kids.push_back(new_line(vi, sc[k], true));
1886 for (std::size_t k = kids.size(); k-- > 1;) runnable.push_front(kids[k]);
1887 return kids[0];
1888 };
1889
1890 /**
1891 * A request at `entry` on task replica `rep` that nobody waits on: an asynchronous callee or
1892 * an open arrival. It runs as an invocation with no caller and no customer, so it just ends.
1893 */
1894 auto spawn_request = [&](std::size_t entry, std::size_t rep) {
1895 const std::size_t vi = new_inv(entry, rep, LN_NONE, LN_NONE);
1896 if (arrive_at_task(vi)) runnable.push_back(invs[vi].root);
1897 };
1898
1899 /**
1900 * Continue line `lid` from wherever it is: take a thread, begin the current
1901 * activity, ask its host, think, issue its next call, or complete it.
1902 */
1903 advance = [&](std::size_t lid) {
1904 while (lid != LN_NONE) {
1905 const std::size_t vi = lines[lid].inv;
1906 // An invocation holds a THREAD of its task for the whole entry,
1907 // across its calls, and waits outside when none is free.
1908 if (!invs[vi].has_thread) {
1909 if (!acquire_thread(vi)) return;
1910 }
1911 // The entry's service starts with its thread; its occupancy runs from here to the
1912 // end of its last phase, Solver_ssj_ln's entryResidenceTime from serviceStartTime.
1913 if (!invs[vi].started) {
1914 LnInv& v = invs[vi];
1915 v.started = true;
1916 v.entry_t0 = now;
1917 touch(v.entry);
1918 cur_q[v.entry] += 1.0;
1919 }
1920 LnLine& L = lines[lid];
1921 const std::size_t act = L.act;
1922 if (act == 0) { // an entry with no activity replies at once
1923 lid = complete_request(vi);
1924 continue;
1925 }
1926 if (L.act_t0 < 0.0) {
1927 L.act_t0 = now;
1928 touch(act);
1929 cur_q[act] += 1.0;
1930 L.dem_done = 0;
1931 }
1932 // THE HOST DEMAND RUNS FIRST, THEN THE THINK TIME, THEN THE CALLS: the order LQN2QN
1933 // builds (the demand step, then one step per call stage) and Solver_ssj_ln runs.
1934 if (L.dem_done == 0) {
1935 L.dem_done = 1;
1936 if (has_hostdem[act]) {
1937 request_host(lid);
1938 return;
1939 }
1940 }
1941 // The activity's think time, Solver_ssj_ln.proceedAfterHost: it holds the thread but
1942 // not the processor, so it adds to the activity's response and not to host busy.
1943 if (L.dem_done == 1) {
1944 L.dem_done = 2;
1945 if (has_actthink[act]) {
1946 LnEvent e;
1947 e.t = now + actthink[act].next(g_actthink[act]);
1948 e.kind = 6;
1949 e.who = lid;
1950 push_ev(e);
1951 return;
1952 }
1953 }
1954 const std::vector<std::size_t>& calls =
1955 (act < lsn.callsof.size()) ? lsn.callsof[act] : std::vector<std::size_t>();
1956 if (L.call_pos < calls.size()) {
1957 // A pending call of the current activity, made `calls-mean` times
1958 // per execution, drawn once when it is first reached.
1959 const std::size_t cidx = calls[L.call_pos];
1960 if (L.calls_left == NOTDRAWN) L.calls_left = sample_call_count(cidx);
1961 if (L.calls_left == 0) {
1962 ++L.call_pos;
1963 L.calls_left = NOTDRAWN;
1964 continue;
1965 }
1966 --L.calls_left;
1967 if (L.calls_left == 0) {
1968 ++L.call_pos;
1969 L.calls_left = NOTDRAWN;
1970 }
1971 const std::size_t callee = lsn.callpair_dst[cidx];
1972 const std::size_t rep =
1973 callee_replica(invs[vi].task, invs[vi].trep, lsn.parent[callee]);
1974 if (lsn.calltype[cidx] == lang::CallType::SYNC) {
1975 // THE CALLER KEEPS ITS THREAD across the call. That is the
1976 // layered contention the model exists to represent.
1977 const std::size_t child = new_inv(callee, rep, lid, LN_NONE);
1978 if (!arrive_at_task(child)) return;
1979 lid = invs[child].root;
1980 continue;
1981 }
1982 // Asynchronous: the callee runs as a request of its own, the caller
1983 // does not wait, and the reply is never collected.
1984 spawn_request(callee, rep);
1985 continue;
1986 }
1987 // The demand, the think time and the calls are done: the activity
1988 // completes, its response time carrying all three.
1989 L.call_pos = 0;
1990 L.calls_left = NOTDRAWN;
1991 lid = complete_activity(lid);
1992 }
1993 };
1994
1995 // ---- reference tasks ----------------------------------------------------
1996 // Each reference task runs `mult` independent customers, which is what makes
1997 // the closed population of a layered model.
1998 auto start_cycle = [&](std::size_t c) {
1999 const std::size_t t = custs[c].ref_task;
2000 custs[c].t_start = now;
2001 const std::size_t vi = new_inv(lsn.entriesof[t][0], custs[c].repl, LN_NONE, c);
2002 if (arrive_at_task(vi)) advance(invs[vi].root);
2003 };
2004 bool any_ref = false;
2005 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
2006 if (t >= lsn.isref.size() || !lsn.isref[t]) continue;
2007 any_ref = true;
2008 const std::size_t n = (task_threads[t] == std::numeric_limits<std::size_t>::max())
2009 ? 1
2010 : task_threads[t];
2011 const std::vector<std::size_t>& es =
2012 (t < lsn.entriesof.size()) ? lsn.entriesof[t] : std::vector<std::size_t>();
2013 if (es.empty())
2014 throw InputError("SolverLDES (native LN engine): reference task '" + lsn.names[t] +
2015 "' declares no entry to invoke");
2016 // A replicated reference task is r separate populations of `mult`
2017 // customers, not one population of r*mult sharing a queue.
2018 for (std::size_t m = 0; m < repl[t]; ++m) {
2019 for (std::size_t k = 0; k < n; ++k) {
2020 LnCustomer cu;
2021 cu.ref_task = t;
2022 cu.repl = m;
2023 custs.push_back(cu);
2024 start_cycle(custs.size() - 1);
2025 }
2026 }
2027 }
2028 // ---- open arrivals --------------------------------------------------------
2029 // Each replica of the entry's task receives its own stream, as Solver_ssj_ln.startOpenArrivals
2030 // gives it: the declared rate is what one replica sees. Offset block 5000, per entry.
2031 std::vector<Sampler> arrival(nidx + 1);
2032 std::vector<Rng> g_arr;
2033 g_arr.reserve(nidx + 1);
2034 for (std::size_t k = 0; k <= nidx; ++k)
2035 g_arr.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 5000));
2036 for (std::size_t e = lsn.eshift + 1; e <= lsn.eshift + lsn.nentries; ++e) {
2037 if (e >= lsn.has_arrival.size() || !lsn.has_arrival[e] || e >= lsn.arrival.size() ||
2038 lsn.arrival[e].disabled)
2039 continue;
2040 arrival[e] = Sampler(lsn.arrival[e], "the open arrivals at '" + lsn.names[e] + "'");
2041 for (std::size_t m = 0; m < repl[lsn.parent[e]]; ++m) {
2042 LnEvent ev;
2043 ev.t = now + arrival[e].next(g_arr[e]);
2044 ev.kind = 7;
2045 ev.who = e;
2046 ev.aux = m;
2047 push_ev(ev);
2048 }
2049 }
2050 drain();
2051 if (!any_ref)
2052 throw InputError("SolverLDES (native LN engine): the model has no reference task, so "
2053 "nothing drives it");
2054
2055 // ---- the event loop -----------------------------------------------------
2056 while (!evq.empty() && done < max_events) {
2057 const LnEvent ev = evq.top();
2058 evq.pop();
2059 now = ev.t;
2060
2061 if (ev.kind == 1) {
2062 // A reference customer finished thinking: start the next cycle on the
2063 // replica it belongs to.
2064 start_cycle(ev.who);
2065 drain();
2066 continue;
2067 }
2068
2069 if (ev.kind == 2 || ev.kind == 3) {
2070 const std::size_t ts = ev.who, th = ev.aux;
2071 if (ev.gen != thr_gen[ts][th]) continue; // the timer was cancelled
2072 if (ev.kind == 3) {
2073 if (thr_state[ts][th] == ThreadState::DELAYOFF)
2074 thr_state[ts][th] = ThreadState::OFF;
2075 continue;
2076 }
2077 // A cold start finished: the thread is up and takes the head of the
2078 // queue it was woken for.
2079 thr_state[ts][th] = ThreadState::ACTIVE;
2080 if (!thr_held[ts][th] && !thr_queue[ts].empty()) {
2081 const std::size_t nxt = dequeue(thr_queue[ts], task_sched[thr_owner[ts]], inv_key);
2082 thr_held[ts][th] = true;
2083 touch(thr_owner[ts]);
2084 cur_u[thr_owner[ts]] += 1.0;
2085 invs[nxt].has_thread = true;
2086 invs[nxt].th = th;
2087 advance(invs[nxt].root);
2088 drain();
2089 }
2090 continue;
2091 }
2092
2093 if (ev.kind == 4) {
2094 // The next completion at a pooled replica, unless the job set moved it.
2095 const std::size_t hs = ev.who;
2096 if (ev.gen != pool_gen[hs]) continue;
2097 const std::size_t host = host_of_slot[hs];
2098 advance_pool(hs);
2099 std::vector<std::size_t> finished, keep;
2100 for (std::size_t lid : pool_jobs[hs])
2101 (lines[lid].remaining <= 1e-9 ? finished : keep).push_back(lid);
2102 pool_jobs[hs].swap(keep);
2103 rearm_pool(hs);
2104 for (std::size_t lid : finished) {
2105 touch(host);
2106 cur_q[host] -= 1.0;
2107 completions[host] += 1.0;
2108 act_host_resid[lines[lid].act] += now - lines[lid].host_t0;
2109 release_host_admission(lid);
2110 advance(lid); // on to its think time and calls
2111 drain();
2112 }
2113 continue;
2114 }
2115
2116 if (ev.kind == 5) {
2117 // A PS host replica's next completion: every line whose work is done leaves together.
2118 const std::size_t hs = ev.who;
2119 if (ev.gen != ps_gen[hs]) continue; // the job set changed since this was armed
2120 const std::size_t host = host_of_slot[hs];
2121 ps_advance(hs);
2122 std::vector<std::size_t> finished, keep;
2123 for (std::size_t lid : ps_jobs[hs])
2124 (lines[lid].remaining <= 1e-9 ? finished : keep).push_back(lid);
2125 const std::size_t n0 = ps_jobs[hs].size();
2126 ps_jobs[hs].swap(keep);
2127 touch(host);
2128 cur_u[host] -= ps_busy(hs, n0) - ps_busy(hs, ps_jobs[hs].size());
2129 ps_schedule(hs);
2130 for (std::size_t lid : finished) {
2131 cur_q[host] -= 1.0;
2132 --host_busy[hs];
2133 completions[host] += 1.0;
2134 act_host_resid[lines[lid].act] += now - lines[lid].host_t0;
2135 release_host_admission(lid);
2136 advance(lid); // on to its think time and calls
2137 drain();
2138 }
2139 continue;
2140 }
2141
2142 if (ev.kind == 6) {
2143 // A line finished its activity's think time and goes on to its calls.
2144 advance(ev.who);
2145 drain();
2146 continue;
2147 }
2148
2149 if (ev.kind == 7) {
2150 // An open arrival: a request nobody waits on, then the next arrival of the stream.
2151 spawn_request(ev.who, ev.aux);
2152 LnEvent nx = ev;
2153 nx.t = now + arrival[ev.who].next(g_arr[ev.who]);
2154 push_ev(nx);
2155 drain();
2156 continue;
2157 }
2158
2159 // A host completion at a c-server (or INF) processor, unless a preemption made it stale.
2160 const std::size_t lid = ev.who;
2161 if (ev.gen != lines[lid].host_gen) continue;
2162 const std::size_t act = lines[lid].act;
2163 const std::size_t host = lsn.host_of(act);
2164 // The demand ran on the processor replica paired with the task replica
2165 // that holds the request, and that pairing is fixed for the invocation.
2166 const std::size_t hs = host_slot(host, invs[lines[lid].inv].trep);
2167 touch(host);
2168 cur_q[host] -= 1.0;
2169 cur_u[host] -= 1.0;
2170 --host_busy[hs];
2171 completions[host] += 1.0;
2172 act_host_resid[act] += now - lines[lid].host_t0;
2173 if (preemptive(host_sched[host]))
2174 in_service[hs].erase(std::find(in_service[hs].begin(), in_service[hs].end(), lid));
2175 if (!host_queue[hs].empty()) {
2176 const std::size_t nxt = dequeue(host_queue[hs], host_sched[host], line_key);
2177 ++host_busy[hs];
2178 cur_u[host] += 1.0;
2179 begin_host(nxt, hs);
2180 }
2181 release_host_admission(lid);
2182 // The demand is the FIRST thing an activity does: it goes on to its think
2183 // time and its calls, and advance() completes it once they are done.
2184 advance(lid);
2185 drain();
2186 }
2187
2188 // A run that stops early with a request still blocked by an admission
2189 // constraint did not finish its budget: the constraint deadlocked it, and
2190 // the measures so far describe a model that has stopped moving.
2191 if (done < max_events && evq.empty()) {
2192 for (std::size_t s = 0; s < nhost_slots; ++s)
2193 if (!hadm[s].empty())
2194 throw InputError("SolverLDES (native LN engine): the admission constraint on "
2195 "host '" + lsn.names[host_of_slot[s]] + "' deadlocked the model: " +
2196 std::to_string(hadm[s].size()) +
2197 " request(s) are blocked with no event left");
2198 for (std::size_t s = 0; s < ntask_slots; ++s)
2199 if (!tadm[s].empty())
2200 throw InputError("SolverLDES (native LN engine): the admission constraint on "
2201 "task '" + lsn.names[thr_owner[s]] + "' deadlocked the model: " +
2202 std::to_string(tadm[s].size()) +
2203 " request(s) are blocked with no event left");
2204 }
2205
2206 // ---- result -------------------------------------------------------------
2207 for (std::size_t k = 1; k <= nidx; ++k) touch(k);
2208 LnResult res;
2209 res.nidx = nidx;
2210 res.QLN = Matrix<double>(nidx + 1, 1, 0.0);
2211 res.ULN = Matrix<double>(nidx + 1, 1, 0.0);
2212 res.RLN = Matrix<double>(nidx + 1, 1, 0.0);
2213 res.TLN = Matrix<double>(nidx + 1, 1, 0.0);
2214 // Residence starts at NaN everywhere and is written only where it exists:
2215 // activities and their tasks. See LnResult::WLN.
2216 res.WLN = Matrix<double>(nidx + 1, 1, std::numeric_limits<double>::quiet_NaN());
2217 for (std::size_t k = 1; k <= nidx; ++k) {
2218 if (now > 0.0) {
2219 res.QLN(k, 0) = tot_q[k] / now;
2220 // A host's utilization is its busy servers over its capacity; an
2221 // infinite server has no capacity to be busy against, so the
2222 // integral itself is the answer.
2223 // Replicas add capacity: r copies of a c-server processor hold r*c
2224 // servers, and the integral above already runs over all of them.
2225 // A task's capacity is its threads, the same way a processor's is its
2226 // servers: both are now integrated above, so both normalise the same.
2227 const bool is_task = (k > lsn.tshift && k <= lsn.tshift + lsn.ntasks);
2228 const std::size_t cap_k = is_task ? task_threads[k] : host_servers[k];
2229 const double c = (cap_k == std::numeric_limits<std::size_t>::max())
2230 ? 1.0
2231 : static_cast<double>(cap_k * repl[k]);
2232 res.ULN(k, 0) = tot_u[k] / (now * c);
2233 res.TLN(k, 0) = completions[k] / now;
2234 }
2235 if (resp_cnt[k] > 0.0) res.RLN(k, 0) = resp_sum[k] / resp_cnt[k];
2236 }
2237 res.entry_resp_samples.swap(entry_resp_samples);
2238
2239 // ResidT, per activity and summed onto its task. SolverLN builds
2240 // residt(aidx) as QN(host)/TN_ref, and TN_ref -- the reference of the
2241 // activity's layer -- is the completion rate of the activity's TASK: the
2242 // cycle rate of a reference task, the request rate into a called one,
2243 // TLN(task) either way. QN(host) is the mean occupancy at the host, which
2244 // is the total host time over the run divided by it, so
2245 //
2246 // ResidT = QN(host) / X_task = (total host time) / (elapsed * X_task)
2247 //
2248 // i.e. host time accumulated per driving visit. An activity executing once
2249 // per request reports its host residence, one inside a loop the loop's
2250 // total, and neither reading needs a visit count of its own. Accumulated
2251 // aside so a task with no activity at all keeps its NaN instead of 0.
2252 if (now > 0.0) {
2253 std::vector<double> task_resid(nidx + 1, 0.0);
2254 std::vector<bool> task_has_act(nidx + 1, false);
2255 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
2256 const std::size_t task = lsn.parent[a];
2257 const double x_task = (task >= 1 && task <= nidx) ? res.TLN(task, 0) : 0.0;
2258 const double resid = (x_task > 0.0) ? act_host_resid[a] / (now * x_task) : 0.0;
2259 res.WLN(a, 0) = resid;
2260 if (task >= 1 && task <= nidx) {
2261 task_resid[task] += resid;
2262 task_has_act[task] = true;
2263 }
2264 }
2265 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks && t <= nidx; ++t)
2266 if (task_has_act[t]) res.WLN(t, 0) = task_resid[t];
2267 }
2268
2269 // ENTRY AND ACTIVITY UTILIZATION ARE DERIVED, the way `Solver_ssj_ln`
2270 // derives them: the utilization an activity causes at its host is its
2271 // throughput times its host demand, an entry's is the sum over the
2272 // activities bound under it, and an activity's own figure is that share of
2273 // its task's total. There is nothing to integrate for either kind -- an
2274 // entry is not a server and holds none -- so the `cur_u` integral that
2275 // answers for hosts and tasks leaves both at zero, which read as idle.
2276 //
2277 // The HOST AND TASK columns are NOT touched here: this port measures them as
2278 // the integral of busy servers and of busy threads, which is a different
2279 // quantity from the JAR's derived task utilization (sum of X*D over the
2280 // task's activities) and the one its own tests pin. The two ports therefore
2281 // still disagree on the task Util column; see `_kb/09-ldes-and-cache.md`.
2282 if (now > 0.0) {
2283 std::vector<double> proc_util(nidx + 1, 0.0), task_proc_util(nidx + 1, 0.0);
2284 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
2285 const double d = (a < lsn.hostdem.size() && !lsn.hostdem[a].disabled)
2286 ? num_traits<T>::to_double(lsn.hostdem[a].mean)
2287 : 0.0;
2288 proc_util[a] = res.TLN(a, 0) * d;
2289 const std::size_t task = lsn.parent[a];
2290 if (task >= 1 && task <= nidx) task_proc_util[task] += proc_util[a];
2291 }
2292 for (std::size_t e = lsn.eshift + 1; e <= lsn.eshift + lsn.nentries && e <= nidx; ++e) {
2293 double u = 0.0;
2294 const std::vector<std::size_t>& acts =
2295 (e < lsn.actsof.size()) ? lsn.actsof[e] : std::vector<std::size_t>();
2296 for (std::size_t i = 0; i < acts.size(); ++i)
2297 if (acts[i] <= nidx) u += proc_util[acts[i]];
2298 res.ULN(e, 0) = u;
2299 }
2300 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
2301 const std::size_t task = lsn.parent[a];
2302 const double tu = (task >= 1 && task <= nidx) ? task_proc_util[task] : 0.0;
2303 if (tu > 0.0 && tu < 1.0)
2304 res.ULN(a, 0) = std::min(1.0, proc_util[a] / tu);
2305 else
2306 res.ULN(a, 0) = (proc_util[a] > 0.0) ? 1.0 : 0.0;
2307 }
2308 }
2309
2310 // Cache measures per ItemEntry, pooled over the replicas of its cache task.
2311 for (std::size_t ai = 0; ai < accesses.size(); ++ai) {
2312 long long reads = 0, hits = 0, misses = 0, delayed = 0;
2313 for (std::size_t ci : accesses[ai].caches) {
2314 reads += caches[ci].reads;
2315 hits += caches[ci].hits;
2316 misses += caches[ci].misses;
2317 delayed += caches[ci].delayed;
2318 }
2319 if (reads <= 0 || !(now > 0.0)) continue;
2320 const double n = static_cast<double>(reads);
2321 res.cache_task.push_back(lsn.parent[accesses[ai].entry]);
2322 res.cache_entry.push_back(accesses[ai].entry);
2323 res.cache_hit.push_back(static_cast<double>(hits) / n);
2324 res.cache_miss.push_back(static_cast<double>(misses) / n);
2325 res.cache_delayed.push_back(static_cast<double>(delayed) / n);
2326 res.cache_read_rate.push_back(n / now);
2327 }
2328
2329 res.simulated_time = now;
2330 res.completions = static_cast<long long>(done);
2331 return res;
2332}
2333
2334} // namespace ldes
2335} // namespace line
2336
2337#endif // LINE_SOLVERS_LDES_LDES_LN_ENGINE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
The option and result records of SolverLDES, the discrete-event simulator.
The variate generators of the native LDES engine.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:472
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
@ HLRU
h-LRU / LRU(m): h lists, promote i -> i+1 on a hit
Definition lang_types.h:385
@ QLRU
q-LRU: LRU with probabilistic admission on a miss
Definition lang_types.h:387
@ LRU
least recently used
Definition lang_types.h:384
LnColumn
A column of the shared layered average table, as line-cli prints it.
static const std::size_t LN_NONE
Index meaning "none" for a line, an invocation or a customer.
static const std::size_t NOTHR
Thread index meaning "no thread held", also used for an infinite-thread task.
static const std::size_t NOTDRAWN
LnJob::calls_left before the repetition count of a call has been drawn.
ThreadState
Power state of one task thread, mirroring Solver_ssj_ln's THREAD_* constants.
bool ln_defined(const lqn::LqnStruct< T > &lsn, const LnResult &r, std::size_t i, LnColumn c)
Does the element at i HAVE the quantity in column c?
std::vector< LnEntryCdf > ldes_ln_cdf_respt(const LnResult &r, std::size_t nentries)
The empirical response time CDF of every ENTRY, the getCdfRespTLN of the other codebases: one [F(t),...
engine::LnResult ldes_ln_engine_solve(const lqn::LqnStruct< T > &lsn, const LdesOptions &o)
Simulate a layered model in process.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The knobs of one LDES run.
long seed
–seed; -1 requests a random stream
std::size_t events
0 = not given; overrides samples when set
std::size_t samples
-s, service-completion budget
One ItemEntry read: its driver activity, its item law and its two branches.
std::vector< std::size_t > caches
cache state per replica of the cache task
Live content of one replica of a cache task, LNCacheState of the Java engine.
lang::ReplacementStrategy rs
std::vector< int > cap
per level list
std::vector< std::vector< std::pair< std::size_t, std::size_t > > > held
(line, access)
std::vector< char > inflight
[item] a fetch is in progress
std::vector< std::list< std::size_t > > levels
per level, most recent first
A reference-task customer: it thinks, then starts an invocation.
double t_start
when the current cycle began
std::size_t repl
replica of the reference task it belongs to
One entry's empirical response-time CDF: F[j] = P(R <= t[j]).
bool operator()(const LnEvent &a, const LnEvent &b) const
One scheduled event of the layered engine.
int kind
0 = host completion (who = the LINE), 1 = think completion (who = the reference customer),...
One invocation of an entry: what a call or a reference cycle starts.
std::size_t caller
the line blocked on this call, if synchronous
std::size_t th
thread held, NOTHR on an INF task
std::size_t root
root line
std::size_t cust
the reference customer, at the top level only
std::size_t ts
task slot of that replica
int tcol
task admission column held, -1 = none
std::map< std::size_t, std::set< std::size_t > > joins
join activity -> inputs arrived
long pending
live lines after a fork, -1 = never forked
double entry_t0
when the entry took its thread, the start of its service
bool started
the entry's service has begun (a thread is held)
bool early_reply
replied before completing (phase 2 follows)
bool fetching
this read is fetching an item (retrieval)
std::size_t trep
replica of the task serving it
double siro_key
uniform key in a SIRO thread queue
One LINE of execution: a point in the activity graph of one invocation.
double act_t0
When the current activity began, -1 before it is reached.
int dem_done
How far the current activity is before its calls: 0 nothing started, 1 host demand started or none,...
int hcol
host admission column held, -1 = none
std::size_t act
current activity, 0 for an entry with none
double host_t0
when the line asked its processor, queueing included
std::size_t call_pos
next call index of the current activity
int pcol
operand column at a pooled host
std::size_t calls_left
Repetitions of the current call still to be made; NOTDRAWN until drawn.
std::size_t inv
the invocation this line belongs to
double remaining
work left at a pooled or PS host
std::uint64_t host_gen
Host service in progress at a queueing host.
double siro_key
uniform key in a SIRO host queue; the smallest is served next
bool branch
a line an AND fork created (never the root)
The layered result: per element, the mean measures.
std::vector< double > cache_read_rate
std::vector< double > cache_miss
Matrix< double > WLN
Residence time per element, the ResidT column: the time an activity holds ITS HOST PROCESSOR per visi...
std::vector< std::size_t > cache_entry
std::vector< double > cache_hit
std::vector< std::vector< double > > entry_resp_samples
Every per-request ENTRY response time observed, one vector per entry in LOCAL index space (0....
std::vector< std::size_t > cache_task
Cache measures, one row per ItemEntry that was read, pooled over the replicas of its cache task as So...
std::vector< double > cache_delayed
Heterogeneous server pools declared on a layer server, the twin of the nservertypes / servertypenames...
Definition lqn_struct.h:79
Matrix< T > compat
(npools x noperands), nonzero = eligible
Definition lqn_struct.h:83
std::vector< double > counts
(npools) servers held by each pool
Definition lqn_struct.h:81
std::vector< T > rates
(npools) per-pool rate multiplier
Definition lqn_struct.h:82
std::size_t npools() const
Definition lqn_struct.h:86