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