75 zc.dead.assign(N + 1,
false);
76 zc.trapped.assign(N + 1,
false);
77 zc.onroute.assign(N + 1,
false);
78 zc.ignore.assign(N + 1,
false);
79 zc.held.assign(N + 1, 0.0);
85 const std::size_t NT = lqn.tshift + lqn.ntasks;
87 for (std::size_t idx = 1; idx <= NT; ++idx) {
89 anydead = anydead || zc.dead[idx];
91 if (!anydead)
return zc;
96 state.assign(N + 1, 0);
97 reach.assign(N + 1,
false);
98 blockAct.assign(N + 1, 0);
99 next.assign(N + 1, 0);
100 for (std::size_t e = lqn.eshift + 1; e <= lqn.eshift + lqn.nentries; ++e) {
102 if (!zc.reason.empty())
return zc;
106 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
107 const std::size_t tgt = lqn.callpair_dst[c];
108 if (lqn.calltype[c] == CallType::FWD && reach[tgt]) {
109 zc.reason =
"Entry " + lqn.names[lqn.callpair_src[c]] +
" forwards requests to " + lqn.names[tgt] +
110 ", which reaches a zero-capacity element: forwarding into a blocked call path is "
115 std::vector<bool> arv(N + 1,
false);
116 for (std::size_t e = lqn.eshift + 1; e <= lqn.eshift + lqn.nentries && e < lqn.has_arrival.size(); ++e) {
117 arv[e] = lqn.has_arrival[e];
118 if (arv[e] && reach[e]) {
119 zc.reason =
"Entry " + lqn.names[e] +
" has an open arrival stream and reaches a zero-capacity "
120 "element, where its requests accumulate without bound: the model has no steady state.";
126 std::vector<std::size_t> refs;
127 for (std::size_t t = 1; t <= lqn.ntasks; ++t)
128 if (lqn.isref[lqn.tshift + t]) refs.push_back(lqn.tshift + t);
129 std::vector<double> incoming(N + 1, 0.0), nroutes(N + 1, 0.0);
130 for (std::size_t t : refs) {
131 std::vector<std::size_t> ents;
132 for (std::size_t e : lqn.entriesof[t])
133 if (reach[e]) ents.push_back(e);
134 if (ents.empty())
continue;
135 if (ents.size() > 1) {
136 zc.reason =
"Reference task " + lqn.names[t] +
" reaches a zero-capacity element from " +
137 std::to_string(ents.size()) +
138 " of its entries: how its users divide between the blocked paths depends on the "
142 zc.trapped[t] =
true;
143 double h = lqn.mult[t];
145 std::size_t e = ents[0];
148 const std::size_t tt = lqn.parent[e];
157 h = std::min(h, task_cap(tt));
162 mark(blockAct[e], h);
164 zc.onroute[lqn.parent[tt]] =
true;
167 e = std::size_t(next[e]);
170 for (std::size_t tt = 1; tt <= N; ++tt)
171 if (nroutes[tt] > 1.0 && incoming[tt] > task_cap(tt)) {
172 zc.reason =
"Task " + lqn.names[tt] +
" has " +
fmt_g(lqn.mult[tt]) +
173 " threads and is on the blocked call paths of several reference tasks that send it " +
174 fmt_g(incoming[tt]) +
" requests: which of them hold its threads depends on the "
178 for (std::size_t idx = 1; idx <= NT; ++idx)
179 if (zc.onroute[idx] && lqn.repl[idx] > 1.0) {
180 zc.reason = lqn.names[idx] +
181 " is replicated and on a call path blocked at a zero-capacity element, which is not "
187 std::vector<std::size_t> starts;
188 for (std::size_t t : refs)
189 if (!zc.trapped[t]) starts.push_back(t);
190 for (std::size_t e = 1; e <= N; ++e)
191 if (arv[e]) starts.push_back(e);
192 const std::vector<bool> live = reach_from(starts);
193 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
194 const std::size_t tgt = lqn.callpair_dst[c];
195 if (lqn.calltype[c] == CallType::ASYNC && live[lqn.callpair_src[c]] &&
196 (reach[tgt] || zc.dead[lqn.parent[tgt]])) {
197 zc.reason =
"Activity " + lqn.names[lqn.callpair_src[c]] +
" calls " + lqn.names[tgt] +
198 " asynchronously, which reaches a zero-capacity element where its requests accumulate "
199 "without bound: the model has no steady state.";
203 for (std::size_t idx = 1; idx <= N; ++idx)
204 if (live[idx] && zc.dead[idx]) {
205 zc.reason = lqn.names[idx] +
206 " has zero capacity and is reached by a reference task that is not trapped there, "
207 "through a call that cannot block it.";
210 for (std::size_t tt = lqn.tshift + 1; tt <= NT; ++tt)
211 if (live[tt] && zc.held[tt] > 0.0 && std::isfinite(task_cap(tt)) && !lqn.isref[tt]) {
212 zc.reason =
"Task " + lqn.names[tt] +
213 " is reached by a reference task that is not trapped, while " +
fmt_g(zc.held[tt]) +
214 " of its " +
fmt_g(lqn.mult[tt]) +
215 " threads are held forever by requests blocked at a zero-capacity element.";
219 for (std::size_t i = 1; i <= N; ++i) zc.live = zc.live || live[i];
222 std::vector<std::size_t> tstarts;
223 for (std::size_t t : refs)
224 if (zc.trapped[t]) tstarts.push_back(t);
225 const std::vector<bool> fromTrapped = reach_from(tstarts);
226 std::vector<bool> drop(N + 1,
false);
227 for (std::size_t i = 1; i <= NT; ++i) drop[i] = (fromTrapped[i] || zc.dead[i]) && !live[i];
228 for (std::size_t idx = lqn.eshift + 1; idx <= lqn.ashift + lqn.nacts; ++idx) drop[idx] = drop[lqn.parent[idx]];
237 std::vector<int> state;
238 std::vector<bool> reach;
239 std::vector<std::size_t> blockAct;
240 std::vector<long> next;
242 void mark(std::size_t idx,
double h) {
248 double task_cap(std::size_t tt)
const {
251 return std::numeric_limits<double>::infinity();
254 static bool has(
const std::vector<std::size_t>& v, std::size_t x) {
255 return std::find(v.begin(), v.end(), x) != v.end();
259 bool entry_route(std::size_t e) {
261 if (state[e] == 2)
return reach[e];
263 if (state[e] == 1 || !zc.
reason.empty())
return false;
265 const std::size_t tk = lqn.
parent[e];
269 const std::vector<std::size_t> starts = acts_after(e);
270 const std::vector<std::size_t> acts = act_closure(starts, {});
271 if (zc.dead[lqn.parent[tk]]) {
273 if (starts.size() != 1) {
274 zc.reason =
"Entry " + lqn.names[e] +
" starts " + std::to_string(starts.size()) +
275 " activities on zero-capacity processor " + lqn.names[lqn.parent[tk]] +
276 ": how its requests divide between them depends on the initial state.";
279 blockAct[e] = starts[0];
283 std::vector<std::size_t> cand;
284 std::vector<std::vector<std::size_t>> candCall;
285 for (std::size_t a : acts) {
286 std::vector<std::size_t> dc;
287 if (a < lqn.callsof.size())
288 for (std::size_t ci : lqn.callsof[a])
289 if (lqn.calltype[ci] == CallType::SYNC &&
290 num_traits<T>::to_double(lqn.callproc_mean[ci]) > 0.0 &&
291 entry_route(lqn.callpair_dst[ci]))
295 candCall.push_back(dc);
299 std::vector<std::size_t> c2;
300 std::vector<std::vector<std::size_t>> cc2;
301 for (std::size_t k = 0; k < cand.size(); ++k) {
302 std::vector<std::size_t> others;
303 for (std::size_t j = 0; j < cand.size(); ++j)
304 if (j != k) others.push_back(cand[j]);
305 if (has(act_closure(starts, others), cand[k])) {
306 c2.push_back(cand[k]);
307 cc2.push_back(candCall[k]);
310 if (c2.size() > 1 || (c2.size() == 1 && cc2[0].size() > 1)) {
311 zc.reason =
"Entry " + lqn.names[e] +
312 " reaches a zero-capacity element along more than one call path: how its "
313 "requests divide between them depends on the initial state.";
314 }
else if (c2.size() == 1) {
317 next[e] = long(lqn.callpair_dst[cc2[0][0]]);
327 std::vector<std::size_t> acts_after(std::size_t i)
const {
328 std::vector<std::size_t> s;
329 for (std::size_t j : lqn.graph.succ(i))
336 std::vector<std::size_t> act_closure(
const std::vector<std::size_t>& starts,
337 const std::vector<std::size_t>& blocked)
const {
338 std::vector<std::size_t> seen, stack;
339 for (std::size_t s : starts)
340 if (!has(blocked, s)) stack.push_back(s);
341 while (!stack.empty()) {
342 const std::size_t a = stack.back();
344 if (has(seen, a))
continue;
346 for (std::size_t b : acts_after(a))
347 if (!has(seen, b) && !has(blocked, b)) stack.push_back(b);
353 std::vector<bool> reach_from(
const std::vector<std::size_t>& starts)
const {
354 std::vector<bool> seen(N + 1,
false);
355 std::vector<std::size_t> stack = starts;
356 while (!stack.empty()) {
357 const std::size_t i = stack.back();
359 if (seen[i])
continue;
361 std::vector<std::size_t> nxt;
362 switch (lqn.type[i]) {
364 nxt.push_back(lqn.parent[i]);
365 if (lqn.isref[i]) nxt.insert(nxt.end(), lqn.entriesof[i].begin(), lqn.entriesof[i].end());
369 nxt.push_back(lqn.parent[i]);
370 for (std::size_t j : lqn.graph.succ(i))
371 if (j <= N) nxt.push_back(j);
377 for (std::size_t j : nxt)
378 if (j >= 1 && !seen[j]) stack.push_back(j);