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 THIS INCREMENT COVERS: reference tasks with think time and
31 * multiplicity, entries with sequential activity graphs, host demand under
32 * FCFS / PS / INF host scheduling, task multiplicity as a thread semaphore,
33 * synchronous calls (blocking, with a mean call multiplicity) and asynchronous
34 * calls (fire and forget), open arrivals at an entry, and REPLICATION of a
35 * processor or a task, materialised one queue per copy.
36 *
37 * WHAT IT REFUSES by name: activity graphs with AND/OR forks or loops, cache
38 * tasks, setup/delay-off on a task, and admission constraints.
39 */
40
41#include <algorithm>
42#include <cmath>
43#include <cstddef>
44#include <cstdint>
45#include <deque>
46#include <functional>
47#include <limits>
48#include <map>
49#include <queue>
50#include <string>
51#include <vector>
52
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace ldes {
61namespace engine {
62
63/** One scheduled event of the layered engine. */
64struct LnEvent {
65 double t = 0.0;
66 /**
67 * 0 = host completion, 1 = think completion, 2 = a task thread finished its
68 * cold start, 3 = an idle task thread's delay-off countdown expired.
69 *
70 * For kinds 2 and 3 `who` is the task SLOT and `aux` the thread within it,
71 * and `gen` is the generation the timer was armed with: a countdown caught
72 * by an arriving request is cancelled by bumping the generation, so the
73 * event still fires but finds itself stale and does nothing. The event queue
74 * has no removal.
75 */
76 int kind = 0;
77 std::size_t who = 0;
78 std::size_t aux = 0;
79 std::uint64_t gen = 0;
80 std::uint64_t seq = 0;
81};
82
84 bool operator()(const LnEvent& a, const LnEvent& b) const {
85 if (a.t != b.t) return a.t > b.t;
86 if (a.kind != b.kind) return a.kind > b.kind;
87 return a.seq > b.seq;
88 }
89};
90
91/** One in-flight request: a reference-task job somewhere in its call tree. */
92struct LnJob {
93 std::size_t id = 0;
94 std::size_t ref_task = 0;
95 /** The activity stack: what this job is executing and what it will return to. */
96 std::vector<std::size_t> stack; ///< entry indices, innermost last
97 std::vector<std::size_t> act_pos; ///< next activity of each stacked entry
98 /**
99 * Repetitions of the CURRENT call still to be made, per stack level.
100 *
101 * `NOTDRAWN` means the count has not been sampled yet for this execution of
102 * the activity. An LQN call carries a MEAN NUMBER OF CALLS (`calls-mean`),
103 * not a single dispatch, and the count is drawn once when the call is first
104 * reached -- so `A.synchCall(E, 3)` blocks on E three times per execution of
105 * A, which is three times the callee's demand in the caller's cycle.
106 */
107 std::vector<std::size_t> calls_left;
108 std::vector<std::size_t> call_pos; ///< next call index of the current activity
109 /** When each stacked entry was entered, for its response time and occupancy. */
110 std::vector<double> entry_t0;
111 /**
112 * When the current activity of each level began; -1 between activities.
113 *
114 * An activity's span runs from the moment it becomes current to the moment
115 * its host demand completes, so it CARRIES THE NESTED CALLS it makes -- that
116 * is what makes it a response time and not a residence.
117 */
118 std::vector<double> act_t0;
119 /**
120 * Replica of the task owning each stacked entry. A call picks the callee
121 * replica once and the request stays on it until it replies, so the whole
122 * of one invocation runs on one copy of the callee and its processor.
123 */
124 std::vector<std::size_t> repl_of;
125 /**
126 * The task thread each stacked entry holds. A caller keeps its thread across
127 * a synchronous call, so every level of the stack holds one of its own task's
128 * threads at once, and that is the layered contention the model represents.
129 * `NOTHR` marks a level whose thread is not yet held (it is queueing, or its
130 * thread is powering up) or a level on an infinite-thread task.
131 */
132 std::vector<std::size_t> thr_slot;
133 std::vector<std::size_t> thr_id;
134 double t_start = 0.0; ///< when the current top-level invocation began
135 /**
136 * When this job last asked its host processor for service -- the instant
137 * the residence at the host starts, queueing included. A job is at one host
138 * at a time, so one scalar carries it.
139 */
140 double host_t0 = 0.0;
141 bool holding_thread = false;
142 std::size_t thread_task = 0;
143};
144
145/** Thread index meaning "no thread held", also used for an infinite-thread task. */
146static const std::size_t NOTHR = static_cast<std::size_t>(-1);
147
148/** `LnJob::calls_left` before the repetition count of a call has been drawn. */
149static const std::size_t NOTDRAWN = static_cast<std::size_t>(-1);
150
151/** Power state of one task thread, mirroring Solver_ssj_ln's THREAD_* constants. */
153
154/** The layered result: per element, the mean measures. */
155struct LnResult {
156 std::size_t nidx = 0;
158 /**
159 * Residence time per element, the ResidT column: the time an activity holds
160 * ITS HOST PROCESSOR per visit of the request stream driving its task, and
161 * for a task the sum over its activities.
162 *
163 * NOT the response time RLN, which also carries the nested synchronous
164 * calls the activity makes. NaN at the element kinds that have no residence
165 * -- processors and entries -- because a zero residence is a claim (nothing
166 * is held there) and this is the absence of one, the same convention
167 * SolverLN and LQNS report.
168 */
170 /**
171 * Every per-request ENTRY response time observed, one vector per entry in
172 * LOCAL index space (0..nentries-1). `RLN` at the entry indices is their
173 * mean; these are the observations themselves, which is what an empirical
174 * CDF has to be built from -- a law fitted to the mean says nothing about
175 * the tail, and the tail is the reason to ask a simulator at all.
176 */
177 std::vector<std::vector<double> > entry_resp_samples;
178 double simulated_time = 0.0;
179 long long completions = 0;
180};
181
182/** One entry's empirical response-time CDF: F[j] = P(R <= t[j]). */
184 std::vector<double> t;
185 std::vector<double> F;
186};
187
188/**
189 * The empirical response time CDF of every ENTRY, the `getCdfRespTLN` of the
190 * other codebases: one [F(t), t] table per entry in the LOCAL index space,
191 * empty where the run observed nothing.
192 *
193 * The ecdf of each entry's observations: sort, step by 1/n, and collapse ties
194 * keeping the LARGEST value at each distinct time, so an interpolating reader
195 * cannot land on a multivalued point.
196 */
197inline std::vector<LnEntryCdf> ldes_ln_cdf_respt(const LnResult& r, std::size_t nentries) {
198 std::vector<LnEntryCdf> out(nentries);
199 for (std::size_t e = 0; e < nentries; ++e) {
200 if (e >= r.entry_resp_samples.size()) continue;
201 std::vector<double> x = r.entry_resp_samples[e];
202 if (x.empty()) continue;
203 std::sort(x.begin(), x.end());
204 const double n = static_cast<double>(x.size());
205 for (std::size_t i = 0; i < x.size(); ++i) {
206 if (i + 1 < x.size() && x[i + 1] == x[i]) continue;
207 out[e].t.push_back(x[i]);
208 out[e].F.push_back(static_cast<double>(i + 1) / n);
209 }
210 }
211 return out;
212}
213
214/** A column of the shared layered average table, as `line-cli` prints it. */
215enum class LnColumn { QLen, Util, RespT, ResidT, Tput };
216
217/**
218 * Does the element at `i` HAVE the quantity in column `c`?
219 *
220 * THE MASK BELONGS TO THE TABLE, not to whichever engine filled it. A NaN there
221 * is not a failed computation: it says the quantity is not defined for that
222 * element kind, and SolverLN and LQNS report the same one --
223 *
224 * Processor QLen no, Util yes, RespT no, ResidT no, Tput no
225 * Task QLen yes, Util yes, RespT no, ResidT yes, Tput yes
226 * Entry QLen yes, Util yes, RespT yes, ResidT no, Tput yes
227 * Activity all five
228 *
229 * -- so a reader can diff the arms row by row. This engine MEASURES more than
230 * that: a processor's completion rate is sitting in `TLN` and an entry's
231 * occupancy in `QLN`, both perfectly well defined as sample-path quantities.
232 * Reporting them where every other solver says NaN would make one column mean
233 * different things depending on who filled it, which is the divergence the JAR
234 * removed from `getLNAvgTable` on 2026-08-21. Nothing is discarded: the caller
235 * still has the whole `LnResult`; only the SHARED TABLE is masked.
236 *
237 * ResidT needs no rule of its own here because the engine already applies it:
238 * `WLN` starts at NaN and only activities and their tasks are written, so a
239 * task with no activity at all keeps its NaN rather than claiming a residence
240 * of zero (which would say something is held there for no time, not that
241 * nothing is held).
242 *
243 * Twin of the mask `jline.solvers.ldes.SolverLDES.getLNAvgTable` applies and of
244 * the `defined_*` flags of `ln::LnSolution`; the three are pinned against each
245 * other by `cpp/tests/test_lqn_nan_mask_parity.cpp`.
246 */
247template <class T>
248inline bool ln_defined(const lqn::LqnStruct<T>& lsn, const LnResult& r, std::size_t i,
249 LnColumn c) {
250 if (i < 1 || i > r.nidx || i >= lsn.type.size()) return false;
251 const bool host = lsn.type[i] == line::lang::LqnElement::HOST;
252 switch (c) {
253 case LnColumn::QLen: return !host;
254 case LnColumn::Util: return true;
255 case LnColumn::RespT:
256 return lsn.type[i] == line::lang::LqnElement::ENTRY ||
258 case LnColumn::ResidT: return !std::isnan(r.WLN(i, 0));
259 case LnColumn::Tput: return !host;
260 }
261 return false;
262}
263
264} // namespace engine
265
266/**
267 * Simulate a layered model in process.
268 *
269 * The budget is ENTRY COMPLETIONS, mirroring the flat engine's service
270 * completions: a layered model has no single notion of "a job leaving", so the
271 * count of entry invocations that returned is what a horizon can be set on.
272 */
273template <class T>
275 using namespace engine;
277
278 const std::size_t nidx = lsn.nidx;
279 if (nidx == 0) throw InputError("SolverLDES (native LN engine): empty layered model");
280
281 // ---- refuse what this increment does not simulate -----------------------
282 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
283 if (t < lsn.iscache.size() && lsn.iscache[t])
284 throw UnsupportedError("SolverLDES (native LN engine): cache task '" + lsn.names[t] +
285 "' is not ported yet");
286 // A setup IS simulated now, except on an infinite-thread task, which holds
287 // no thread to power down. Same rule as the Java engine.
288 if (t < lsn.hassetup.size() && lsn.hassetup[t]) {
289 const double m = (t < lsn.mult.size()) ? lsn.mult[t] : 1.0;
290 if (!std::isfinite(m) || (t < lsn.sched.size() && lsn.sched[t] == lang::SchedStrategy::INF))
291 throw UnsupportedError("SolverLDES (native LN engine): task '" + lsn.names[t] +
292 "' declares a setup time on an infinite-server task, which "
293 "holds no thread to power down; give it a finite "
294 "multiplicity");
295 }
296 }
297 // The Java engine (Solver_ssj_ln) admits requests against A n <= b and parks
298 // the ones that would breach it; this engine does not, so it must say so
299 // rather than return the unconstrained sample path under a constrained model.
300 for (std::size_t k = 1; k < lsn.lincon_A.size(); ++k) {
301 if (lsn.lincon_A[k].rows() > 0)
302 throw UnsupportedError("SolverLDES (native LN engine): the admission constraint on '" +
303 lsn.names[k] + "' is not ported yet; solve this model with the "
304 "Java LDES engine");
305 }
306 // Heterogeneous server pools on a layer server. The Java engine holds them
307 // concretely -- a request occupies one server of one compatible pool, and
308 // under PS the per-job rates are the max-min fair allocation of
309 // SnCompatShare -- so a model that declares them is a DIFFERENT system from
310 // the multiserver it would otherwise look like. Flattening them into the
311 // multiplicity here would finish and report plausible numbers for that other
312 // system, which is the failure this validation exists to prevent.
313 for (std::size_t k = 0; k < lsn.pools.size(); ++k) {
314 if (lsn.pools[k].npools() > 0)
315 throw UnsupportedError("SolverLDES (native LN engine): '" + lsn.names[k] +
316 "' declares heterogeneous server pools, which are not ported "
317 "yet; solve this model with the Java LDES engine");
318 }
319 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts; ++a) {
320 if (a < lsn.actpretype.size() && lsn.actpretype[a] != lang::PrecedenceType::NONE &&
321 lsn.actpretype[a] != lang::PrecedenceType::PRE_SEQ)
322 throw UnsupportedError("SolverLDES (native LN engine): activity '" + lsn.names[a] +
323 "' has a non-sequential precedence, which is not ported yet");
324 if (a < lsn.actposttype.size() && lsn.actposttype[a] != lang::PrecedenceType::NONE &&
325 lsn.actposttype[a] != lang::PrecedenceType::POST_SEQ)
326 throw UnsupportedError("SolverLDES (native LN engine): activity '" + lsn.names[a] +
327 "' has a non-sequential post-precedence, which is not ported "
328 "yet");
329 }
330
331 const std::uint64_t max_events =
332 static_cast<std::uint64_t>(o.events > 0 ? o.events : o.samples);
333 const std::uint64_t base =
334 (o.seed >= 0) ? static_cast<std::uint64_t>(o.seed) : std::random_device{}();
335 /**
336 * ONE STREAM PER ACTIVITY, as the flat engine now does per (node, class).
337 *
338 * The layered reference indexes its generators by the LQN entity rather
339 * than by a station, so the offsets here are built the same way from the
340 * activity index: host demands in the service band (+1000) and think times
341 * in the arrival band, which is what those two are. Sharing one stream
342 * across every activity interleaves draws that the reference keeps apart,
343 * and the divergence is immediate rather than statistical.
344 */
345 const long long ln_seed = static_cast<long long>(base);
346 std::vector<Rng> g_host, g_think;
347 g_host.reserve(nidx + 1);
348 g_think.reserve(nidx + 1);
349 for (std::size_t k = 0; k <= nidx; ++k) {
350 g_host.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 1000));
351 g_think.push_back(Rng(ln_seed, static_cast<long long>(k) * 10));
352 }
353 Rng g_call(ln_seed, 900000);
354
355 // ---- resolved model -----------------------------------------------------
356 std::vector<Sampler> hostdem(nidx + 1), think(nidx + 1);
357 std::vector<bool> has_hostdem(nidx + 1, false), has_think(nidx + 1, false);
358 for (std::size_t k = 1; k <= nidx; ++k) {
359 if (k < lsn.hostdem.size() && !lsn.hostdem[k].disabled &&
360 num_traits<T>::to_double(lsn.hostdem[k].mean) > 0.0) {
361 hostdem[k] = Sampler(lsn.hostdem[k], "the host demand of '" + lsn.names[k] + "'");
362 has_hostdem[k] = true;
363 }
364 if (k < lsn.think.size() && !lsn.think[k].disabled &&
365 num_traits<T>::to_double(lsn.think[k].mean) > 0.0) {
366 think[k] = Sampler(lsn.think[k], "the think time of '" + lsn.names[k] + "'");
367 has_think[k] = true;
368 }
369 }
370
371 // Host servers and task threads: the two resources an activity holds.
372 std::vector<std::size_t> host_servers(nidx + 1, 1), task_threads(nidx + 1, 1);
373 std::vector<SchedStrategy> host_sched(nidx + 1, SchedStrategy::FCFS);
374 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h) {
375 const double m = (h < lsn.mult.size()) ? lsn.mult[h] : 1.0;
376 host_servers[h] = std::isfinite(m) ? static_cast<std::size_t>(m + 0.5)
377 : std::numeric_limits<std::size_t>::max();
378 if (h < lsn.sched.size()) host_sched[h] = lsn.sched[h];
379 }
380 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
381 const double m = (t < lsn.mult.size()) ? lsn.mult[t] : 1.0;
382 task_threads[t] = std::isfinite(m) ? static_cast<std::size_t>(m + 0.5)
383 : std::numeric_limits<std::size_t>::max();
384 }
385
386 /**
387 * REPLICATION. A processor or task declared with replication r is r
388 * identical copies of itself, and a copy is a server of its own: pooling
389 * them into one server of r times the capacity would let one queue absorb
390 * what r separate queues cannot. So the host state below is indexed by a
391 * SLOT -- the processor plus its replica -- while what the copies share by
392 * definition (servers per copy, scheduling) stays indexed by the element.
393 * The reported measures are summed over the copies, which keeps throughput
394 * conserved across a call and utilization a fraction of the total capacity.
395 */
396 std::vector<std::size_t> repl(nidx + 1, 1);
397 for (std::size_t k = 1; k < lsn.repl.size() && k <= nidx; ++k) {
398 const double r = lsn.repl[k];
399 repl[k] = (r > 1.0) ? static_cast<std::size_t>(r + 0.5) : 1;
400 }
401 std::vector<std::size_t> host_slot0(nidx + 1, 0);
402 std::size_t nhost_slots = 0;
403 for (std::size_t h = lsn.hshift + 1; h <= lsn.hshift + lsn.nhosts; ++h) {
404 host_slot0[h] = nhost_slots;
405 nhost_slots += repl[h];
406 }
407 if (nhost_slots == 0) nhost_slots = 1;
408
409 /** Slot of the processor replica running replica trep of one of its tasks. */
410 auto host_slot = [&](std::size_t host, std::size_t trep) {
411 return host_slot0[host] + (trep % repl[host]);
412 };
413
414 // Task replicas get their own slots for exactly the reason processors do: r
415 // copies are r thread pools, not one pool of r times the size.
416 std::vector<std::size_t> task_slot0(nidx + 1, 0);
417 std::size_t ntask_slots = 0;
418 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
419 task_slot0[t] = ntask_slots;
420 ntask_slots += repl[t];
421 }
422 if (ntask_slots == 0) ntask_slots = 1;
423 auto task_slot = [&](std::size_t task, std::size_t trep) {
424 return task_slot0[task] + (trep % repl[task]);
425 };
426
427 /**
428 * Replica of the callee reached by one call of replica `crep` of the caller.
429 * An unset fan-out is the smallest value consistent with
430 * repl(caller)*fanout = repl(callee)*fanin, and the caller reaches the block
431 * {(i*f+k) mod r}. A call is one indivisible unit of work, so it goes to one
432 * member of that block drawn uniformly: over many calls each member carries
433 * the 1/f share that LQN2QN splits the call mean into. This is deterministic
434 * pairing at f=1 and a uniform spread over every replica at f=r.
435 */
436 auto callee_replica = [&](std::size_t caller_task, std::size_t crep,
437 std::size_t callee_task) -> std::size_t {
438 const std::size_t rb = repl[callee_task];
439 if (rb <= 1) return 0;
440 std::size_t f = static_cast<std::size_t>(lsn.fanout_at(caller_task, callee_task) + 0.5);
441 if (f == 0) {
442 const std::size_t ra = repl[caller_task];
443 f = (rb > ra) ? std::max<std::size_t>(1, rb / ra) : 1;
444 }
445 f = std::min(std::max<std::size_t>(1, f), rb);
446 const std::size_t k =
447 (f > 1) ? static_cast<std::size_t>(uniform01(g_call) * static_cast<double>(f)) : 0;
448 return ((crep * f) + std::min(k, f - 1)) % rb;
449 };
450
451 // ---- live state ---------------------------------------------------------
452 double now = 0.0;
453 std::priority_queue<LnEvent, std::vector<LnEvent>, LnEventLater> evq;
454 std::uint64_t seq = 0;
455 std::map<std::size_t, LnJob> jobs;
456 std::size_t next_job = 0;
457
458 // Busy servers and waiting jobs of ONE processor replica, addressed by slot.
459 std::vector<std::size_t> host_busy(nhost_slots, 0);
460 std::vector<std::deque<std::size_t>> host_queue(nhost_slots);
461 std::vector<std::size_t> running_on(nhost_slots, 0); ///< job currently on a host slot
462
463 // Task threads of ONE task replica, addressed by slot. A request that finds
464 // every thread taken waits in thr_queue, holding no resource meanwhile.
465 std::vector<std::vector<bool>> thr_held(ntask_slots);
466 std::vector<std::vector<ThreadState>> thr_state(ntask_slots);
467 std::vector<std::vector<double>> thr_setup_t0(ntask_slots);
468 std::vector<std::vector<std::uint64_t>> thr_gen(ntask_slots);
469 std::vector<std::deque<std::size_t>> thr_queue(ntask_slots);
470 std::vector<std::size_t> thr_owner(ntask_slots, 0); ///< task index of each slot
471 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
472 const std::size_t n = (task_threads[t] == std::numeric_limits<std::size_t>::max())
473 ? 0 : task_threads[t];
474 for (std::size_t m = 0; m < repl[t]; ++m) {
475 const std::size_t ts = task_slot(t, m);
476 thr_owner[ts] = t;
477 thr_held[ts].assign(n, false);
478 thr_state[ts].assign(n, ThreadState::ACTIVE);
479 thr_setup_t0[ts].assign(n, 0.0);
480 thr_gen[ts].assign(n, 0);
481 }
482 }
483
484 // A SetupTask's two clocks, per task. Both must be positive for the power
485 // cycle to exist: a setup with no delay-off would never fire, because nothing
486 // would ever power a thread down.
487 std::vector<Sampler> setupd(nidx + 1), delayoffd(nidx + 1);
488 std::vector<bool> has_setup(nidx + 1, false);
489 std::vector<Rng> g_setup, g_doff;
490 g_setup.reserve(nidx + 1);
491 g_doff.reserve(nidx + 1);
492 for (std::size_t k = 0; k <= nidx; ++k) {
493 g_setup.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 2000));
494 g_doff.push_back(Rng(ln_seed, static_cast<long long>(k) * 10 + 3000));
495 }
496 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
497 if (t >= lsn.hassetup.size() || !lsn.hassetup[t]) continue;
498 const bool su = t < lsn.setuptime.size() && !lsn.setuptime[t].disabled &&
499 num_traits<T>::to_double(lsn.setuptime[t].mean) > 0.0;
500 const bool df = t < lsn.delayofftime.size() && !lsn.delayofftime[t].disabled &&
501 num_traits<T>::to_double(lsn.delayofftime[t].mean) > 0.0;
502 if (!su || !df) continue;
503 setupd[t] = Sampler(lsn.setuptime[t], "the setup time of '" + lsn.names[t] + "'");
504 delayoffd[t] = Sampler(lsn.delayofftime[t], "the delay-off time of '" + lsn.names[t] + "'");
505 has_setup[t] = true;
506 }
507
508 /// Jobs woken by a thread release, continued after the current walk returns.
509 /// Waking them inside the walk would re-enter it on another job while this
510 /// one still holds a reference into `jobs`.
511 std::deque<std::size_t> runnable;
512
513 // Time integrals per element.
514 std::vector<double> tot_q(nidx + 1, 0.0), tot_u(nidx + 1, 0.0);
515 std::vector<double> cur_q(nidx + 1, 0.0), cur_u(nidx + 1, 0.0);
516 std::vector<double> last_upd(nidx + 1, 0.0);
517 std::vector<double> completions(nidx + 1, 0.0), resp_sum(nidx + 1, 0.0),
518 resp_cnt(nidx + 1, 0.0);
519 // The observations behind `resp_sum` at the ENTRY indices, kept for
520 // get_cdf_respt. Only entries: an activity's and a task's response times are
521 // means the layered table reports, with no distributional getter on them.
522 std::vector<std::vector<double> > entry_resp_samples(lsn.nentries);
523 // Total time the executions of an activity spend AT ITS HOST PROCESSOR --
524 // queueing there plus running. Over the elapsed time this is the mean
525 // occupancy at the host, which is what ResidT is built from below.
526 // MEASURED rather than derived from the nominal `hostdem` mean: that mean is
527 // the SERVICE time, and on a contended processor the residence is that plus
528 // the wait, so a derived figure would report an uncontended residence at
529 // exactly the models where it matters.
530 std::vector<double> act_host_resid(nidx + 1, 0.0);
531
532 // ONE STREAM PER CALL for the repetition count, on the engine's own offset
533 // block (4000), so that adding a call does not shift the variates of the
534 // think times or the host demands and re-baseline every seeded result.
535 std::vector<Rng> g_callmult;
536 g_callmult.reserve(lsn.ncalls + 1);
537 for (std::size_t c = 0; c <= lsn.ncalls; ++c)
538 g_callmult.push_back(Rng(ln_seed, static_cast<long long>(c) * 10 + 4000));
539
540 /**
541 * How many times one execution of the caller makes call `cidx`.
542 *
543 * `floor(mean)` calls plus one more with probability equal to the fraction,
544 * which is `Solver_ssj_ln.sampleCallCount` exactly. It is DETERMINISTIC at
545 * an integer mean, which is what `calls-mean="3"` asks for: three calls
546 * every time, not three on average.
547 */
548 auto sample_call_count = [&](std::size_t cidx) -> std::size_t {
549 const double m = (cidx < lsn.callproc_mean.size())
550 ? num_traits<T>::to_double(lsn.callproc_mean[cidx])
551 : 1.0;
552 if (!(m > 0.0)) return 0;
553 std::size_t n = static_cast<std::size_t>(std::floor(m));
554 const double frac = m - static_cast<double>(n);
555 if (frac > 0.0 && cidx < g_callmult.size() && uniform01(g_callmult[cidx]) < frac) ++n;
556 return n;
557 };
558
559 auto touch = [&](std::size_t k) {
560 const double dt = now - last_upd[k];
561 if (dt > 0.0) {
562 tot_q[k] += cur_q[k] * dt;
563 tot_u[k] += cur_u[k] * dt;
564 }
565 last_upd[k] = now;
566 };
567 auto push_ev = [&](LnEvent e) {
568 e.seq = seq++;
569 evq.push(e);
570 };
571
572 std::uint64_t done = 0;
573
574 // ---- the activity walk --------------------------------------------------
575 std::function<void(std::size_t)> advance;
576 /** Continue every job a thread release woke, and every job those wake. */
577 auto drain = [&]() {
578 while (!runnable.empty()) {
579 const std::size_t id = runnable.front();
580 runnable.pop_front();
581 advance(id);
582 }
583 };
584
585 /** Put `job` on its host replica, queueing when every server of it is taken. */
586 auto start_host = [&](std::size_t jid, std::size_t act, std::size_t trep) {
587 const std::size_t task = lsn.parent[act];
588 const std::size_t host = lsn.host_of(act);
589 const std::size_t hs = host_slot(host, trep);
590 touch(host);
591 cur_q[host] += 1.0;
592 // Stamped BEFORE the branch: a request that has to queue starts its
593 // residence when it asks, not when a server frees up.
594 jobs[jid].host_t0 = now;
595 if (host_busy[hs] < host_servers[host]) {
596 ++host_busy[hs];
597 cur_u[host] += 1.0;
598 running_on[hs] = jid;
599 LnEvent e;
600 e.t = now + hostdem[act].next(g_host[act]);
601 e.kind = 0;
602 e.who = jid;
603 push_ev(e);
604 } else {
605 host_queue[hs].push_back(jid);
606 }
607 (void)task;
608 };
609
610 /** Arm THREAD's cold start; the request that woke it waits in thr_queue. */
611 auto start_setup = [&](std::size_t ts, std::size_t th) {
612 const std::size_t task = thr_owner[ts];
613 thr_state[ts][th] = ThreadState::SETUP;
614 thr_setup_t0[ts][th] = now;
615 LnEvent e;
616 e.t = now + setupd[task].next(g_setup[task]);
617 e.kind = 2;
618 e.who = ts;
619 e.aux = th;
620 e.gen = ++thr_gen[ts][th];
621 push_ev(e);
622 };
623
624 /** Arm THREAD's idle countdown; when it expires the thread is OFF. */
625 auto start_delayoff = [&](std::size_t ts, std::size_t th) {
626 const std::size_t task = thr_owner[ts];
627 thr_state[ts][th] = ThreadState::DELAYOFF;
628 LnEvent e;
629 e.t = now + delayoffd[task].next(g_doff[task]);
630 e.kind = 3;
631 e.who = ts;
632 e.aux = th;
633 e.gen = ++thr_gen[ts][th];
634 push_ev(e);
635 };
636
637 /**
638 * Give `jid` a thread of the task owning the entry on top of its stack.
639 *
640 * Returns false when the job could not proceed: either every thread is taken,
641 * or the only thread available was powered off and is now warming up. Either
642 * way the job is parked in thr_queue and will be continued by whoever frees
643 * or finishes warming a thread. An infinite-thread task never blocks and
644 * holds no identified thread.
645 */
646 auto acquire_thread = [&](std::size_t jid, std::size_t entry) -> bool {
647 LnJob& j = jobs[jid];
648 const std::size_t task = lsn.parent[entry];
649 const std::size_t trep = j.repl_of.back();
650 const std::size_t ts = task_slot(task, trep);
651 touch(task);
652 if (task_threads[task] == std::numeric_limits<std::size_t>::max()) {
653 cur_u[task] += 1.0;
654 j.thr_slot.back() = ts;
655 j.thr_id.back() = NOTHR;
656 return true;
657 }
658 std::vector<bool>& held = thr_held[ts];
659 std::vector<ThreadState>& st = thr_state[ts];
660 // An ACTIVE idle thread first, then one still counting down -- caught
661 // before it powers off, it serves having paid nothing -- and only then a
662 // thread that is already OFF, which must warm up first.
663 for (std::size_t th = 0; th < held.size(); ++th) {
664 if (!held[th] && st[th] == ThreadState::ACTIVE) {
665 held[th] = true;
666 cur_u[task] += 1.0;
667 j.thr_slot.back() = ts;
668 j.thr_id.back() = th;
669 return true;
670 }
671 }
672 for (std::size_t th = 0; th < held.size(); ++th) {
673 if (!held[th] && st[th] == ThreadState::DELAYOFF) {
674 ++thr_gen[ts][th]; // cancel the countdown
675 st[th] = ThreadState::ACTIVE;
676 held[th] = true;
677 cur_u[task] += 1.0;
678 j.thr_slot.back() = ts;
679 j.thr_id.back() = th;
680 return true;
681 }
682 }
683 thr_queue[ts].push_back(jid);
684 for (std::size_t th = 0; th < held.size(); ++th) {
685 if (!held[th] && st[th] == ThreadState::OFF) {
686 start_setup(ts, th);
687 break;
688 }
689 }
690 return false;
691 };
692
693 /** Free the thread the innermost entry of `jid` holds and wake the next job. */
694 auto release_thread = [&](std::size_t jid) {
695 LnJob& j = jobs[jid];
696 const std::size_t ts = j.thr_slot.back();
697 const std::size_t th = j.thr_id.back();
698 const std::size_t task = thr_owner[ts];
699 touch(task);
700 cur_u[task] -= 1.0;
701 j.thr_slot.back() = NOTHR;
702 j.thr_id.back() = NOTHR;
703 if (th == NOTHR) return; // an infinite-thread task holds nothing
704 thr_held[ts][th] = false;
705 if (!thr_queue[ts].empty()) {
706 const std::size_t nxt = thr_queue[ts].front();
707 thr_queue[ts].pop_front();
708 LnJob& nj = jobs[nxt];
709 thr_held[ts][th] = true;
710 cur_u[task] += 1.0;
711 nj.thr_slot.back() = ts;
712 nj.thr_id.back() = th;
713 runnable.push_back(nxt);
714 } else if (has_setup[task]) {
715 start_delayoff(ts, th);
716 }
717 };
718
719 /**
720 * Continue `jid` from wherever it is: run the next activity of the innermost
721 * entry, issue its next call, or return to the caller.
722 */
723 /**
724 * One execution of `act` has finished.
725 *
726 * ITS RESPONSE TIME CARRIES THE CALLS IT MADE, which is what separates it
727 * from the residence `act_host_resid` accumulates: on an activity that calls
728 * a server three times, the response is its own demand plus the three
729 * replies and the residence is its own demand alone. The occupancy is closed
730 * off through `cur_q` at the same instant, so QLen, RespT and Tput satisfy
731 * Little's law by construction rather than by three separate estimators
732 * happening to agree.
733 */
734 auto finish_act = [&](LnJob& j, std::size_t act) {
735 const double t0 = j.act_t0.empty() ? -1.0 : j.act_t0.back();
736 completions[act] += 1.0;
737 if (t0 >= 0.0) {
738 resp_sum[act] += now - t0;
739 resp_cnt[act] += 1.0;
740 touch(act);
741 cur_q[act] -= 1.0;
742 }
743 if (!j.act_t0.empty()) j.act_t0.back() = -1.0;
744 };
745
746 advance = [&](std::size_t jid) {
747 LnJob& j = jobs[jid];
748 while (true) {
749 if (j.stack.empty()) {
750 // The invocation is finished: the reference task thinks again.
751 //
752 // THE COMPLETION IS NOT COUNTED HERE. The reference task's own
753 // entry just replied, and the reply branch below already charged
754 // `completions[parent(entry)]`, which for a reference task IS
755 // this task -- so counting it again reported TWICE the cycle
756 // rate for every reference task, and with it half the residence
757 // of every activity on one (ResidT divides by TLN(task)).
758 // `resp_sum` is the CYCLE response time and is the task's own,
759 // so it does belong here.
760 const std::size_t rt = j.ref_task;
761 resp_sum[rt] += now - j.t_start;
762 resp_cnt[rt] += 1.0;
763 ++done;
764 LnEvent e;
765 e.t = now + (has_think[rt] ? think[rt].next(g_think[rt]) : 0.0);
766 e.kind = 1;
767 e.who = jid;
768 push_ev(e);
769 return;
770 }
771 const std::size_t entry = j.stack.back();
772 // A request holds a THREAD of the entry's task for the whole entry,
773 // across its calls, and waits outside when none is free.
774 if (j.thr_slot.back() == NOTHR && j.thr_id.back() == NOTHR) {
775 if (!acquire_thread(jid, entry)) return;
776 }
777 const std::vector<std::size_t>& acts =
778 (entry < lsn.actsof.size()) ? lsn.actsof[entry] : std::vector<std::size_t>();
779 if (j.act_pos.back() >= acts.size()) {
780 // Every activity of this entry has run: the entry replies.
781 //
782 // AN ENTRY'S RESPONSE TIME IS THE WHOLE INVOCATION, from the
783 // instant the request reached it -- queueing for a thread of its
784 // task included -- to this reply, and so carries every nested
785 // call its activities made. Its occupancy follows from the same
786 // interval through `cur_q`, which is Little's law rather than a
787 // second measurement of the same thing.
788 completions[entry] += 1.0;
789 const double e0 = j.entry_t0.back();
790 resp_sum[entry] += now - e0;
791 resp_cnt[entry] += 1.0;
792 // 1-BASED element indices in this engine, unlike the JAR and
793 // python ports: entries are eshift+1 .. eshift+nentries, so the
794 // local index is one less than the difference.
795 if (entry > lsn.eshift && entry <= lsn.eshift + lsn.nentries) {
796 entry_resp_samples[entry - lsn.eshift - 1].push_back(now - e0);
797 }
798 touch(entry);
799 cur_q[entry] -= 1.0;
800 const std::size_t task = lsn.parent[entry];
801 completions[task] += 1.0;
802 touch(task);
803 cur_q[task] -= 1.0;
804 release_thread(jid);
805 j.stack.pop_back();
806 j.act_pos.pop_back();
807 j.call_pos.pop_back();
808 j.calls_left.pop_back();
809 j.entry_t0.pop_back();
810 j.act_t0.pop_back();
811 j.repl_of.pop_back();
812 j.thr_slot.pop_back();
813 j.thr_id.pop_back();
814 continue;
815 }
816 const std::size_t act = acts[j.act_pos.back()];
817 // The activity begins the first time it is reached and stays open
818 // across its calls, so `act_t0` is set once and cleared when it
819 // completes. -1 marks the gap between two activities of one entry.
820 if (j.act_t0.back() < 0.0) {
821 j.act_t0.back() = now;
822 touch(act);
823 cur_q[act] += 1.0;
824 }
825 const std::vector<std::size_t>& calls =
826 (act < lsn.callsof.size()) ? lsn.callsof[act] : std::vector<std::size_t>();
827 if (j.call_pos.back() < calls.size()) {
828 // A pending call of the current activity. HOW MANY TIMES it is
829 // made is `calls-mean`, drawn once per execution: walking the
830 // call LIST and issuing each entry once ignored the multiplicity
831 // entirely, so `synchCall(E, 3)` blocked on E once and every
832 // number on such a model was computed on the wrong cycle.
833 const std::size_t cidx = calls[j.call_pos.back()];
834 if (j.calls_left.back() == NOTDRAWN)
835 j.calls_left.back() = sample_call_count(cidx);
836 if (j.calls_left.back() == 0) {
837 // A mean below 1 can draw zero: this call is not made at all
838 // on this execution.
839 ++j.call_pos.back();
840 j.calls_left.back() = NOTDRAWN;
841 continue;
842 }
843 --j.calls_left.back();
844 if (j.calls_left.back() == 0) {
845 ++j.call_pos.back();
846 j.calls_left.back() = NOTDRAWN;
847 }
848 const std::size_t callee = lsn.callpair_dst[cidx];
849 const bool sync = (lsn.calltype[cidx] == lang::CallType::SYNC);
850 if (sync) {
851 // THE CALLER KEEPS ITS THREAD across the call. That is the
852 // layered contention the model exists to represent.
853 j.stack.push_back(callee);
854 j.act_pos.push_back(0);
855 j.call_pos.push_back(0);
856 j.calls_left.push_back(NOTDRAWN);
857 j.entry_t0.push_back(now);
858 j.act_t0.push_back(-1.0);
859 j.repl_of.push_back(callee_replica(lsn.parent[entry], j.repl_of.back(),
860 lsn.parent[callee]));
861 j.thr_slot.push_back(NOTHR);
862 j.thr_id.push_back(NOTHR);
863 touch(lsn.parent[callee]);
864 cur_q[lsn.parent[callee]] += 1.0;
865 touch(callee);
866 cur_q[callee] += 1.0;
867 continue;
868 }
869 // Asynchronous: the callee runs on its own, the caller does not
870 // wait, and the reply is never collected.
871 completions[callee] += 1.0;
872 continue;
873 }
874 // The activity's calls are done; run its host demand, if any.
875 ++j.act_pos.back();
876 j.call_pos.back() = 0;
877 j.calls_left.back() = NOTDRAWN;
878 if (has_hostdem[act]) {
879 start_host(jid, act, j.repl_of.back());
880 return;
881 }
882 // No host demand: the activity ends here rather than at a host
883 // completion, and still has a response time -- the calls it made.
884 finish_act(j, act);
885 }
886 };
887
888 // ---- reference tasks ----------------------------------------------------
889 // Each reference task runs `mult` independent jobs, which is what makes the
890 // closed population of a layered model.
891 bool any_ref = false;
892 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks; ++t) {
893 if (t >= lsn.isref.size() || !lsn.isref[t]) continue;
894 any_ref = true;
895 const std::size_t n = (task_threads[t] == std::numeric_limits<std::size_t>::max())
896 ? 1
897 : task_threads[t];
898 const std::vector<std::size_t>& es =
899 (t < lsn.entriesof.size()) ? lsn.entriesof[t] : std::vector<std::size_t>();
900 if (es.empty())
901 throw InputError("SolverLDES (native LN engine): reference task '" + lsn.names[t] +
902 "' declares no entry to invoke");
903 // A replicated reference task is r separate populations of `mult`
904 // customers, not one population of r*mult sharing a queue.
905 for (std::size_t m = 0; m < repl[t]; ++m) {
906 for (std::size_t k = 0; k < n; ++k) {
907 LnJob j;
908 j.id = ++next_job;
909 j.ref_task = t;
910 j.t_start = 0.0;
911 j.stack.push_back(es[0]);
912 j.act_pos.push_back(0);
913 j.call_pos.push_back(0);
914 j.calls_left.push_back(NOTDRAWN);
915 j.entry_t0.push_back(now);
916 j.act_t0.push_back(-1.0);
917 j.repl_of.push_back(m);
918 j.thr_slot.push_back(NOTHR);
919 j.thr_id.push_back(NOTHR);
920 jobs[j.id] = j;
921 touch(t);
922 cur_q[t] += 1.0;
923 touch(es[0]);
924 cur_q[es[0]] += 1.0;
925 advance(j.id);
926 }
927 }
928 }
929 drain();
930 if (!any_ref)
931 throw InputError("SolverLDES (native LN engine): the model has no reference task, so "
932 "nothing drives it");
933
934 // ---- the event loop -----------------------------------------------------
935 while (!evq.empty() && done < max_events) {
936 const LnEvent ev = evq.top();
937 evq.pop();
938 now = ev.t;
939
940 if (ev.kind == 1) {
941 // A reference task finished thinking: start the next invocation.
942 LnJob& j = jobs[ev.who];
943 const std::vector<std::size_t>& es = lsn.entriesof[j.ref_task];
944 j.t_start = now;
945 const std::size_t rep = j.repl_of.empty() ? 0 : j.repl_of.front();
946 j.stack.assign(1, es[0]);
947 j.act_pos.assign(1, 0);
948 j.call_pos.assign(1, 0);
949 j.calls_left.assign(1, NOTDRAWN);
950 j.entry_t0.assign(1, now);
951 j.act_t0.assign(1, -1.0);
952 // The customer belongs to one replica of its reference task for the
953 // whole run: it returns to the copy it came from.
954 j.repl_of.assign(1, rep);
955 j.thr_slot.assign(1, NOTHR);
956 j.thr_id.assign(1, NOTHR);
957 touch(j.ref_task);
958 cur_q[j.ref_task] += 1.0;
959 touch(es[0]);
960 cur_q[es[0]] += 1.0;
961 advance(ev.who);
962 drain();
963 continue;
964 }
965
966 if (ev.kind == 2 || ev.kind == 3) {
967 const std::size_t ts = ev.who, th = ev.aux;
968 if (ev.gen != thr_gen[ts][th]) continue; // the timer was cancelled
969 if (ev.kind == 3) {
970 if (thr_state[ts][th] == ThreadState::DELAYOFF)
971 thr_state[ts][th] = ThreadState::OFF;
972 continue;
973 }
974 // A cold start finished: the thread is up and takes the head of the
975 // queue it was woken for.
976 thr_state[ts][th] = ThreadState::ACTIVE;
977 if (!thr_held[ts][th] && !thr_queue[ts].empty()) {
978 const std::size_t nxt = thr_queue[ts].front();
979 thr_queue[ts].pop_front();
980 LnJob& nj = jobs[nxt];
981 thr_held[ts][th] = true;
982 touch(thr_owner[ts]);
983 cur_u[thr_owner[ts]] += 1.0;
984 nj.thr_slot.back() = ts;
985 nj.thr_id.back() = th;
986 advance(nxt);
987 drain();
988 }
989 continue;
990 }
991
992 // A host completion.
993 LnJob& j = jobs[ev.who];
994 const std::size_t entry = j.stack.empty() ? 0 : j.stack.back();
995 const std::vector<std::size_t>& acts =
996 (entry != 0 && entry < lsn.actsof.size()) ? lsn.actsof[entry]
997 : std::vector<std::size_t>();
998 // act_pos was advanced before the demand was started, so the activity
999 // that just finished is the one before it.
1000 const std::size_t ai = j.act_pos.empty() ? 0 : j.act_pos.back();
1001 const std::size_t act = (ai > 0 && ai - 1 < acts.size()) ? acts[ai - 1] : 0;
1002 if (act == 0) continue;
1003 const std::size_t host = lsn.host_of(act);
1004 // The demand ran on the processor replica paired with the task replica
1005 // that holds the request, and that pairing is fixed for the invocation.
1006 const std::size_t hs = host_slot(host, j.repl_of.empty() ? 0 : j.repl_of.back());
1007 touch(host);
1008 cur_q[host] -= 1.0;
1009 cur_u[host] -= 1.0;
1010 --host_busy[hs];
1011 completions[host] += 1.0;
1012 act_host_resid[act] += now - j.host_t0;
1013 // The host demand is the LAST thing an activity does, so this is where
1014 // the activity itself completes.
1015 finish_act(j, act);
1016
1017 if (!host_queue[hs].empty()) {
1018 const std::size_t nxt = host_queue[hs].front();
1019 host_queue[hs].pop_front();
1020 LnJob& nj = jobs[nxt];
1021 const std::size_t ne = nj.stack.back();
1022 const std::size_t nai = nj.act_pos.back();
1023 const std::size_t nact = lsn.actsof[ne][nai - 1];
1024 ++host_busy[hs];
1025 cur_u[host] += 1.0;
1026 LnEvent e;
1027 e.t = now + hostdem[nact].next(g_host[nact]);
1028 e.kind = 0;
1029 e.who = nxt;
1030 push_ev(e);
1031 }
1032 advance(ev.who);
1033 drain();
1034 }
1035
1036 // ---- result -------------------------------------------------------------
1037 for (std::size_t k = 1; k <= nidx; ++k) touch(k);
1038 LnResult res;
1039 res.nidx = nidx;
1040 res.QLN = Matrix<double>(nidx + 1, 1, 0.0);
1041 res.ULN = Matrix<double>(nidx + 1, 1, 0.0);
1042 res.RLN = Matrix<double>(nidx + 1, 1, 0.0);
1043 res.TLN = Matrix<double>(nidx + 1, 1, 0.0);
1044 // Residence starts at NaN everywhere and is written only where it exists:
1045 // activities and their tasks. See LnResult::WLN.
1046 res.WLN = Matrix<double>(nidx + 1, 1, std::numeric_limits<double>::quiet_NaN());
1047 for (std::size_t k = 1; k <= nidx; ++k) {
1048 if (now > 0.0) {
1049 res.QLN(k, 0) = tot_q[k] / now;
1050 // A host's utilization is its busy servers over its capacity; an
1051 // infinite server has no capacity to be busy against, so the
1052 // integral itself is the answer.
1053 // Replicas add capacity: r copies of a c-server processor hold r*c
1054 // servers, and the integral above already runs over all of them.
1055 // A task's capacity is its threads, the same way a processor's is its
1056 // servers: both are now integrated above, so both normalise the same.
1057 const bool is_task = (k > lsn.tshift && k <= lsn.tshift + lsn.ntasks);
1058 const std::size_t cap_k = is_task ? task_threads[k] : host_servers[k];
1059 const double c = (cap_k == std::numeric_limits<std::size_t>::max())
1060 ? 1.0
1061 : static_cast<double>(cap_k * repl[k]);
1062 res.ULN(k, 0) = tot_u[k] / (now * c);
1063 res.TLN(k, 0) = completions[k] / now;
1064 }
1065 if (resp_cnt[k] > 0.0) res.RLN(k, 0) = resp_sum[k] / resp_cnt[k];
1066 }
1067 res.entry_resp_samples.swap(entry_resp_samples);
1068
1069 // ResidT, per activity and summed onto its task. SolverLN builds
1070 // residt(aidx) as QN(host)/TN_ref, and TN_ref -- the reference of the
1071 // activity's layer -- is the completion rate of the activity's TASK: the
1072 // cycle rate of a reference task, the request rate into a called one,
1073 // TLN(task) either way. QN(host) is the mean occupancy at the host, which
1074 // is the total host time over the run divided by it, so
1075 //
1076 // ResidT = QN(host) / X_task = (total host time) / (elapsed * X_task)
1077 //
1078 // i.e. host time accumulated per driving visit. An activity executing once
1079 // per request reports its host residence, one inside a loop the loop's
1080 // total, and neither reading needs a visit count of its own. Accumulated
1081 // aside so a task with no activity at all keeps its NaN instead of 0.
1082 if (now > 0.0) {
1083 std::vector<double> task_resid(nidx + 1, 0.0);
1084 std::vector<bool> task_has_act(nidx + 1, false);
1085 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
1086 const std::size_t task = lsn.parent[a];
1087 const double x_task = (task >= 1 && task <= nidx) ? res.TLN(task, 0) : 0.0;
1088 const double resid = (x_task > 0.0) ? act_host_resid[a] / (now * x_task) : 0.0;
1089 res.WLN(a, 0) = resid;
1090 if (task >= 1 && task <= nidx) {
1091 task_resid[task] += resid;
1092 task_has_act[task] = true;
1093 }
1094 }
1095 for (std::size_t t = lsn.tshift + 1; t <= lsn.tshift + lsn.ntasks && t <= nidx; ++t)
1096 if (task_has_act[t]) res.WLN(t, 0) = task_resid[t];
1097 }
1098
1099 // ENTRY AND ACTIVITY UTILIZATION ARE DERIVED, the way `Solver_ssj_ln`
1100 // derives them: the utilization an activity causes at its host is its
1101 // throughput times its host demand, an entry's is the sum over the
1102 // activities bound under it, and an activity's own figure is that share of
1103 // its task's total. There is nothing to integrate for either kind -- an
1104 // entry is not a server and holds none -- so the `cur_u` integral that
1105 // answers for hosts and tasks leaves both at zero, which read as idle.
1106 //
1107 // The HOST AND TASK columns are NOT touched here: this port measures them as
1108 // the integral of busy servers and of busy threads, which is a different
1109 // quantity from the JAR's derived task utilization (sum of X*D over the
1110 // task's activities) and the one its own tests pin. The two ports therefore
1111 // still disagree on the task Util column; see `_kb/09-ldes-and-cache.md`.
1112 if (now > 0.0) {
1113 std::vector<double> proc_util(nidx + 1, 0.0), task_proc_util(nidx + 1, 0.0);
1114 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
1115 const double d = (a < lsn.hostdem.size() && !lsn.hostdem[a].disabled)
1116 ? num_traits<T>::to_double(lsn.hostdem[a].mean)
1117 : 0.0;
1118 proc_util[a] = res.TLN(a, 0) * d;
1119 const std::size_t task = lsn.parent[a];
1120 if (task >= 1 && task <= nidx) task_proc_util[task] += proc_util[a];
1121 }
1122 for (std::size_t e = lsn.eshift + 1; e <= lsn.eshift + lsn.nentries && e <= nidx; ++e) {
1123 double u = 0.0;
1124 const std::vector<std::size_t>& acts =
1125 (e < lsn.actsof.size()) ? lsn.actsof[e] : std::vector<std::size_t>();
1126 for (std::size_t i = 0; i < acts.size(); ++i)
1127 if (acts[i] <= nidx) u += proc_util[acts[i]];
1128 res.ULN(e, 0) = u;
1129 }
1130 for (std::size_t a = lsn.ashift + 1; a <= lsn.ashift + lsn.nacts && a <= nidx; ++a) {
1131 const std::size_t task = lsn.parent[a];
1132 const double tu = (task >= 1 && task <= nidx) ? task_proc_util[task] : 0.0;
1133 if (tu > 0.0 && tu < 1.0)
1134 res.ULN(a, 0) = std::min(1.0, proc_util[a] / tu);
1135 else
1136 res.ULN(a, 0) = (proc_util[a] > 0.0) ? 1.0 : 0.0;
1137 }
1138 }
1139
1140 res.simulated_time = now;
1141 res.completions = static_cast<long long>(done);
1142 return res;
1143}
1144
1145} // namespace ldes
1146} // namespace line
1147
1148#endif // LINE_SOLVERS_LDES_LDES_LN_ENGINE_H
InputError(const std::string &what)
Definition error.h:39
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
LnColumn
A column of the shared layered average table, as line-cli prints it.
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.
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 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, 1 = think completion, 2 = a task thread finished its cold start,...
One in-flight request: a reference-task job somewhere in its call tree.
std::vector< std::size_t > calls_left
Repetitions of the CURRENT call still to be made, per stack level.
std::vector< double > act_t0
When the current activity of each level began; -1 between activities.
std::vector< std::size_t > act_pos
next activity of each stacked entry
std::vector< std::size_t > call_pos
next call index of the current activity
std::vector< double > entry_t0
When each stacked entry was entered, for its response time and occupancy.
std::vector< std::size_t > thr_id
std::vector< std::size_t > stack
The activity stack: what this job is executing and what it will return to.
double t_start
when the current top-level invocation began
double host_t0
When this job last asked its host processor for service – the instant the residence at the host start...
std::vector< std::size_t > repl_of
Replica of the task owning each stacked entry.
std::vector< std::size_t > thr_slot
The task thread each stacked entry holds.
The layered result: per element, the mean measures.
Matrix< double > WLN
Residence time per element, the ResidT column: the time an activity holds ITS HOST PROCESSOR per visi...
std::vector< std::vector< double > > entry_resp_samples
Every per-request ENTRY response time observed, one vector per entry in LOCAL index space (0....