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