1function model = LQN2QN(lqn)
2% LQN2QN Convert a LayeredNetwork (LQN) to a Network (QN)
using REPLY signals
4% model = LQN2QN(lqn) flattens a LayeredNetwork into a single queueing
5% network in which synchronous call blocking
is represented by REPLY
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.
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.
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.
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.
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.
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.
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.
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.
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
88% lqn = LayeredNetwork('MyLQN
');
89% % ... define LQN model ...
91% SolverLDES(model).getAvgTable()
93% Copyright (c) 2012-2026, Imperial College London
97model = Network([lqn.getName(), '-QN
']);
99refTaskIndices = find(lsn.isref);
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
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>
113if isempty(refTaskIndices) && isempty(openEntries)
114 line_error(mfilename,
'LQN must have at least one reference task or open arrival.');
117MAXCALLSTAGES = 20; % guard against unrolling a huge call multiplicity
119%% Unsupported features, reported once each
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);
126 tidx_ = lsn.tshift + t_;
127 if lsn.isref(tidx_) || (tidx_ <= length(lsn.iscache) && lsn.iscache(tidx_))
130 if ~isfinite(lsn.mult(tidx_)) || lsn.sched(tidx_) == SchedStrategy.INF
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.'], ...
140 fcrTask(tidx_) =
true;
143% A task called transitively from inside an AND-fork branch
is excluded too -- see _kb/04-networkstruct.md
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
151 while ~isempty(frontier_)
152 cur_ = frontier_(1); frontier_(1) = [];
153 if any(branchActs == cur_)
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
162 succ_ = find(lsn.graph(cur_, :));
163 succ_ = succ_(succ_ > lsn.ashift & lsn.parent(succ_).
' == lsn.parent(cur_));
164 frontier_ = [frontier_, succ_]; %#ok<AGROW>
168if ~isempty(branchActs) && any(fcrTask)
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>
177 shadow_ = false(size(fcrTask));
178 while ~isempty(front_)
179 t_ = front_(1); front_(1) = [];
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>
192 for t_ = find(shadow_(:).' & fcrTask(:).
')
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.
'], ...
201%% Stations: one per host processor
202hostStation = cell(lsn.nhosts, 1);
203hostIsDelay = false(lsn.nhosts, 1);
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;
211 q = Queue(model, lsn.names{h}, sched);
212 q.setNumberOfServers(nservers);
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
']);
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
237% flow/reply/spawnPairs/ph2Exits step-graph arrays -- see _kb/04-networkstruct.md
240spawnPairs = zeros(0, 2);
241ph2Exits = zeros(0, 4);
243entryStack = []; % guards against recursive call cycles
244threadStack = []; % tasks holding a thread during expansion; tracks entryStack except across forwarding (releases the forwarder's thread)
246% Cache
nodes, one per CacheTask, and the read/hit/miss wiring to apply once
248cacheNodeOf = containers.Map(
'KeyType',
'double',
'ValueType',
'any');
249cacheWiring =
struct(
'node', {},
'readStep', {},
'hitStep', {},
'missStep', {}, ...
250 'itemproc', {},
'nitems', {});
252for rt = 1:length(refTaskIndices)
253 refTidx = refTaskIndices(rt);
254 thinkStep = addStep([], [], [], [lsn.names{refTidx},
'_Think'],
false,
true, refTidx);
256 entries = lsn.entriesof{refTidx};
258 [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx);
259 if isempty(firstStep)
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));
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');
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.', ...
287 openWiring(end+1) =
struct(
'eidx', eidx,
'firstStep', firstStep, ...
288 'exits', [replyExits; terminals]); %#ok<AGROW>
291%% Pass 2: create classes and reply signals
292nsteps = length(stepAidx);
293stepClass = cell(nsteps, 1);
294stepSignal = cell(nsteps, 1);
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
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));
309 stepClass{i} = ClosedClass(model, stepName{i}, 0, thinkNode(refTidx));
313 stepClass{i} = stepClass{stepClassOwner(i)};
318 sig = Signal(model, [stepName{i},
'_Reply'], SignalType.REPLY);
319 sig.forJobClass(stepClass{i});
324% Signal.m installs RAND routing at every node; clear it so that link()
325% only honours the routes set below.
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);
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)});
342% Phase-2 token destructor (closed chains): NEGATIVE signal at a station nothing
visits, one per reference task -- see _kb/04-networkstruct.md
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);
355 ph2DumpNode.setService(sig, Immediate());
356 ph2DestructorOf(rft) = sig;
360%% Pass 3: service times
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());
369 tn_ = thinkNode(stepRefTask(i));
370 tn_.setService(stepClass{i}, Immediate());
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());
382 tnode.setService(stepClass{i}, thinkDist);
386 if isempty(stepSvc{i})
387 station.setService(stepClass{i}, Immediate());
389 station.setService(stepClass{i}, stepSvc{i});
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
396 if ~isempty(stepSignal{i})
398 hostStation{h}.setService(stepSignal{i}, Immediate());
400 tkeys = cell2mat(thinkNode.keys);
403 tn.setService(stepSignal{i}, Immediate());
408%% Cache read/hit/miss wiring, now that the classes exist
409for w = 1:length(cacheWiring)
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});
417P = model.initRoutingMatrix();
419for e = 1:size(flow, 1)
420 i = flow(e, 1); j = flow(e, 2); p = flow(e, 3);
422 P{stepClass{j}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
424 P{stepSignal{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
426 P{stepClass{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
430for e = 1:size(reply, 1)
431 i = reply(e, 1); owner = reply(e, 2); viaSignal = reply(e, 3); p = reply(e, 4);
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;
437 P{stepClass{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
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);
445 % Open chain: the spawned token leaves through the Sink.
447 P{ecls, ecls}(stationOf(i), snkNode) = p;
449 P{stepSignal{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
451 P{stepClass{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
455%% Open arrival wiring: Source into the first step, exits into the Sink
456for w = 1:length(openWiring)
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);
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(:).');
474 Amat = zeros(length(fcrList), length(model.classes));
475 bvec = zeros(length(fcrList), 1);
477 for tsel = 1:length(fcrList)
479 if isempty(stepTasks{stp}) || ~any(stepTasks{stp} == fcrList(tsel))
482 Amat(tsel, stepClass{stp}.index) = 1;
484 if isa(nd,
'Station') && ~any(cellfun(@(x) x == nd, regionNodes))
485 regionNodes{end+1} = nd; %#ok<AGROW>
488 bvec(tsel) = lsn.mult(fcrList(tsel));
490 if any(Amat(:)) && ~isempty(regionNodes)
491 fcr = model.addRegion(regionNodes);
492 fcr.setConstraint(Amat, bvec);
496%% ---------------------------------------------------------------- helpers
498 function node = stationOf(i)
499 if ~isempty(stepNode{i})
501 elseif stepIsThink(i)
502 node = thinkNode(stepRefTask(i));
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>
523 stepTasks{end+1} = unique(threadStack(fcrTask(threadStack))); %#ok<AGROW>
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);
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.
539 replyExits = zeros(0, 3);
540 terminals = zeros(0, 3);
542 if any(entryStack == eidx)
543 line_warning(mfilename, sprintf(
'Recursive call cycle at entry %s truncated.', lsn.names{eidx}));
546 entryStack(end+1) = eidx;
547 threadStack(end+1) = lsn.parent(eidx);
548 restore = onCleanup(@() popEntry());
550 if eidx > length(lsn.actsof) || isempty(lsn.actsof{eidx})
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)
562 [firstStep, replyExits, terminals] = expandActivities(boundActs(1), eidx, refTidx);
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);
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);
579 for r = 1:size(ownPorts, 1)
580 addRoute(ownPorts(r, 1:2), fFirst, ownPorts(r, 3) * p);
583 fwdExits = [fwdExits; fReplies; fTerms]; %#ok<AGROW>
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];
592 function fwd = forwardingOf(eidx)
593 % Rows [targetEidx, probability] of the forwarding calls of an entry.
595 if ~isfield(lsn,
'calltype') || isempty(lsn.calltype)
598 for cidx = 1:size(lsn.callpair, 1)
599 if full(lsn.calltype(cidx)) ~= CallType.FWD || lsn.callpair(cidx, 1) ~= eidx
603 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
604 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx},
'Distribution')
605 p = lsn.callproc{cidx}.getMean();
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>
615 entryStack(end) = [];
616 threadStack(end) = [];
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.
624 replyExits = zeros(0, 3);
625 terminals = zeros(0, 3);
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
634 [firstStep, ~] = walk(a0);
636 % Every chain end returns the token to the caller: as a reply
if the entry replies anywhere,
else a plain terminal
638 replyExits = [replyExits; terminals]; %#ok<AGROW>
639 terminals = zeros(0, 3);
642 function [entryStep, exitPort] = walk(aidx)
643 if isKey(visited, aidx)
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.
652 if tidx <= length(lsn.iscache) && lsn.iscache(tidx) && full(lsn.graph(eidx, aidx)) > 0
653 cacheNode = getCacheNode(tidx);
655 [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode);
656 visited(aidx) = [entryStep, exitPort];
658 % Reply
is deferred to the ends of the chain, not emitted here -- see _kb/04-networkstruct.md
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));
671 % Local successors within the same entry.
672 succ = find(lsn.graph(aidx, :));
673 succ = succ(ismember(succ, localActs));
675 terminals(end+1, :) = [exitPort, 1.0]; %
#ok<AGROW>
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
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;
696 nT0 = size(terminals, 1);
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);
706 spawnPairs(end+1, :) = [trigH, sEntry]; %#ok<AGROW>
708 spawnPairs(end+1, :) = [trigM, sEntry]; %#ok<AGROW>
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>
716 threadStack = savedStack;
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];
730 ph2Spawn = okCtx && exitPort(2) == 0 && isempty(stepNode{exitPort(1)});
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}));
737 replyExits(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
738 savedStack = threadStack;
740 nT0 = size(terminals, 1);
741 posSucc = succ(full(lsn.graph(aidx, succ)) > 0);
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);
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));
755 elseif isscalar(posSucc)
756 [sEntry, ~] = walk(posSucc);
757 if isempty(stepNode{sEntry})
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);
766 [sEntry, ~] = walk(s2);
767 addRoute([head, 0], sEntry, full(lsn.graph(aidx, s2)));
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>
777 threadStack = savedStack;
782 if ~isempty(cacheNode)
783 % CacheAccess precedence: successors are hit then miss branch; the Cache node decides, so no probability on these routes
785 line_warning(mfilename, sprintf(...
786 'Cache read %s has no hit/miss pair; treated as an ordinary activity.
', ...
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>
801 wireAndFork(exitPort, succ, aidx);
805 if isAndJoinPre(aidx)
806 wireAndJoin(exitPort, succ(1));
811 p = full(lsn.graph(aidx, s));
815 [sEntry, ~] = walk(s);
816 addRoute(exitPort, sEntry, p);
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);
840 forkOwnerStack(end) = [];
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);
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);
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];
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.
877 while ~isempty(stack)
880 if any(seenActs == a)
883 seenActs(end+1) = a; %#ok<AGROW>
884 if isfield(lsn, 'replygraph
') && ~isempty(lsn.replygraph)
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))
897 nxt = find(lsn.graph(a, :));
898 stack = [stack, nxt(ismember(nxt, localActs))]; %#ok<AGROW>
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}));
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}));
921 exitPort = [entryStep, 0];
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
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];
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;
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
949 asyncThread = threadStack(end);
950 threadStack(end) = [];
952 [calleeFirst, calleeReplies, calleeTerms] = expandEntry(stage.targetEidx, refTidx);
954 threadStack(end+1) = asyncThread;
956 if isempty(calleeFirst)
957 continue; % callee not expandable: drop the call, never block
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>
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);
968 nxt = addStep(aidx, hidx, [], sprintf(
'%s_c%d_ret', lsn.names{aidx}, k),
false,
false, refTidx);
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));
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])
979 stepBlocks(blk) = true;
981 blk = addStep(aidx, hidx, [], sprintf('%s_c%d', lsn.names{aidx}, k),
true,
false, refTidx);
982 addRoute(cur, blk, stage.prob);
984 addRoute(cur, nxt, 1.0 - stage.prob);
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>
992 addRoute([blk, 1], nxt, 1.0);
998 addRoute(cur, calleeFirst, stage.prob);
1000 addRoute(cur, nxt, 1.0 - stage.prob);
1002 for r = 1:size(calleeReplies, 1)
1003 addRoute(calleeReplies(r, 1:2), nxt, calleeReplies(r, 3));
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);
1018 cnode = Cache(model, [lsn.names{tidx},
'_Cache'], lsn.nitems(tidx), ...
1019 lsn.itemcap{tidx}, lsn.replacestrat(tidx));
1020 cacheNodeOf(tidx) = cnode;
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.
1027 if length(succ) < 2 || ~isfield(lsn,
'actposttype') || isempty(lsn.actposttype)
1030 tf = all(arrayfun(@(s) s <= length(lsn.actposttype) && ...
1031 full(lsn.actposttype(s)) == ActivityPrecedenceType.POST_AND, succ));
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;
1041 function addRoute(fromPort, toStep, prob)
1042 flow(end+1, :) = [fromPort(1), toStep, prob, fromPort(2), 0]; %
#ok<AGROW>
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>
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})
1057 for cidx = lsn.callsof{aidx}
1058 ctype = full(lsn.calltype(cidx));
1059 if ctype ~= CallType.SYNC && ctype ~= CallType.ASYNC
1062 isasync = (ctype == CallType.ASYNC);
1063 targetEidx = lsn.callpair(cidx, 2);
1065 if isfield(lsn,
'callproc') && ~isempty(lsn.callproc) && ...
1066 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx},
'Distribution')
1067 m = lsn.callproc{cidx}.getMean();
1069 nfull = floor(m + GlobalConstants.FineTol);
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;
1078 stages(end+1) =
struct(
'targetEidx', targetEidx,
'prob', 1.0,
'isasync', isasync); %#ok<AGROW>
1080 if frac > GlobalConstants.FineTol
1081 stages(end+1) =
struct(
'targetEidx', targetEidx,
'prob', frac,
'isasync', isasync); %#ok<AGROW>
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.']);
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.');
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.
1101 if ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
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
1113 function v = ifempty(x, d)