LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
LQN2QN.m
1function model = LQN2QN(lqn)
2% LQN2QN Convert a LayeredNetwork (LQN) to a Network (QN) using REPLY signals
3%
4% model = LQN2QN(lqn) flattens a LayeredNetwork into a single queueing
5% network in which synchronous call blocking is represented by REPLY
6% signals.
7%
8% Construction
9% - One station per host processor (scheduling and multiplicity taken from
10% the processor). Tasks sharing a processor share the station, as in the
11% LQN semantics where the processor is the contended resource.
12% - One Delay per reference task, holding its think time.
13% - One closed class per step of the expanded activity graph. A step is an
14% activity, or one call stage of an activity that issues synchronous
15% calls. Steps of the reference task chain carry population 0 except the
16% think class, which carries the reference task multiplicity.
17% - A synchronous call site blocks its caller: the step class has a REPLY
18% signal bound to it (sn.syncreply), the token proceeds to the callee,
19% and the callee's replying activity class-switches to that signal, which
20% returns to the caller station and unblocks it.
21% - Call multiplicity mean m is unrolled into floor(m) mandatory call
22% stages plus, if m is not integer, one further stage entered with
23% probability m-floor(m). This preserves both the mean number of calls
24% and the blocking of each individual call.
25% - OR-branch and loop probabilities are read from the activity graph
26% weights lsn.graph(a,b). Multiple entries per task and branching call
27% trees are expanded, each call site receiving its own copy of the callee
28% subgraph so that per-call-path response times remain distinguishable.
29%
30% - AND precedences become Fork and Join nodes: a POST_AND successor set is
31% entered through a Fork and one Router per branch, since a Fork cannot
32% switch class per output link, and the PRE_AND branch tails switch back
33% to the class that entered the Fork, which is the class the Join matches
34% its siblings on. A branch tail that issues a synchronous call is given a
35% merge step, so that it reaches the Join in an ordinary class rather than
36% as a REPLY signal, which carries no forked-task identity.
37%
38% - A CacheTask becomes a Cache node. The activity bound to an ItemEntry is
39% the read step and sits on that node, carrying the item popularity and
40% cardinality of the entry; its two CacheAccess successors become the hit
41% and the miss class, and the class switch is performed by the Cache node
42% itself, so the routes leaving it are written in the successor class.
43%
44% - An asynchronous call is lowered to a non-blocking visit: the caller does
45% not hold its server for the duration of the call, but it is serialised
46% behind it, since a closed network has no means of creating the second
47% token that a truly concurrent send would require.
48%
49% - Entry forwarding splits the reply exits of the forwarding entry: with
50% the forwarding probability the request is handed to the target entry,
51% which replies to the original caller, so the forwarder is released while
52% the caller stays blocked.
53%
54% - The multiplicity of a non-reference task is its thread pool: at most
55% that many requests may be inside the task at once, where inside spans
56% the task's own steps and those of its nested callees, since a thread is
57% held for the whole of a synchronous call. It is enforced by a finite
58% capacity region with one linear admission constraint per task, and the
59% calls of such a task do not hold the caller's server, since the
60% processor is released while a thread waits for a reply.
61%
62% - An entry with an open arrival process receives requests from a Source:
63% they traverse the entry's subgraph in open classes and leave through a
64% Sink where the entry would reply. Calls on an open chain do not hold
65% the caller's server, since a REPLY signal is a closed class that cannot
66% be woven into an open chain; the thread pools of the traversed tasks
67% are still enforced by the finite capacity region.
68%
69% - Phase-2 activities, the successors of a replying activity, run after the
70% reply: the replying step's exit routes back to the caller, and each of
71% its service completions spawns the continuation at the host station
72% (sn.classspawn). The spawned token walks the phase-2 subgraph holding
73% only the task's own thread and is destroyed at the chain end, through a
74% NEGATIVE signal that always misses on a closed chain, or through the
75% Sink on an open one. A boundary that ends on a call site is
76% normalised through a merge step at the host station; a phase 2 that
77% opens with an AND-fork spawns into an immediate head that feeds the
78% Fork; at an AND-join branch tail the spawned token inherits the fork
79% identity of the trigger and stands in for it at the Join; at a cache
80% read the reply is emitted by an immediate trigger step per hit/miss
81% outcome, whose completion spawns the matching branch continuation.
82%
83% Not yet represented: delayed-hit retrieval on the cache miss path, and the
84% thread pool of a task with an internal AND-fork. Each is reported through
85% line_warning.
86%
87% Example:
88% lqn = LayeredNetwork('MyLQN');
89% % ... define LQN model ...
90% model = LQN2QN(lqn);
91% SolverLDES(model).getAvgTable()
92%
93% Copyright (c) 2012-2026, Imperial College London
94% All rights reserved.
95
96lsn = lqn.getStruct();
97model = Network([lqn.getName(), '-QN']);
98
99refTaskIndices = find(lsn.isref);
100
101%% Entries with an open arrival process
102% Source -> open classes -> Sink; open-chain calls never hold the caller's server -- see _kb/04-networkstruct.md
103openEntries = [];
104if isfield(lsn, 'arrival') && ~isempty(lsn.arrival)
105 for e_ = lsn.eshift+1:lsn.eshift+lsn.nentries
106 if e_ <= length(lsn.arrival) && isa(lsn.arrival{e_}, 'Distribution') && ...
107 isfinite(lsn.arrival{e_}.getMean()) && lsn.arrival{e_}.getMean() > GlobalConstants.FineTol
108 openEntries(end+1) = e_; %#ok<AGROW>
109 end
110 end
111end
112
113if isempty(refTaskIndices) && isempty(openEntries)
114 line_error(mfilename, 'LQN must have at least one reference task or open arrival.');
115end
116
117MAXCALLSTAGES = 20; % guard against unrolling a huge call multiplicity
118
119%% Unsupported features, reported once each
120warnUnsupported(lsn);
121
122%% Tasks whose multiplicity is a thread pool
123% One thread per request, capped by a finite capacity region instead of server-hold blocking -- see _kb/04-networkstruct.md
124fcrTask = false(lsn.nhosts + lsn.ntasks, 1);
125for t_ = 1:lsn.ntasks
126 tidx_ = lsn.tshift + t_;
127 if lsn.isref(tidx_) || (tidx_ <= length(lsn.iscache) && lsn.iscache(tidx_))
128 continue;
129 end
130 if ~isfinite(lsn.mult(tidx_)) || lsn.sched(tidx_) == SchedStrategy.INF
131 continue;
132 end
133 if taskHasAndFork(tidx_)
134 line_warning(mfilename, sprintf(['Multiplicity of task %s is not enforced: ' ...
135 'an AND-fork inside a task cannot be capped by a finite capacity ' ...
136 'region, whose job count would double-count the forked siblings.'], ...
137 lsn.names{tidx_}));
138 continue;
139 end
140 fcrTask(tidx_) = true;
141end
142
143% A task called transitively from inside an AND-fork branch is excluded too -- see _kb/04-networkstruct.md
144branchActs = [];
145if isfield(lsn, 'actposttype') && ~isempty(lsn.actposttype)
146 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
147 if a_ > length(lsn.actposttype) || full(lsn.actposttype(a_)) ~= ActivityPrecedenceType.POST_AND
148 continue;
149 end
150 frontier_ = a_;
151 while ~isempty(frontier_)
152 cur_ = frontier_(1); frontier_(1) = [];
153 if any(branchActs == cur_)
154 continue;
155 end
156 branchActs(end+1) = cur_; %#ok<AGROW>
157 if isfield(lsn, 'actpretype') && ~isempty(lsn.actpretype) && ...
158 cur_ <= length(lsn.actpretype) && ...
159 full(lsn.actpretype(cur_)) == ActivityPrecedenceType.PRE_AND
160 continue; % branch tail: do not traverse past the join
161 end
162 succ_ = find(lsn.graph(cur_, :));
163 succ_ = succ_(succ_ > lsn.ashift & lsn.parent(succ_).' == lsn.parent(cur_));
164 frontier_ = [frontier_, succ_]; %#ok<AGROW>
165 end
166 end
167end
168if ~isempty(branchActs) && any(fcrTask)
169 front_ = [];
170 for a_ = branchActs
171 if a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
172 for c_ = lsn.callsof{a_}
173 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
174 end
175 end
176 end
177 shadow_ = false(size(fcrTask));
178 while ~isempty(front_)
179 t_ = front_(1); front_(1) = [];
180 if shadow_(t_)
181 continue;
182 end
183 shadow_(t_) = true;
184 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
185 if lsn.parent(a_) == t_ && a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
186 for c_ = lsn.callsof{a_}
187 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
188 end
189 end
190 end
191 end
192 for t_ = find(shadow_(:).' & fcrTask(:).')
193 fcrTask(t_) = false;
194 line_warning(mfilename, sprintf(['Multiplicity of task %s is not enforced: ' ...
195 'it is called from inside an AND-fork branch, whose flows the ' ...
196 'fork-join transformation retags outside the admission constraint.'], ...
197 lsn.names{t_}));
198 end
199end
200
201%% Stations: one per host processor
202hostStation = cell(lsn.nhosts, 1);
203hostIsDelay = false(lsn.nhosts, 1);
204for h = 1:lsn.nhosts
205 nservers = lsn.mult(h);
206 sched = lsn.sched(h);
207 if isinf(nservers) || sched == SchedStrategy.INF
208 hostStation{h} = Delay(model, lsn.names{h});
209 hostIsDelay(h) = true;
210 else
211 q = Queue(model, lsn.names{h}, sched);
212 q.setNumberOfServers(nservers);
213 hostStation{h} = q;
214 end
215end
216
217%% Think delays, one per reference task
218thinkNode = containers.Map('KeyType', 'double', 'ValueType', 'any');
219for rt = 1:length(refTaskIndices)
220 refTidx = refTaskIndices(rt);
221 thinkNode(refTidx) = Delay(model, [lsn.names{refTidx}, '_Think']);
222end
223
224%% Pass 1: expand the activity graph into a step graph
225% Steps carry no LINE objects yet, so all classes can be created before the routing matrix is initialised
226stepAidx = []; % activity index of the step
227stepHost = []; % host processor index of the step station
228stepSvc = {}; % service distribution at the step station, [] if none
229stepName = {}; % class name
230stepBlocks = []; % true if the step blocks on a synchronous call
231stepIsThink = []; % true for a think step (reference task)
232stepRefTask = []; % reference task the step belongs to
233stepNode = {}; % Fork/Join/Router node the step sits on, [] for stations
234stepClassOwner = []; % step whose class this step travels in (itself, normally)
235stepTasks = {}; % thread-pool tasks holding a thread while at this step
236
237% flow/reply/spawnPairs/ph2Exits step-graph arrays -- see _kb/04-networkstruct.md
238flow = zeros(0, 5);
239reply = zeros(0, 4);
240spawnPairs = zeros(0, 2);
241ph2Exits = zeros(0, 4);
242
243entryStack = []; % guards against recursive call cycles
244threadStack = []; % tasks holding a thread during expansion; tracks entryStack except across forwarding (releases the forwarder's thread)
245
246% Cache nodes, one per CacheTask, and the read/hit/miss wiring to apply once
247% the classes exist.
248cacheNodeOf = containers.Map('KeyType', 'double', 'ValueType', 'any');
249cacheWiring = struct('node', {}, 'readStep', {}, 'hitStep', {}, 'missStep', {}, ...
250 'itemproc', {}, 'nitems', {});
251
252for rt = 1:length(refTaskIndices)
253 refTidx = refTaskIndices(rt);
254 thinkStep = addStep([], [], [], [lsn.names{refTidx}, '_Think'], false, true, refTidx);
255
256 entries = lsn.entriesof{refTidx};
257 for eidx = entries
258 [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx);
259 if isempty(firstStep)
260 continue;
261 end
262 addRoute([thinkStep, 0], firstStep, 1.0);
263 % A reference task has no caller: its replies and its dead ends
264 % both close the cycle at the think delay.
265 exits = [replyExits; terminals];
266 for s = 1:size(exits, 1)
267 addRoute(exits(s, 1:2), thinkStep, exits(s, 3));
268 end
269 end
270end
271
272%% Pass 1b: open arrival chains
273% openWiring rows: {eidx, firstStep, exits}, wired to Source/Sink in pass 4; reference task index 0 marks an open-chain step
274openWiring = struct('eidx', {}, 'firstStep', {}, 'exits', {});
275srcNode = []; snkNode = [];
276if ~isempty(openEntries)
277 srcNode = Source(model, 'Source');
278 snkNode = Sink(model, 'Sink');
279end
280for eidx = openEntries
281 [firstStep, replyExits, terminals] = expandEntry(eidx, 0);
282 if isempty(firstStep)
283 line_warning(mfilename, sprintf('Open arrival entry %s has no bound activity; ignored.', ...
284 lsn.names{eidx}));
285 continue;
286 end
287 openWiring(end+1) = struct('eidx', eidx, 'firstStep', firstStep, ...
288 'exits', [replyExits; terminals]); %#ok<AGROW>
289end
290
291%% Pass 2: create classes and reply signals
292nsteps = length(stepAidx);
293stepClass = cell(nsteps, 1);
294stepSignal = cell(nsteps, 1);
295
296for i = 1:nsteps
297 refTidx = stepRefTask(i);
298 if stepClassOwner(i) ~= i
299 % Fork/Join/Router steps carry the job unchanged in the class that entered the fork
300 continue;
301 end
302 if refTidx == 0
303 % A step of an open arrival chain travels in an open class.
304 stepClass{i} = OpenClass(model, stepName{i});
305 elseif stepIsThink(i)
306 population = lsn.mult(refTidx);
307 stepClass{i} = ClosedClass(model, stepName{i}, population, thinkNode(refTidx));
308 else
309 stepClass{i} = ClosedClass(model, stepName{i}, 0, thinkNode(refTidx));
310 end
311end
312for i = 1:nsteps
313 stepClass{i} = stepClass{stepClassOwner(i)};
314end
315
316for i = 1:nsteps
317 if stepBlocks(i)
318 sig = Signal(model, [stepName{i}, '_Reply'], SignalType.REPLY);
319 sig.forJobClass(stepClass{i});
320 stepSignal{i} = sig;
321 end
322end
323
324% Signal.m installs RAND routing at every node; clear it so that link()
325% only honours the routes set below.
326for i = 1:nsteps
327 if ~isempty(stepSignal{i})
328 for n = 1:length(model.nodes)
329 if ~isa(model.nodes{n}, 'Sink')
330 model.nodes{n}.setRouting(stepSignal{i}, RoutingStrategy.DISABLED);
331 end
332 end
333 end
334end
335
336% Spawn bindings for phase-2 continuations: each completion of the trigger
337% class injects a job of the target class at the same station.
338for sp = 1:size(spawnPairs, 1)
339 stepClass{spawnPairs(sp, 1)}.setSpawnClass(stepClass{spawnPairs(sp, 2)});
340end
341
342% Phase-2 token destructor (closed chains): NEGATIVE signal at a station nothing visits, one per reference task -- see _kb/04-networkstruct.md
343ph2DumpNode = [];
344ph2DestructorOf = containers.Map('KeyType', 'double', 'ValueType', 'any');
345if ~isempty(ph2Exits) && any(ph2Exits(:, 4) > 0)
346 ph2DumpNode = Queue(model, 'Ph2Sink', SchedStrategy.FCFS);
347 for rft = unique(ph2Exits(ph2Exits(:, 4) > 0, 4).')
348 sig = ClosedSignal(model, sprintf('Ph2End_%s', lsn.names{rft}), ...
349 SignalType.NEGATIVE, thinkNode(rft));
350 for n = 1:length(model.nodes)
351 if ~isa(model.nodes{n}, 'Sink')
352 model.nodes{n}.setRouting(sig, RoutingStrategy.DISABLED);
353 end
354 end
355 ph2DumpNode.setService(sig, Immediate());
356 ph2DestructorOf(rft) = sig;
357 end
358end
359
360%% Pass 3: service times
361for i = 1:nsteps
362 if ~isempty(stepNode{i})
363 % A Router-hosted merge step owns a class, declared Immediate at the reference think delay (as for signals)
364 if stepClassOwner(i) == i && isa(stepNode{i}, 'Router')
365 if stepRefTask(i) == 0
366 % Open chain: no think delay exists; declare at the caller's host station, which the class never visits
367 hostStation{stepHost(i)}.setService(stepClass{i}, Immediate());
368 else
369 tn_ = thinkNode(stepRefTask(i));
370 tn_.setService(stepClass{i}, Immediate());
371 end
372 end
373 continue;
374 end
375 if stepIsThink(i)
376 refTidx = stepRefTask(i);
377 thinkDist = lsn.think{refTidx};
378 tnode = thinkNode(refTidx);
379 if isempty(thinkDist) || isa(thinkDist, 'Immediate') || thinkDist.getMean() < GlobalConstants.FineTol
380 tnode.setService(stepClass{i}, Immediate());
381 else
382 tnode.setService(stepClass{i}, thinkDist);
383 end
384 else
385 station = hostStation{stepHost(i)};
386 if isempty(stepSvc{i})
387 station.setService(stepClass{i}, Immediate());
388 else
389 station.setService(stepClass{i}, stepSvc{i});
390 end
391 end
392end
393
394% A reply signal is consumed at the caller's station; declared at every station since a signal with no pending reply falls back to its reference station
395for i = 1:nsteps
396 if ~isempty(stepSignal{i})
397 for h = 1:lsn.nhosts
398 hostStation{h}.setService(stepSignal{i}, Immediate());
399 end
400 tkeys = cell2mat(thinkNode.keys);
401 for kk = tkeys
402 tn = thinkNode(kk);
403 tn.setService(stepSignal{i}, Immediate());
404 end
405 end
406end
407
408%% Cache read/hit/miss wiring, now that the classes exist
409for w = 1:length(cacheWiring)
410 cw = cacheWiring(w);
411 cw.node.setReadItemEntry(stepClass{cw.readStep}, cw.itemproc, cw.nitems);
412 cw.node.setHitClass(stepClass{cw.readStep}, stepClass{cw.hitStep});
413 cw.node.setMissClass(stepClass{cw.readStep}, stepClass{cw.missStep});
414end
415
416%% Pass 4: routing
417P = model.initRoutingMatrix();
418
419for e = 1:size(flow, 1)
420 i = flow(e, 1); j = flow(e, 2); p = flow(e, 3);
421 if flow(e, 5)
422 P{stepClass{j}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
423 elseif flow(e, 4)
424 P{stepSignal{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
425 else
426 P{stepClass{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
427 end
428end
429
430for e = 1:size(reply, 1)
431 i = reply(e, 1); owner = reply(e, 2); viaSignal = reply(e, 3); p = reply(e, 4);
432 if viaSignal
433 % A nested call returns through its own reply signal, which
434 % class-switches into the reply signal of the outer call site.
435 P{stepSignal{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
436 else
437 P{stepClass{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
438 end
439end
440
441%% Phase-2 chain ends: destroy the spawned token
442for e = 1:size(ph2Exits, 1)
443 i = ph2Exits(e, 1); viaSig = ph2Exits(e, 2); p = ph2Exits(e, 3); rft = ph2Exits(e, 4);
444 if rft == 0
445 % Open chain: the spawned token leaves through the Sink.
446 ecls = stepClass{i};
447 P{ecls, ecls}(stationOf(i), snkNode) = p;
448 elseif viaSig
449 P{stepSignal{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
450 else
451 P{stepClass{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
452 end
453end
454
455%% Open arrival wiring: Source into the first step, exits into the Sink
456for w = 1:length(openWiring)
457 ow = openWiring(w);
458 firstCls = stepClass{ow.firstStep};
459 srcNode.setArrival(firstCls, lsn.arrival{ow.eidx});
460 P{firstCls, firstCls}(srcNode, stationOf(ow.firstStep)) = 1.0;
461 for s = 1:size(ow.exits, 1)
462 % Open chains carry no signals, so every exit is an ordinary class.
463 ecls = stepClass{ow.exits(s, 1)};
464 P{ecls, ecls}(stationOf(ow.exits(s, 1)), snkNode) = ow.exits(s, 3);
465 end
466end
467
468model.link(P);
469
470%% Thread pools: one finite capacity region, one linear constraint per task
471% One admission row A(t,:)*x <= mult(t) per task, coefficients shared across nested tasks -- see _kb/04-networkstruct.md
472fcrList = find(fcrTask(:).');
473if ~isempty(fcrList)
474 Amat = zeros(length(fcrList), length(model.classes));
475 bvec = zeros(length(fcrList), 1);
476 regionNodes = {};
477 for tsel = 1:length(fcrList)
478 for stp = 1:nsteps
479 if isempty(stepTasks{stp}) || ~any(stepTasks{stp} == fcrList(tsel))
480 continue;
481 end
482 Amat(tsel, stepClass{stp}.index) = 1;
483 nd = stationOf(stp);
484 if isa(nd, 'Station') && ~any(cellfun(@(x) x == nd, regionNodes))
485 regionNodes{end+1} = nd; %#ok<AGROW>
486 end
487 end
488 bvec(tsel) = lsn.mult(fcrList(tsel));
489 end
490 if any(Amat(:)) && ~isempty(regionNodes)
491 fcr = model.addRegion(regionNodes);
492 fcr.setConstraint(Amat, bvec);
493 end
494end
495
496%% ---------------------------------------------------------------- helpers
497
498 function node = stationOf(i)
499 if ~isempty(stepNode{i})
500 node = stepNode{i};
501 elseif stepIsThink(i)
502 node = thinkNode(stepRefTask(i));
503 else
504 node = hostStation{stepHost(i)};
505 end
506 end
507
508 function id = addStep(aidx, hidx, svc, name, blocks, isthink, refTidx)
509 stepAidx(end+1) = ifempty(aidx, 0); %#ok<AGROW>
510 stepHost(end+1) = ifempty(hidx, 0); %#ok<AGROW>
511 stepSvc{end+1} = svc; %#ok<AGROW>
512 stepName{end+1} = name; %#ok<AGROW>
513 stepBlocks(end+1) = blocks; %#ok<AGROW>
514 stepIsThink(end+1) = isthink; %#ok<AGROW>
515 stepRefTask(end+1) = refTidx; %#ok<AGROW>
516 stepNode{end+1} = []; %#ok<AGROW>
517 id = length(stepAidx);
518 stepClassOwner(end+1) = id; %#ok<AGROW>
519 % Every thread-pool task on the stack holds a thread here (a synchronous caller releases it only on reply)
520 if isempty(threadStack)
521 stepTasks{end+1} = []; %#ok<AGROW>
522 else
523 stepTasks{end+1} = unique(threadStack(fcrTask(threadStack))); %#ok<AGROW>
524 end
525 end
526
527 function id = addAuxStep(nodeObj, ownerStep, name, refTidx)
528 % A step on a Fork, Join or Router node: no station, no service, and
529 % no class of its own.
530 id = addStep([], [], [], name, false, false, refTidx);
531 stepNode{id} = nodeObj;
532 stepClassOwner(id) = stepClassOwner(ownerStep);
533 end
534
535 function [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx)
536 % Expands the activity subgraph bound to an entry, in the call
537 % context given by the current entry stack.
538 firstStep = [];
539 replyExits = zeros(0, 3);
540 terminals = zeros(0, 3);
541
542 if any(entryStack == eidx)
543 line_warning(mfilename, sprintf('Recursive call cycle at entry %s truncated.', lsn.names{eidx}));
544 return;
545 end
546 entryStack(end+1) = eidx;
547 threadStack(end+1) = lsn.parent(eidx);
548 restore = onCleanup(@() popEntry());
549
550 if eidx > length(lsn.actsof) || isempty(lsn.actsof{eidx})
551 return;
552 end
553
554 % The bound activity is the activity successor of the entry.
555 succ = find(lsn.graph(eidx, :));
556 boundActs = succ(arrayfun(@(a) a <= length(lsn.type) && lsn.type(a) == LayeredNetworkElement.ACTIVITY, succ));
557 boundActs = intersect(boundActs, lsn.actsof{eidx}, 'stable');
558 if isempty(boundActs)
559 return;
560 end
561
562 [firstStep, replyExits, terminals] = expandActivities(boundActs(1), eidx, refTidx);
563
564 % Forwarding: with prob p the entry hands off to another entry, which replies directly to the original caller
565 fwd = forwardingOf(eidx);
566 if ~isempty(fwd) && ~isempty(replyExits)
567 ownPorts = replyExits;
568 fwdExits = zeros(0, 3);
569 pforw = 0.0;
570 % Forwarder's thread released at handoff; the forwarded chain expands without it on the thread stack
571 fwdThread = threadStack(end);
572 threadStack(end) = [];
573 for f = 1:size(fwd, 1)
574 [fFirst, fReplies, fTerms] = expandEntry(fwd(f, 1), refTidx);
575 if isempty(fFirst)
576 continue;
577 end
578 p = fwd(f, 2);
579 for r = 1:size(ownPorts, 1)
580 addRoute(ownPorts(r, 1:2), fFirst, ownPorts(r, 3) * p);
581 end
582 pforw = pforw + p;
583 fwdExits = [fwdExits; fReplies; fTerms]; %#ok<AGROW>
584 end
585 threadStack(end+1) = fwdThread;
586 % What is left of each of this entry's own ports still replies.
587 replyExits(:, 3) = replyExits(:, 3) * max(0.0, 1.0 - pforw);
588 replyExits = [replyExits; fwdExits];
589 end
590 end
591
592 function fwd = forwardingOf(eidx)
593 % Rows [targetEidx, probability] of the forwarding calls of an entry.
594 fwd = zeros(0, 2);
595 if ~isfield(lsn, 'calltype') || isempty(lsn.calltype)
596 return;
597 end
598 for cidx = 1:size(lsn.callpair, 1)
599 if full(lsn.calltype(cidx)) ~= CallType.FWD || lsn.callpair(cidx, 1) ~= eidx
600 continue;
601 end
602 p = 1.0;
603 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
604 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx}, 'Distribution')
605 p = lsn.callproc{cidx}.getMean();
606 end
607 p = min(max(p, 0.0), 1.0);
608 if p > GlobalConstants.FineTol
609 fwd(end+1, :) = [lsn.callpair(cidx, 2), p]; %#ok<AGROW>
610 end
611 end
612 end
613
614 function popEntry()
615 entryStack(end) = [];
616 threadStack(end) = [];
617 end
618
619 function [firstStep, replyExits, terminals] = expandActivities(a0, eidx, refTidx)
620 % Walks the intra-task activity graph from a0, creating steps and
621 % expanding every synchronous call site. Reply exits and terminals
622 % are returned as ports, [step, isSignal] rows.
623 firstStep = [];
624 replyExits = zeros(0, 3);
625 terminals = zeros(0, 3);
626
627 tidx = lsn.parent(a0);
628 localActs = lsn.actsof{eidx};
629 visited = containers.Map('KeyType', 'double', 'ValueType', 'any'); % aidx -> [entryStep, exitStep, exitIsSignal]
630 joinOf = containers.Map('KeyType', 'double', 'ValueType', 'any'); % join activity -> [joinStep, entryStep]
631 forkOwnerStack = []; % class-owner step of each enclosing AND-fork
632 sawReply = false;
633
634 [firstStep, ~] = walk(a0);
635
636 % Every chain end returns the token to the caller: as a reply if the entry replies anywhere, else a plain terminal
637 if sawReply
638 replyExits = [replyExits; terminals]; %#ok<AGROW>
639 terminals = zeros(0, 3);
640 end
641
642 function [entryStep, exitPort] = walk(aidx)
643 if isKey(visited, aidx)
644 se = visited(aidx);
645 entryStep = se(1);
646 exitPort = se(2:3);
647 return;
648 end
649 % The activity bound to an ItemEntry of a CacheTask is the read
650 % step: it sits on the Cache node rather than on the processor.
651 cacheNode = [];
652 if tidx <= length(lsn.iscache) && lsn.iscache(tidx) && full(lsn.graph(eidx, aidx)) > 0
653 cacheNode = getCacheNode(tidx);
654 end
655 [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode);
656 visited(aidx) = [entryStep, exitPort];
657
658 % Reply is deferred to the ends of the chain, not emitted here -- see _kb/04-networkstruct.md
659 repliesHere = false;
660 if isfield(lsn, 'replygraph') && ~isempty(lsn.replygraph)
661 a = aidx - lsn.ashift;
662 e = eidx - lsn.eshift;
663 if a >= 1 && a <= size(lsn.replygraph, 1) && e >= 1 && e <= size(lsn.replygraph, 2)
664 repliesHere = full(lsn.replygraph(a, e));
665 end
666 end
667 if repliesHere
668 sawReply = true;
669 end
670
671 % Local successors within the same entry.
672 succ = find(lsn.graph(aidx, :));
673 succ = succ(ismember(succ, localActs));
674 if isempty(succ)
675 terminals(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
676 return;
677 end
678
679 % Phase 2 runs after the reply via sn.classspawn; needs a station-departure exit in the step's own class -- see _kb/04-networkstruct.md
680 if repliesHere
681 if ~isempty(cacheNode) && length(succ) >= 2
682 % Phase 2 at a cache read: each hit/miss outcome routes through its own immediate trigger step -- see _kb/04-networkstruct.md
683 trigH = addStep(aidx, lsn.parent(tidx), [], ...
684 [lsn.names{aidx}, '_ph2h'], false, false, refTidx);
685 trigM = addStep(aidx, lsn.parent(tidx), [], ...
686 [lsn.names{aidx}, '_ph2m'], false, false, refTidx);
687 addCacheRoute(entryStep, trigH);
688 addCacheRoute(entryStep, trigM);
689 cacheWiring(end+1) = struct('node', cacheNode, ...
690 'readStep', entryStep, 'hitStep', trigH, 'missStep', trigM, ...
691 'itemproc', lsn.itemproc{eidx}, 'nitems', lsn.nitems(eidx)); %#ok<AGROW>
692 replyExits(end+1, :) = [trigH, 0, 1.0]; %#ok<AGROW>
693 replyExits(end+1, :) = [trigM, 0, 1.0]; %#ok<AGROW>
694 savedStack = threadStack;
695 threadStack = tidx;
696 nT0 = size(terminals, 1);
697 for hm = 1:2
698 [sEntry, ~] = walk(succ(hm));
699 if ~isempty(stepNode{sEntry})
700 hmHead = addStep(aidx, lsn.parent(tidx), [], ...
701 sprintf('%s_ph2b%d', lsn.names{aidx}, hm), false, false, refTidx);
702 addRoute([hmHead, 0], sEntry, 1.0);
703 sEntry = hmHead;
704 end
705 if hm == 1
706 spawnPairs(end+1, :) = [trigH, sEntry]; %#ok<AGROW>
707 else
708 spawnPairs(end+1, :) = [trigM, sEntry]; %#ok<AGROW>
709 end
710 end
711 ph2New = terminals(nT0+1:end, :);
712 terminals(nT0+1:end, :) = [];
713 for r2 = 1:size(ph2New, 1)
714 ph2Exits(end+1, :) = [ph2New(r2, 1:3), refTidx]; %#ok<AGROW>
715 end
716 threadStack = savedStack;
717 return;
718 end
719 % Inside an AND-fork branch, the lift applies only at the branch tail -- see _kb/04-networkstruct.md
720 okCtx = isempty(cacheNode) && ...
721 (isempty(forkOwnerStack) || isAndJoinPre(aidx));
722 % A merge step at the host station normalises a call-site exit to a station departure before the phase-2 walk
723 if okCtx && (exitPort(2) == 1 || ...
724 (~isempty(stepNode{exitPort(1)}) && isa(stepNode{exitPort(1)}, 'Router')))
725 trig = addStep(aidx, lsn.parent(tidx), [], ...
726 [lsn.names{aidx}, '_ph2t'], false, false, refTidx);
727 addRoute(exitPort, trig, 1.0);
728 exitPort = [trig, 0];
729 end
730 ph2Spawn = okCtx && exitPort(2) == 0 && isempty(stepNode{exitPort(1)});
731 if ~ph2Spawn
732 line_warning(mfilename, sprintf(['Phase-2 activities of %s run ' ...
733 'before the reply: the boundary is not a station ' ...
734 'departure, a degenerate cache read, or mid-branch ' ...
735 'inside an AND-fork.'], lsn.names{aidx}));
736 else
737 replyExits(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
738 savedStack = threadStack;
739 threadStack = tidx;
740 nT0 = size(terminals, 1);
741 posSucc = succ(full(lsn.graph(aidx, succ)) > 0);
742 target = [];
743 if isAndFork(succ)
744 % Phase 2 opens with an AND-fork: spawn into an immediate head step and fork from there
745 head = addStep(aidx, lsn.parent(tidx), [], ...
746 [lsn.names{aidx}, '_ph2'], false, false, refTidx);
747 wireAndFork([head, 0], succ, aidx);
748 target = head;
749 elseif isAndJoinPre(aidx)
750 % Phase 2 at an AND-join branch tail: spawn into an immediate head standing in for this branch at the Join
751 head = addStep(aidx, lsn.parent(tidx), [], ...
752 [lsn.names{aidx}, '_ph2'], false, false, refTidx);
753 wireAndJoin([head, 0], succ(1));
754 target = head;
755 elseif isscalar(posSucc)
756 [sEntry, ~] = walk(posSucc);
757 if isempty(stepNode{sEntry})
758 target = sEntry;
759 end
760 end
761 if isempty(target)
762 % Branching phase 2, or a head on a non-station node: spawn into an immediate head carrying branch probabilities
763 head = addStep(aidx, lsn.parent(tidx), [], ...
764 [lsn.names{aidx}, '_ph2'], false, false, refTidx);
765 for s2 = posSucc
766 [sEntry, ~] = walk(s2);
767 addRoute([head, 0], sEntry, full(lsn.graph(aidx, s2)));
768 end
769 target = head;
770 end
771 spawnPairs(end+1, :) = [exitPort(1), target]; %#ok<AGROW>
772 ph2New = terminals(nT0+1:end, :);
773 terminals(nT0+1:end, :) = [];
774 for r2 = 1:size(ph2New, 1)
775 ph2Exits(end+1, :) = [ph2New(r2, 1:3), refTidx]; %#ok<AGROW>
776 end
777 threadStack = savedStack;
778 return;
779 end
780 end
781
782 if ~isempty(cacheNode)
783 % CacheAccess precedence: successors are hit then miss branch; the Cache node decides, so no probability on these routes
784 if length(succ) < 2
785 line_warning(mfilename, sprintf(...
786 'Cache read %s has no hit/miss pair; treated as an ordinary activity.', ...
787 lsn.names{aidx}));
788 else
789 [hEntry, ~] = walk(succ(1));
790 [mEntry, ~] = walk(succ(2));
791 addCacheRoute(entryStep, hEntry);
792 addCacheRoute(entryStep, mEntry);
793 cacheWiring(end+1) = struct('node', cacheNode, ...
794 'readStep', entryStep, 'hitStep', hEntry, 'missStep', mEntry, ...
795 'itemproc', lsn.itemproc{eidx}, 'nitems', lsn.nitems(eidx)); %#ok<AGROW>
796 return;
797 end
798 end
799
800 if isAndFork(succ)
801 wireAndFork(exitPort, succ, aidx);
802 return;
803 end
804
805 if isAndJoinPre(aidx)
806 wireAndJoin(exitPort, succ(1));
807 return;
808 end
809
810 for s = succ
811 p = full(lsn.graph(aidx, s));
812 if p <= 0
813 continue;
814 end
815 [sEntry, ~] = walk(s);
816 addRoute(exitPort, sEntry, p);
817 end
818 end
819
820 function wireAndFork(fromPort, fsucc, aidx)
821 % AND-fork: the branches run concurrently, so the job is
822 % replicated by a Fork node. One Router per branch, because a
823 % Fork cannot switch class per output link, and the branch
824 % class is what tells the branch apart.
825 forkNode = Fork(model, ['Fork_', lsn.names{aidx}]);
826 forkStep = addAuxStep(forkNode, fromPort(1), ['Fork_', lsn.names{aidx}], refTidx);
827 addRoute(fromPort, forkStep, 1.0);
828 forkOwnerStack(end+1) = forkStep;
829 % Walk a replying branch first so the Join/post-join subgraph is created in its phase-2 context
830 rep = arrayfun(@branchReplies, fsucc);
831 fsucc = [fsucc(rep), fsucc(~rep)];
832 for b = 1:length(fsucc)
833 routerNode = Router(model, sprintf('Fork_%s_%d', lsn.names{aidx}, b));
834 routerStep = addAuxStep(routerNode, forkStep, ...
835 sprintf('Fork_%s_%d', lsn.names{aidx}, b), refTidx);
836 addRoute([forkStep, 0], routerStep, 1.0);
837 [sEntry, ~] = walk(fsucc(b));
838 addRoute([routerStep, 0], sEntry, 1.0);
839 end
840 forkOwnerStack(end) = [];
841 end
842
843 function wireAndJoin(fromPort, joinAidx)
844 % Route a branch tail into the AND-join, creating the Join on
845 % first arrival. The siblings are matched there in the class
846 % that entered the fork, so switch back to it on the way in.
847 % The post-join subgraph is walked in the calling context.
848 if isKey(joinOf, joinAidx)
849 js = joinOf(joinAidx);
850 addRoute(fromPort, js(1), 1.0);
851 return;
852 end
853 if isempty(forkOwnerStack)
854 line_warning(mfilename, sprintf(...
855 'AND-join at %s has no enclosing AND-fork; branches are serialised.', ...
856 lsn.names{joinAidx}));
857 [sEntry, ~] = walk(joinAidx);
858 addRoute(fromPort, sEntry, 1.0);
859 return;
860 end
861 joinNode = Join(model, ['Join_', lsn.names{joinAidx}], stepNode{forkOwnerStack(end)});
862 joinStep = addAuxStep(joinNode, forkOwnerStack(end), ...
863 ['Join_', lsn.names{joinAidx}], refTidx);
864 addRoute(fromPort, joinStep, 1.0);
865 [sEntry, ~] = walk(joinAidx);
866 addRoute([joinStep, 0], sEntry, 1.0);
867 joinOf(joinAidx) = [joinStep, sEntry];
868 end
869
870 function tf = branchReplies(a0)
871 % True if the branch rooted at a0 contains an activity that
872 % replies to the current entry, searching up to and excluding
873 % the AND-join that closes the branch.
874 tf = false;
875 stack = a0;
876 seenActs = [];
877 while ~isempty(stack)
878 a = stack(end);
879 stack(end) = [];
880 if any(seenActs == a)
881 continue;
882 end
883 seenActs(end+1) = a; %#ok<AGROW>
884 if isfield(lsn, 'replygraph') && ~isempty(lsn.replygraph)
885 ar = a - lsn.ashift;
886 er = eidx - lsn.eshift;
887 if ar >= 1 && ar <= size(lsn.replygraph, 1) && ...
888 er >= 1 && er <= size(lsn.replygraph, 2) && ...
889 full(lsn.replygraph(ar, er))
890 tf = true;
891 return;
892 end
893 end
894 if isAndJoinPre(a)
895 continue;
896 end
897 nxt = find(lsn.graph(a, :));
898 stack = [stack, nxt(ismember(nxt, localActs))]; %#ok<AGROW>
899 end
900 end
901 end
902
903 function [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode)
904 % One step for the host demand, plus one step per unrolled
905 % synchronous call stage.
906 hidx = lsn.parent(tidx);
907 if nargin >= 4 && ~isempty(cacheNode)
908 % A read step holds no demand and issues no call: the lookup is
909 % instantaneous and the work is done on the hit or miss branch.
910 entryStep = addStep(aidx, hidx, [], lsn.names{aidx}, false, false, refTidx);
911 stepNode{entryStep} = cacheNode;
912 if aidx <= length(lsn.callsof) && ~isempty(lsn.callsof{aidx})
913 line_warning(mfilename, sprintf(...
914 'Calls issued by cache read activity %s are ignored.', lsn.names{aidx}));
915 end
916 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx}, 'Distribution') && ...
917 ~isa(lsn.hostdem{aidx}, 'Immediate') && lsn.hostdem{aidx}.getMean() > GlobalConstants.FineTol
918 line_warning(mfilename, sprintf(...
919 'Host demand of cache read activity %s is ignored.', lsn.names{aidx}));
920 end
921 exitPort = [entryStep, 0];
922 return;
923 end
924 svc = [];
925 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx}, 'Distribution')
926 d = lsn.hostdem{aidx};
927 if ~isa(d, 'Immediate') && d.getMean() > GlobalConstants.FineTol
928 svc = d;
929 end
930 end
931
932 callStages = synchCallStages(aidx);
933 entryStep = addStep(aidx, hidx, svc, lsn.names{aidx}, false, false, refTidx);
934 % cur is the port through which the activity is currently left. A
935 % blocking call site is left through its reply signal.
936 cur = [entryStep, 0];
937
938 % A call blocks the caller's server only when host servers are finite, the task is not a thread pool,
939 % multiplicity is finite, and the chain is not open -- see _kb/04-networkstruct.md
940 hostBlocks = ~hostIsDelay(hidx) && ~fcrTask(tidx) && refTidx ~= 0 && ...
941 isfinite(lsn.mult(tidx)) && lsn.sched(tidx) ~= SchedStrategy.INF;
942
943 for k = 1:length(callStages)
944 stage = callStages(k);
945 % Async call does not hold the caller's server (returns via an external class switch); still serialised, which is the approximation
946 blocks = hostBlocks && ~stage.isasync;
947 % Async send also releases the caller's thread for the callee expansion
948 if stage.isasync
949 asyncThread = threadStack(end);
950 threadStack(end) = [];
951 end
952 [calleeFirst, calleeReplies, calleeTerms] = expandEntry(stage.targetEidx, refTidx);
953 if stage.isasync
954 threadStack(end+1) = asyncThread;
955 end
956 if isempty(calleeFirst)
957 continue; % callee not expandable: drop the call, never block
958 end
959 % A callee path that neither replies nor continues still holds a
960 % token, so it returns to the caller like a reply would.
961 calleeReplies = [calleeReplies; calleeTerms]; %#ok<AGROW>
962
963 % Merge step needed when the call may be skipped, another stage follows, or an AND-join branch tail must reach
964 % the Join in an ordinary class (a REPLY signal carries no forked-task identity) -- see _kb/04-networkstruct.md
965 needsMerge = (stage.prob < 1.0) || (k < length(callStages)) || ...
966 ~blocks || isAndJoinPre(aidx);
967 if needsMerge
968 nxt = addStep(aidx, hidx, [], sprintf('%s_c%d_ret', lsn.names{aidx}, k), false, false, refTidx);
969 if ~blocks
970 % Non-blocking return carries no signal, so the merge point can sit on a Router (no server to queue behind)
971 stepNode{nxt} = Router(model, sprintf('%s_c%d_ret', lsn.names{aidx}, k));
972 end
973 end
974
975 if blocks
976 % First mandatory call bound to the service class itself; an intervening class switch would release the server
977 if k == 1 && stage.prob >= 1.0 && isequal(cur, [entryStep, 0])
978 blk = entryStep;
979 stepBlocks(blk) = true;
980 else
981 blk = addStep(aidx, hidx, [], sprintf('%s_c%d', lsn.names{aidx}, k), true, false, refTidx);
982 addRoute(cur, blk, stage.prob);
983 if stage.prob < 1.0
984 addRoute(cur, nxt, 1.0 - stage.prob);
985 end
986 end
987 addRoute([blk, 0], calleeFirst, 1.0);
988 for r = 1:size(calleeReplies, 1)
989 reply(end+1, :) = [calleeReplies(r, 1), blk, calleeReplies(r, 2), calleeReplies(r, 3)]; %#ok<AGROW>
990 end
991 if needsMerge
992 addRoute([blk, 1], nxt, 1.0);
993 cur = [nxt, 0];
994 else
995 cur = [blk, 1];
996 end
997 else
998 addRoute(cur, calleeFirst, stage.prob);
999 if stage.prob < 1.0
1000 addRoute(cur, nxt, 1.0 - stage.prob);
1001 end
1002 for r = 1:size(calleeReplies, 1)
1003 addRoute(calleeReplies(r, 1:2), nxt, calleeReplies(r, 3));
1004 end
1005 cur = [nxt, 0];
1006 end
1007 end
1008 exitPort = cur;
1009 end
1010
1011 function cnode = getCacheNode(tidx)
1012 % One Cache node per CacheTask. The name is suffixed so that it does
1013 % not collide with the station of the task's processor.
1014 if isKey(cacheNodeOf, tidx)
1015 cnode = cacheNodeOf(tidx);
1016 return;
1017 end
1018 cnode = Cache(model, [lsn.names{tidx}, '_Cache'], lsn.nitems(tidx), ...
1019 lsn.itemcap{tidx}, lsn.replacestrat(tidx));
1020 cacheNodeOf(tidx) = cnode;
1021 end
1022
1023 function tf = isAndFork(succ)
1024 % An AND-fork is a precedence whose post activities are all marked
1025 % POST_AND: they are entered together, not with a probability each.
1026 tf = false;
1027 if length(succ) < 2 || ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
1028 return;
1029 end
1030 tf = all(arrayfun(@(s) s <= length(lsn.actposttype) && ...
1031 full(lsn.actposttype(s)) == ActivityPrecedenceType.POST_AND, succ));
1032 end
1033
1034 function tf = isAndJoinPre(aidx)
1035 % An activity marked PRE_AND is one branch tail of an AND-join.
1036 tf = isfield(lsn, 'actpretype') && ~isempty(lsn.actpretype) && ...
1037 aidx <= length(lsn.actpretype) && ...
1038 full(lsn.actpretype(aidx)) == ActivityPrecedenceType.PRE_AND;
1039 end
1040
1041 function addRoute(fromPort, toStep, prob)
1042 flow(end+1, :) = [fromPort(1), toStep, prob, fromPort(2), 0]; %#ok<AGROW>
1043 end
1044
1045 function addCacheRoute(fromStep, toStep)
1046 % Leaving a Cache node: the switch into the hit or the miss class is
1047 % made by the node, so the route is declared in the target class.
1048 flow(end+1, :) = [fromStep, toStep, 1.0, 0, 1]; %#ok<AGROW>
1049 end
1050
1051 function stages = synchCallStages(aidx)
1052 % Unrolls the synchronous calls of an activity into blocking stages.
1053 stages = struct('targetEidx', {}, 'prob', {}, 'isasync', {});
1054 if aidx > length(lsn.callsof) || isempty(lsn.callsof{aidx})
1055 return;
1056 end
1057 for cidx = lsn.callsof{aidx}
1058 ctype = full(lsn.calltype(cidx));
1059 if ctype ~= CallType.SYNC && ctype ~= CallType.ASYNC
1060 continue;
1061 end
1062 isasync = (ctype == CallType.ASYNC);
1063 targetEidx = lsn.callpair(cidx, 2);
1064 m = 1.0;
1065 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
1066 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx}, 'Distribution')
1067 m = lsn.callproc{cidx}.getMean();
1068 end
1069 nfull = floor(m + GlobalConstants.FineTol);
1070 frac = m - nfull;
1071 if nfull > MAXCALLSTAGES
1072 line_warning(mfilename, sprintf('Call multiplicity %g on %s truncated to %d stages.', ...
1073 m, lsn.callnames{cidx}, MAXCALLSTAGES));
1074 nfull = MAXCALLSTAGES;
1075 frac = 0;
1076 end
1077 for k = 1:nfull
1078 stages(end+1) = struct('targetEidx', targetEidx, 'prob', 1.0, 'isasync', isasync); %#ok<AGROW>
1079 end
1080 if frac > GlobalConstants.FineTol
1081 stages(end+1) = struct('targetEidx', targetEidx, 'prob', frac, 'isasync', isasync); %#ok<AGROW>
1082 end
1083 end
1084 end
1085
1086 function warnUnsupported(lsn)
1087 if any(full(lsn.calltype) == CallType.ASYNC)
1088 line_warning(mfilename, ['Asynchronous calls are represented by LQN2QN as ' ...
1089 'non-blocking visits: the caller releases its server but remains ' ...
1090 'serialised behind the callee.']);
1091 end
1092 if isfield(lsn, 'hasretrieval') && any(lsn.hasretrieval)
1093 line_warning(mfilename, 'Delayed-hit retrieval on the cache miss path is not represented by LQN2QN.');
1094 end
1095 end
1096
1097 function tf = taskHasAndFork(tidx_)
1098 % True if any activity of the task is the head of an AND-fork
1099 % branch, i.e. is marked POST_AND.
1100 tf = false;
1101 if ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
1102 return;
1103 end
1104 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
1105 if lsn.parent(a_) == tidx_ && a_ <= length(lsn.actposttype) && ...
1106 full(lsn.actposttype(a_)) == ActivityPrecedenceType.POST_AND
1107 tf = true;
1108 return;
1109 end
1110 end
1111 end
1112
1113 function v = ifempty(x, d)
1114 if isempty(x)
1115 v = d;
1116 else
1117 v = x;
1118 end
1119 end
1120
1121end
Definition fjtag.m:161