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. Where the replying activity's exit
is not a plain
76% station departure (it ends on a call site, an AND precedence or a cache
77% read), phase 2 falls back to running before
the reply, with a warning.
79% Not yet represented: delayed-hit retrieval on
the cache miss path, and
the
80% thread pool of a task with an internal AND-fork. Each
is reported through
84% lqn = LayeredNetwork(
'MyLQN');
85% % ... define LQN model ...
87% SolverLDES(model).getAvgTable()
89% Copyright (c) 2012-2026, Imperial College London
93model = Network([lqn.getName(),
'-QN']);
95refTaskIndices = find(lsn.isref);
97%% Entries with an open arrival process
98% Requests of such an entry arrive from
the environment through a Source,
99% traverse
the entry
's expanded subgraph in open classes, and leave through
100% a Sink where the entry would reply. Calls issued along an open chain do
101% not hold the caller's server, so that no REPLY signal, a closed
class,
102% has to be woven into an open chain;
the thread pools of
the tasks
103% traversed are still enforced by
the finite capacity region.
105if isfield(lsn,
'arrival') && ~isempty(lsn.arrival)
106 for e_ = lsn.eshift+1:lsn.eshift+lsn.nentries
107 if e_ <= length(lsn.arrival) && isa(lsn.arrival{e_},
'Distribution') && ...
108 isfinite(lsn.arrival{e_}.getMean()) && lsn.arrival{e_}.getMean() > GlobalConstants.FineTol
109 openEntries(end+1) = e_; %#ok<AGROW>
114if isempty(refTaskIndices) && isempty(openEntries)
115 line_error(mfilename,
'LQN must have at least one reference task or open arrival.');
118MAXCALLSTAGES = 20; % guard against unrolling a huge call multiplicity
120%% Unsupported features, reported once each
123%% Tasks whose multiplicity
is a thread pool
124% Such a task holds one thread per request from entry to reply, also across
125% its nested synchronous calls. Its calls
do not hold
the caller
's server,
126% and a finite capacity region caps the jobs across its step classes at the
127% task multiplicity instead. A task with an internal AND-fork is excluded:
128% its forked siblings would double-count the thread that spawned them.
129fcrTask = false(lsn.nhosts + lsn.ntasks, 1);
131 tidx_ = lsn.tshift + t_;
132 if lsn.isref(tidx_) || (tidx_ <= length(lsn.iscache) && lsn.iscache(tidx_))
135 if ~isfinite(lsn.mult(tidx_)) || lsn.sched(tidx_) == SchedStrategy.INF
138 if taskHasAndFork(tidx_)
139 line_warning(mfilename, sprintf(['Multiplicity of task %s
is not enforced:
' ...
140 'an AND-fork inside a task cannot be capped by a finite capacity
' ...
141 'region, whose job
count would
double-
count the forked siblings.
'], ...
145 fcrTask(tidx_) = true;
148% A task called, transitively, from inside an AND-fork branch is excluded
149% too: the fork-join transformation retags branch flows into auxiliary
150% classes, which would silently bypass the task's admission row. Such a
151% task keeps
the server-hold blocking semantics instead.
153if isfield(lsn,
'actposttype') && ~isempty(lsn.actposttype)
154 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
155 if a_ > length(lsn.actposttype) || full(lsn.actposttype(a_)) ~= ActivityPrecedenceType.POST_AND
159 while ~isempty(frontier_)
160 cur_ = frontier_(1); frontier_(1) = [];
161 if any(branchActs == cur_)
164 branchActs(end+1) = cur_; %
#ok<AGROW>
165 if isfield(lsn,
'actpretype') && ~isempty(lsn.actpretype) && ...
166 cur_ <= length(lsn.actpretype) && ...
167 full(lsn.actpretype(cur_)) == ActivityPrecedenceType.PRE_AND
168 continue; % branch tail:
do not traverse past
the join
170 succ_ = find(lsn.graph(cur_, :));
171 succ_ = succ_(succ_ > lsn.ashift & lsn.parent(succ_).
' == lsn.parent(cur_));
172 frontier_ = [frontier_, succ_]; %#ok<AGROW>
176if ~isempty(branchActs) && any(fcrTask)
179 if a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
180 for c_ = lsn.callsof{a_}
181 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
185 shadow_ = false(size(fcrTask));
186 while ~isempty(front_)
187 t_ = front_(1); front_(1) = [];
192 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
193 if lsn.parent(a_) == t_ && a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
194 for c_ = lsn.callsof{a_}
195 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
200 for t_ = find(shadow_(:).' & fcrTask(:).
')
202 line_warning(mfilename, sprintf(['Multiplicity of task %s
is not enforced:
' ...
203 'it
is called from inside an AND-fork branch, whose flows
the ' ...
204 'fork-join transformation retags outside
the admission constraint.
'], ...
209%% Stations: one per host processor
210hostStation = cell(lsn.nhosts, 1);
211hostIsDelay = false(lsn.nhosts, 1);
213 nservers = lsn.mult(h);
214 sched = lsn.sched(h);
215 if isinf(nservers) || sched == SchedStrategy.INF
216 hostStation{h} = Delay(model, lsn.names{h});
217 hostIsDelay(h) = true;
219 q = Queue(model, lsn.names{h}, sched);
220 q.setNumberOfServers(nservers);
225%% Think delays, one per reference task
226thinkNode = containers.Map('KeyType
', 'double
', 'ValueType
', 'any
');
227for rt = 1:length(refTaskIndices)
228 refTidx = refTaskIndices(rt);
229 thinkNode(refTidx) = Delay(model, [lsn.names{refTidx}, '_Think
']);
232%% Pass 1: expand the activity graph into a step graph
233% Steps carry no LINE objects yet, so that all classes can be created in
234% one block before the routing matrix is initialised.
235stepAidx = []; % activity index of the step
236stepHost = []; % host processor index of the step station
237stepSvc = {}; % service distribution at the step station, [] if none
238stepName = {}; % class name
239stepBlocks = []; % true if the step blocks on a synchronous call
240stepIsThink = []; % true for a think step (reference task)
241stepRefTask = []; % reference task the step belongs to
242stepNode = {}; % Fork/Join/Router node the step sits on, [] for stations
243stepClassOwner = []; % step whose class this step travels in (itself, normally)
244stepTasks = {}; % thread-pool tasks holding a thread while at this step
246% flow(i,:) = [fromStep, toStep, prob, fromIsSignal, inTargetClass]. A step
247% is left either through its own class or, when it is a blocking call site,
248% through the reply signal that unblocks it. A Cache node switches class on
249% its own, so the routes leaving it are written in the class of the target
250% step, which is what inTargetClass marks.
252% reply(i,:) = [calleeExitStep, callerStep, calleeExitIsSignal, prob]: class
253% switch into the caller's reply signal, destined for
the caller station.
254% prob
is below one only where
the callee entry forwards part of its replies.
256% spawnPairs(i,:) = [triggerStep, targetStep]: each service completion of
the
257% trigger class spawns a job of
the target class at
the same station
258% (sn.classspawn), which
is how a phase-2 continuation survives
the reply.
259spawnPairs = zeros(0, 2);
260% ph2Exits(i,:) = [step, isSignal, prob, refTidx]: end of a phase-2 chain.
261% The spawned token
is destroyed there: on a closed chain it switches into a
262% NEGATIVE signal aimed at a station nothing
visits, whose miss annihilates
263% it; on an open chain it leaves through
the Sink.
264ph2Exits = zeros(0, 4);
266entryStack = []; % guards against recursive call cycles
267threadStack = []; % tasks holding a thread during
the current expansion; it
268 % tracks entryStack except across forwarding, which hands
269 %
the request on and releases
the forwarder
's thread
271% Cache nodes, one per CacheTask, and the read/hit/miss wiring to apply once
273cacheNodeOf = containers.Map('KeyType
', 'double', 'ValueType
', 'any
');
274cacheWiring = struct('node
', {}, 'readStep
', {}, 'hitStep
', {}, 'missStep
', {}, ...
275 'itemproc
', {}, 'nitems
', {});
277for rt = 1:length(refTaskIndices)
278 refTidx = refTaskIndices(rt);
279 thinkStep = addStep([], [], [], [lsn.names{refTidx}, '_Think
'], false, true, refTidx);
281 entries = lsn.entriesof{refTidx};
283 [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx);
284 if isempty(firstStep)
287 addRoute([thinkStep, 0], firstStep, 1.0);
288 % A reference task has no caller: its replies and its dead ends
289 % both close the cycle at the think delay.
290 exits = [replyExits; terminals];
291 for s = 1:size(exits, 1)
292 addRoute(exits(s, 1:2), thinkStep, exits(s, 3));
297%% Pass 1b: open arrival chains
298% openWiring rows: {eidx, firstStep, exits}, wired to Source and Sink in
299% pass 4 once the classes exist. The reference task index 0 marks a step of
301openWiring = struct('eidx
', {}, 'firstStep
', {}, 'exits
', {});
302srcNode = []; snkNode = [];
303if ~isempty(openEntries)
304 srcNode = Source(model, 'Source
');
305 snkNode = Sink(model, 'Sink
');
307for eidx = openEntries
308 [firstStep, replyExits, terminals] = expandEntry(eidx, 0);
309 if isempty(firstStep)
310 line_warning(mfilename, sprintf('Open arrival entry %s has no bound activity; ignored.
', ...
314 openWiring(end+1) = struct('eidx
', eidx, 'firstStep
', firstStep, ...
315 'exits
', [replyExits; terminals]); %#ok<AGROW>
318%% Pass 2: create classes and reply signals
319nsteps = length(stepAidx);
320stepClass = cell(nsteps, 1);
321stepSignal = cell(nsteps, 1);
324 refTidx = stepRefTask(i);
325 if stepClassOwner(i) ~= i
326 % Fork, Join and Router steps carry the job through unchanged: they
327 % travel in the class of the step that entered the fork, which is
328 % also the class a Join matches its siblings on.
332 % A step of an open arrival chain travels in an open class.
333 stepClass{i} = OpenClass(model, stepName{i});
334 elseif stepIsThink(i)
335 population = lsn.mult(refTidx);
336 stepClass{i} = ClosedClass(model, stepName{i}, population, thinkNode(refTidx));
338 stepClass{i} = ClosedClass(model, stepName{i}, 0, thinkNode(refTidx));
342 stepClass{i} = stepClass{stepClassOwner(i)};
347 sig = Signal(model, [stepName{i}, '_Reply
'], SignalType.REPLY);
348 sig.forJobClass(stepClass{i});
353% Signal.m installs RAND routing at every node; clear it so that link()
354% only honours the routes set below.
356 if ~isempty(stepSignal{i})
357 for n = 1:length(model.nodes)
358 if ~isa(model.nodes{n}, 'Sink
')
359 model.nodes{n}.setRouting(stepSignal{i}, RoutingStrategy.DISABLED);
365% Spawn bindings for phase-2 continuations: each completion of the trigger
366% class injects a job of the target class at the same station.
367for sp = 1:size(spawnPairs, 1)
368 stepClass{spawnPairs(sp, 1)}.setSpawnClass(stepClass{spawnPairs(sp, 2)});
371% Destructor for spawned phase-2 tokens on closed chains: the end of a
372% phase-2 chain switches into a NEGATIVE signal aimed at a station that
373% nothing else visits, so the signal always misses and is annihilated. One
374% destructor per reference task, so that each stays in the chain of the
375% phase-2 classes it collects, whose reference station it shares.
377ph2DestructorOf = containers.Map('KeyType
', 'double', 'ValueType
', 'any
');
378if ~isempty(ph2Exits) && any(ph2Exits(:, 4) > 0)
379 ph2DumpNode = Queue(model, 'Ph2Sink
', SchedStrategy.FCFS);
380 for rft = unique(ph2Exits(ph2Exits(:, 4) > 0, 4).')
381 sig = ClosedSignal(model, sprintf(
'Ph2End_%s', lsn.names{rft}), ...
382 SignalType.NEGATIVE, thinkNode(rft));
383 for n = 1:length(model.nodes)
384 if ~isa(model.
nodes{n},
'Sink')
385 model.nodes{n}.setRouting(sig, RoutingStrategy.DISABLED);
388 ph2DumpNode.setService(sig, Immediate());
389 ph2DestructorOf(rft) = sig;
393%% Pass 3: service times
395 if ~isempty(stepNode{i})
396 % Fork/Join/Router
nodes hold no service, but a Router-hosted merge
397 % step owns a
class, which must have a service pair somewhere: it
is
398 % declared Immediate at
the reference think delay, as for signals.
399 if stepClassOwner(i) == i && isa(stepNode{i},
'Router')
400 if stepRefTask(i) == 0
401 % Open chain: no think delay exists, declare
the pair at
the
402 % caller
's host station instead, which the class never visits.
403 hostStation{stepHost(i)}.setService(stepClass{i}, Immediate());
405 tn_ = thinkNode(stepRefTask(i));
406 tn_.setService(stepClass{i}, Immediate());
412 refTidx = stepRefTask(i);
413 thinkDist = lsn.think{refTidx};
414 tnode = thinkNode(refTidx);
415 if isempty(thinkDist) || isa(thinkDist, 'Immediate
') || thinkDist.getMean() < GlobalConstants.FineTol
416 tnode.setService(stepClass{i}, Immediate());
418 tnode.setService(stepClass{i}, thinkDist);
421 station = hostStation{stepHost(i)};
422 if isempty(stepSvc{i})
423 station.setService(stepClass{i}, Immediate());
425 station.setService(stepClass{i}, stepSvc{i});
430% A reply signal is consumed at the station of the caller it unblocks and
431% passed on without further service. Service is declared at every station
432% because a signal that finds no pending reply falls back to its reference
433% station, which must not be a disabled pair.
435 if ~isempty(stepSignal{i})
437 hostStation{h}.setService(stepSignal{i}, Immediate());
439 tkeys = cell2mat(thinkNode.keys);
442 tn.setService(stepSignal{i}, Immediate());
447%% Cache read/hit/miss wiring, now that the classes exist
448for w = 1:length(cacheWiring)
450 cw.node.setReadItemEntry(stepClass{cw.readStep}, cw.itemproc, cw.nitems);
451 cw.node.setHitClass(stepClass{cw.readStep}, stepClass{cw.hitStep});
452 cw.node.setMissClass(stepClass{cw.readStep}, stepClass{cw.missStep});
456P = model.initRoutingMatrix();
458for e = 1:size(flow, 1)
459 i = flow(e, 1); j = flow(e, 2); p = flow(e, 3);
461 P{stepClass{j}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
463 P{stepSignal{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
465 P{stepClass{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
469for e = 1:size(reply, 1)
470 i = reply(e, 1); owner = reply(e, 2); viaSignal = reply(e, 3); p = reply(e, 4);
472 % A nested call returns through its own reply signal, which
473 % class-switches into the reply signal of the outer call site.
474 P{stepSignal{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
476 P{stepClass{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
480%% Phase-2 chain ends: destroy the spawned token
481for e = 1:size(ph2Exits, 1)
482 i = ph2Exits(e, 1); viaSig = ph2Exits(e, 2); p = ph2Exits(e, 3); rft = ph2Exits(e, 4);
484 % Open chain: the spawned token leaves through the Sink.
486 P{ecls, ecls}(stationOf(i), snkNode) = p;
488 P{stepSignal{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
490 P{stepClass{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
494%% Open arrival wiring: Source into the first step, exits into the Sink
495for w = 1:length(openWiring)
497 firstCls = stepClass{ow.firstStep};
498 srcNode.setArrival(firstCls, lsn.arrival{ow.eidx});
499 P{firstCls, firstCls}(srcNode, stationOf(ow.firstStep)) = 1.0;
500 for s = 1:size(ow.exits, 1)
501 % Open chains carry no signals, so every exit is an ordinary class.
502 ecls = stepClass{ow.exits(s, 1)};
503 P{ecls, ecls}(stationOf(ow.exits(s, 1)), snkNode) = ow.exits(s, 3);
509%% Thread pools: one finite capacity region, one linear constraint per task
510% The region spans every station visited by a thread-pool task's step
511% classes. Each task contributes one admission row A(t,:)*x <= mult(t),
512% where x counts in-region jobs per class, so a job entering
the task, or
513% one of its nested callees, waits outside until a thread
is free. Class
514% coefficients are shared across rows where tasks nest, which keeps
515% intra-region class switches admissible:
the sums are left unchanged.
516fcrList = find(fcrTask(:).
');
518 Amat = zeros(length(fcrList), length(model.classes));
519 bvec = zeros(length(fcrList), 1);
521 for tsel = 1:length(fcrList)
523 if isempty(stepTasks{stp}) || ~any(stepTasks{stp} == fcrList(tsel))
526 Amat(tsel, stepClass{stp}.index) = 1;
528 if isa(nd, 'Station') && ~any(cellfun(@(x) x == nd, regionNodes))
529 regionNodes{end+1} = nd; %#ok<AGROW>
532 bvec(tsel) = lsn.mult(fcrList(tsel));
534 if any(Amat(:)) && ~isempty(regionNodes)
535 fcr = model.addRegion(regionNodes);
536 fcr.setConstraint(Amat, bvec);
540%% ---------------------------------------------------------------- helpers
542 function node = stationOf(i)
543 if ~isempty(stepNode{i})
545 elseif stepIsThink(i)
546 node = thinkNode(stepRefTask(i));
548 node = hostStation{stepHost(i)};
552 function id = addStep(aidx, hidx, svc, name, blocks, isthink, refTidx)
553 stepAidx(end+1) = ifempty(aidx, 0); %#ok<AGROW>
554 stepHost(end+1) = ifempty(hidx, 0); %#ok<AGROW>
555 stepSvc{end+1} = svc; %#ok<AGROW>
556 stepName{end+1} = name; %#ok<AGROW>
557 stepBlocks(end+1) = blocks; %#ok<AGROW>
558 stepIsThink(end+1) = isthink; %#ok<AGROW>
559 stepRefTask(end+1) = refTidx; %#ok<AGROW>
560 stepNode{end+1} = []; %#ok<AGROW>
561 id = length(stepAidx);
562 stepClassOwner(end+1) = id; %#ok<AGROW>
563 % A thread of every thread-pool task on the stack is held while a
564 % job is at this step, since a synchronous caller does not release
565 % its thread until the reply.
566 if isempty(threadStack)
567 stepTasks{end+1} = []; %#ok<AGROW>
569 stepTasks{end+1} = unique(threadStack(fcrTask(threadStack))); %#ok<AGROW>
573 function id = addAuxStep(nodeObj, ownerStep, name, refTidx)
574 % A step on a Fork, Join or Router node: no station, no service, and
575 % no class of its own.
576 id = addStep([], [], [], name, false, false, refTidx);
577 stepNode{id} = nodeObj;
578 stepClassOwner(id) = stepClassOwner(ownerStep);
581 function [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx)
582 % Expands the activity subgraph bound to an entry, in the call
583 % context given by the current entry stack.
585 replyExits = zeros(0, 3);
586 terminals = zeros(0, 3);
588 if any(entryStack == eidx)
589 line_warning(mfilename, sprintf('Recursive call cycle at entry %s truncated.
', lsn.names{eidx}));
592 entryStack(end+1) = eidx;
593 threadStack(end+1) = lsn.parent(eidx);
594 restore = onCleanup(@() popEntry());
596 if eidx > length(lsn.actsof) || isempty(lsn.actsof{eidx})
600 % The bound activity is the activity successor of the entry.
601 succ = find(lsn.graph(eidx, :));
602 boundActs = succ(arrayfun(@(a) a <= length(lsn.type) && lsn.type(a) == LayeredNetworkElement.ACTIVITY, succ));
603 boundActs = intersect(boundActs, lsn.actsof{eidx}, 'stable
');
604 if isempty(boundActs)
608 [firstStep, replyExits, terminals] = expandActivities(boundActs(1), eidx, refTidx);
610 % Forwarding: with probability p the entry hands the request on to
611 % another entry instead of replying, and that entry replies directly
612 % to the original caller. The forwarder is released at that point,
613 % while the caller stays blocked, so the forwarded chain's own reply
614 % exits become
the reply exits of this entry.
615 fwd = forwardingOf(eidx);
616 if ~isempty(fwd) && ~isempty(replyExits)
617 ownPorts = replyExits;
618 fwdExits = zeros(0, 3);
620 % The forwarder
's thread is released at the handoff, so the
621 % forwarded chain is expanded without it on the thread stack.
622 fwdThread = threadStack(end);
623 threadStack(end) = [];
624 for f = 1:size(fwd, 1)
625 [fFirst, fReplies, fTerms] = expandEntry(fwd(f, 1), refTidx);
630 for r = 1:size(ownPorts, 1)
631 addRoute(ownPorts(r, 1:2), fFirst, ownPorts(r, 3) * p);
634 fwdExits = [fwdExits; fReplies; fTerms]; %#ok<AGROW>
636 threadStack(end+1) = fwdThread;
637 % What is left of each of this entry's own ports still replies.
638 replyExits(:, 3) = replyExits(:, 3) * max(0.0, 1.0 - pforw);
639 replyExits = [replyExits; fwdExits];
643 function fwd = forwardingOf(eidx)
644 % Rows [targetEidx, probability] of
the forwarding calls of an entry.
646 if ~isfield(lsn,
'calltype') || isempty(lsn.calltype)
649 for cidx = 1:size(lsn.callpair, 1)
650 if full(lsn.calltype(cidx)) ~= CallType.FWD || lsn.callpair(cidx, 1) ~= eidx
654 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
655 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx},
'Distribution')
656 p = lsn.callproc{cidx}.getMean();
658 p = min(max(p, 0.0), 1.0);
659 if p > GlobalConstants.FineTol
660 fwd(end+1, :) = [lsn.callpair(cidx, 2), p]; %#ok<AGROW>
666 entryStack(end) = [];
667 threadStack(end) = [];
670 function [firstStep, replyExits, terminals] = expandActivities(a0, eidx, refTidx)
671 % Walks
the intra-task activity graph from a0, creating steps and
672 % expanding every synchronous call site. Reply exits and terminals
673 % are returned as ports, [step, isSignal] rows.
675 replyExits = zeros(0, 3);
676 terminals = zeros(0, 3);
678 tidx = lsn.parent(a0);
679 localActs = lsn.actsof{eidx};
680 visited = containers.Map(
'KeyType',
'double',
'ValueType',
'any'); % aidx -> [entryStep, exitStep, exitIsSignal]
681 joinOf = containers.Map(
'KeyType',
'double',
'ValueType',
'any'); % join activity -> [joinStep, entryStep]
682 forkOwnerStack = []; %
class-owner step of each enclosing AND-fork
685 [firstStep, ~] = walk(a0);
687 % Every end of
the chain returns
the token to
the caller: as a reply
688 %
if the entry replies anywhere, otherwise as a plain terminal, which
689 %
the caller still has to route somewhere or
the token
is lost.
691 replyExits = [replyExits; terminals]; %#ok<AGROW>
692 terminals = zeros(0, 3);
695 function [entryStep, exitPort] = walk(aidx)
696 if isKey(visited, aidx)
702 % The activity bound to an ItemEntry of a CacheTask
is the read
703 % step: it sits on
the Cache node rather than on
the processor.
705 if tidx <= length(lsn.iscache) && lsn.iscache(tidx) && full(lsn.graph(eidx, aidx)) > 0
706 cacheNode = getCacheNode(tidx);
708 [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode);
709 visited(aidx) = [entryStep, exitPort];
711 % Reply to
the calling entry,
if this activity replies. The reply
712 %
is not emitted here: an activity may reply and still have
713 % successors (phase 2), and emitting
the reply at that point
714 % would add an outgoing route on top of
the ones to
the
715 % successors. The reply
is therefore deferred to
the ends of
the
716 % chain, which
is what makes phase-2 activities run before
the
717 % reply rather than after it.
719 if isfield(lsn,
'replygraph') && ~isempty(lsn.replygraph)
720 a = aidx - lsn.ashift;
721 e = eidx - lsn.eshift;
722 if a >= 1 && a <= size(lsn.replygraph, 1) && e >= 1 && e <= size(lsn.replygraph, 2)
723 repliesHere = full(lsn.replygraph(a, e));
730 % Local successors within
the same entry.
731 succ = find(lsn.graph(aidx, :));
732 succ = succ(ismember(succ, localActs));
734 terminals(end+1, :) = [exitPort, 1.0]; %
#ok<AGROW>
738 % Phase 2:
the successors of a replying activity run after
the
739 % reply. The exit port becomes a reply exit, and each service
740 % completion of its
class spawns
the continuation at
the host
741 % station (sn.classspawn), so
the reply token returns to
the
742 % caller
while the second phase proceeds concurrently. Only
the
743 % task
's own thread persists through phase 2: the callers have
744 % already been resumed by the reply.
745 % This needs the exit to be a station departure in the step's own
746 %
class: a port on a Router or Cache node, or a reply-signal
747 % port, never completes service, so no spawn can hang off it and
748 % phase 2 falls back to running before
the reply.
750 ph2Spawn = isempty(cacheNode) && exitPort(2) == 0 && ...
751 isempty(stepNode{exitPort(1)}) && ~isAndFork(succ) && ~isAndJoinPre(aidx);
753 line_warning(mfilename, sprintf([
'Phase-2 activities of %s run ' ...
754 'before the reply: its exit is not a plain station departure.'], ...
757 replyExits(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
758 savedStack = threadStack;
760 nT0 = size(terminals, 1);
761 posSucc = succ(full(lsn.graph(aidx, succ)) > 0);
764 [sEntry, ~] = walk(posSucc);
765 if isempty(stepNode{sEntry})
770 % Branching phase 2, or a head on a non-station node:
771 % spawn into an immediate head step that carries
the
772 % branch probabilities.
773 head = addStep(aidx, lsn.parent(tidx), [], ...
774 [lsn.names{aidx},
'_ph2'],
false,
false, refTidx);
776 [sEntry, ~] = walk(s2);
777 addRoute([head, 0], sEntry, full(lsn.graph(aidx, s2)));
781 spawnPairs(end+1, :) = [exitPort(1), target]; %#ok<AGROW>
782 ph2New = terminals(nT0+1:end, :);
783 terminals(nT0+1:end, :) = [];
784 for r2 = 1:size(ph2New, 1)
785 ph2Exits(end+1, :) = [ph2New(r2, 1:3), refTidx]; %
#ok<AGROW>
787 threadStack = savedStack;
792 if ~isempty(cacheNode)
793 % CacheAccess precedence:
the successors are
the hit branch
794 % and
the miss branch, in that order. The Cache node itself
795 % decides which one a job takes, so no probability
is put on
798 line_warning(mfilename, sprintf(...
799 'Cache read %s has no hit/miss pair; treated as an ordinary activity.', ...
802 [hEntry, ~] = walk(succ(1));
803 [mEntry, ~] = walk(succ(2));
804 addCacheRoute(entryStep, hEntry);
805 addCacheRoute(entryStep, mEntry);
806 cacheWiring(end+1) =
struct(
'node', cacheNode, ...
807 'readStep', entryStep,
'hitStep', hEntry,
'missStep', mEntry, ...
808 'itemproc', lsn.itemproc{eidx},
'nitems', lsn.nitems(eidx)); %#ok<AGROW>
814 % AND-fork:
the branches run concurrently, so
the job
is
815 % replicated by a Fork node. One Router per branch, because a
816 % Fork cannot
switch class per output link, and
the branch
817 %
class is what tells
the branch apart.
818 forkNode = Fork(model, [
'Fork_', lsn.names{aidx}]);
819 forkStep = addAuxStep(forkNode, exitPort(1), [
'Fork_', lsn.names{aidx}], refTidx);
820 addRoute(exitPort, forkStep, 1.0);
821 forkOwnerStack(end+1) = forkStep; %#ok<AGROW>
822 for b = 1:length(succ)
823 routerNode = Router(model, sprintf(
'Fork_%s_%d', lsn.names{aidx}, b));
824 routerStep = addAuxStep(routerNode, forkStep, ...
825 sprintf(
'Fork_%s_%d', lsn.names{aidx}, b), refTidx);
826 addRoute([forkStep, 0], routerStep, 1.0);
827 [sEntry, ~] = walk(succ(b));
828 addRoute([routerStep, 0], sEntry, 1.0);
830 forkOwnerStack(end) = [];
834 if isAndJoinPre(aidx)
835 % Tail of an AND-fork branch: it must reach
the Join, and
the
836 % siblings are matched there in
the class that entered
the
837 % fork, so
switch back to it on
the way in.
839 if isKey(joinOf, joinAidx)
840 js = joinOf(joinAidx);
841 addRoute(exitPort, js(1), 1.0);
843 if isempty(forkOwnerStack)
844 line_warning(mfilename, sprintf(...
845 'AND-join at %s has no enclosing AND-fork; branches are serialised.', ...
846 lsn.names{joinAidx}));
847 [sEntry, ~] = walk(joinAidx);
848 addRoute(exitPort, sEntry, 1.0);
851 joinNode = Join(model, [
'Join_', lsn.names{joinAidx}], stepNode{forkOwnerStack(end)});
852 joinStep = addAuxStep(joinNode, forkOwnerStack(end), ...
853 [
'Join_', lsn.names{joinAidx}], refTidx);
854 addRoute(exitPort, joinStep, 1.0);
855 [sEntry, ~] = walk(joinAidx);
856 addRoute([joinStep, 0], sEntry, 1.0);
857 joinOf(joinAidx) = [joinStep, sEntry];
863 p = full(lsn.graph(aidx, s));
867 [sEntry, ~] = walk(s);
868 addRoute(exitPort, sEntry, p);
873 function [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode)
874 % One step
for the host demand, plus one step per unrolled
875 % synchronous call stage.
876 hidx = lsn.parent(tidx);
877 if nargin >= 4 && ~isempty(cacheNode)
878 % A read step holds no demand and issues no call:
the lookup
is
879 % instantaneous and
the work
is done on
the hit or miss branch.
880 entryStep = addStep(aidx, hidx, [], lsn.names{aidx},
false,
false, refTidx);
881 stepNode{entryStep} = cacheNode;
882 if aidx <= length(lsn.callsof) && ~isempty(lsn.callsof{aidx})
883 line_warning(mfilename, sprintf(...
884 'Calls issued by cache read activity %s are ignored.', lsn.names{aidx}));
886 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx},
'Distribution') && ...
887 ~isa(lsn.hostdem{aidx},
'Immediate') && lsn.hostdem{aidx}.getMean() > GlobalConstants.FineTol
888 line_warning(mfilename, sprintf(...
889 'Host demand of cache read activity %s is ignored.', lsn.names{aidx}));
891 exitPort = [entryStep, 0];
895 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx},
'Distribution')
896 d = lsn.hostdem{aidx};
897 if ~isa(d,
'Immediate') && d.getMean() > GlobalConstants.FineTol
902 callStages = synchCallStages(aidx);
903 entryStep = addStep(aidx, hidx, svc, lsn.names{aidx},
false,
false, refTidx);
904 % cur
is the port through which
the activity
is currently left. A
905 % blocking call site
is left through its reply signal.
906 cur = [entryStep, 0];
908 % A call blocks a server only where
the caller
's host has finitely
909 % many of them. On an INF host there is no server to hold, and a
910 % REPLY signal arriving at a delay finds no pending reply and is
911 % annihilated, so such a call returns by an ordinary class switch.
912 % A thread-pool task also does not hold its server across a call:
913 % the processor is released while its thread waits for the reply,
914 % and the finite capacity region caps its concurrency instead. A
915 % call on an open chain (refTidx 0) never holds either, so that no
916 % REPLY signal, a closed class, is woven into an open chain.
917 hostBlocks = ~hostIsDelay(hidx) && ~fcrTask(tidx) && refTidx ~= 0;
919 for k = 1:length(callStages)
920 stage = callStages(k);
921 % An asynchronous call does not hold the caller's server:
the
922 % token
visits the callee and comes back by a
class switch, which
923 %
is an external node and so releases
the server. It
is still
924 % serialised with
the caller, which
is the approximation.
925 blocks = hostBlocks && ~stage.isasync;
926 % An asynchronous send also releases
the caller
's thread: the
927 % callee is expanded without the caller on the thread stack, so
928 % the serialised visit does not count against the caller's pool.
930 asyncThread = threadStack(end);
931 threadStack(end) = [];
933 [calleeFirst, calleeReplies, calleeTerms] = expandEntry(stage.targetEidx, refTidx);
935 threadStack(end+1) = asyncThread;
937 if isempty(calleeFirst)
938 continue; % callee not expandable: drop
the call, never block
940 % A callee path that neither replies nor continues still holds a
941 % token, so it returns to
the caller like a reply would.
942 calleeReplies = [calleeReplies; calleeTerms]; %#ok<AGROW>
944 % A merge step
is needed whenever
the call may be skipped, or
945 % whenever a further stage must follow. Where it
is not needed
946 %
the reply signal itself carries
the activity onward, which
947 % matters because a
class switch
is performed by an external
948 % ClassSwitch node: re-entering
the caller
's own station would
949 % release its server and admit spurious concurrency.
950 % A branch tail of an AND-join must reach the Join in an
951 % ordinary class: a REPLY signal carries no forked-task
952 % identity, so it would never be matched with its siblings.
953 needsMerge = (stage.prob < 1.0) || (k < length(callStages)) || ...
954 ~blocks || isAndJoinPre(aidx);
956 nxt = addStep(aidx, hidx, [], sprintf('%s_c%d_ret
', lsn.names{aidx}, k), false, false, refTidx);
958 % A non-blocking return carries no signal, so the merge
959 % point can sit on a Router: re-entering the caller's
960 % station would queue a zero-work continuation behind
962 stepNode{nxt} = Router(model, sprintf(
'%s_c%d_ret', lsn.names{aidx}, k));
967 % The first mandatory call
is bound to
the service
class
968 % itself rather than to a separate stage,
for the same
969 % reason: an intervening
class switch would release
the
970 % server between
the host demand and
the call.
971 if k == 1 && stage.prob >= 1.0 && isequal(cur, [entryStep, 0])
973 stepBlocks(blk) = true;
975 blk = addStep(aidx, hidx, [], sprintf('%s_c%d', lsn.names{aidx}, k),
true,
false, refTidx);
976 addRoute(cur, blk, stage.prob);
978 addRoute(cur, nxt, 1.0 - stage.prob);
981 addRoute([blk, 0], calleeFirst, 1.0);
982 for r = 1:size(calleeReplies, 1)
983 reply(end+1, :) = [calleeReplies(r, 1), blk, calleeReplies(r, 2), calleeReplies(r, 3)]; %
#ok<AGROW>
986 addRoute([blk, 1], nxt, 1.0);
992 addRoute(cur, calleeFirst, stage.prob);
994 addRoute(cur, nxt, 1.0 - stage.prob);
996 for r = 1:size(calleeReplies, 1)
997 addRoute(calleeReplies(r, 1:2), nxt, calleeReplies(r, 3));
1005 function cnode = getCacheNode(tidx)
1006 % One Cache node per CacheTask. The name
is suffixed so that it does
1007 % not collide with
the station of
the task's processor.
1008 if isKey(cacheNodeOf, tidx)
1009 cnode = cacheNodeOf(tidx);
1012 cnode = Cache(model, [lsn.names{tidx},
'_Cache'], lsn.nitems(tidx), ...
1013 lsn.itemcap{tidx}, lsn.replacestrat(tidx));
1014 cacheNodeOf(tidx) = cnode;
1017 function tf = isAndFork(succ)
1018 % An AND-fork
is a precedence whose post activities are all marked
1019 % POST_AND: they are entered together, not with a probability each.
1021 if length(succ) < 2 || ~isfield(lsn,
'actposttype') || isempty(lsn.actposttype)
1024 tf = all(arrayfun(@(s) s <= length(lsn.actposttype) && ...
1025 full(lsn.actposttype(s)) == ActivityPrecedenceType.POST_AND, succ));
1028 function tf = isAndJoinPre(aidx)
1029 % An activity marked PRE_AND
is one branch tail of an AND-join.
1030 tf = isfield(lsn, 'actpretype') && ~isempty(lsn.actpretype) && ...
1031 aidx <= length(lsn.actpretype) && ...
1032 full(lsn.actpretype(aidx)) == ActivityPrecedenceType.PRE_AND;
1035 function addRoute(fromPort, toStep, prob)
1036 flow(end+1, :) = [fromPort(1), toStep, prob, fromPort(2), 0]; %
#ok<AGROW>
1039 function addCacheRoute(fromStep, toStep)
1040 % Leaving a Cache node:
the switch into
the hit or
the miss
class is
1041 % made by
the node, so
the route
is declared in
the target
class.
1042 flow(end+1, :) = [fromStep, toStep, 1.0, 0, 1]; %#ok<AGROW>
1045 function stages = synchCallStages(aidx)
1046 % Unrolls
the synchronous calls of an activity into blocking stages.
1047 stages =
struct(
'targetEidx', {},
'prob', {},
'isasync', {});
1048 if aidx > length(lsn.callsof) || isempty(lsn.callsof{aidx})
1051 for cidx = lsn.callsof{aidx}
1052 ctype = full(lsn.calltype(cidx));
1053 if ctype ~= CallType.SYNC && ctype ~= CallType.ASYNC
1056 isasync = (ctype == CallType.ASYNC);
1057 targetEidx = lsn.callpair(cidx, 2);
1059 if isfield(lsn,
'callproc') && ~isempty(lsn.callproc) && ...
1060 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx},
'Distribution')
1061 m = lsn.callproc{cidx}.getMean();
1063 nfull = floor(m + GlobalConstants.FineTol);
1065 if nfull > MAXCALLSTAGES
1066 line_warning(mfilename, sprintf(
'Call multiplicity %g on %s truncated to %d stages.', ...
1067 m, lsn.callnames{cidx}, MAXCALLSTAGES));
1068 nfull = MAXCALLSTAGES;
1072 stages(end+1) =
struct(
'targetEidx', targetEidx,
'prob', 1.0,
'isasync', isasync); %#ok<AGROW>
1074 if frac > GlobalConstants.FineTol
1075 stages(end+1) =
struct(
'targetEidx', targetEidx,
'prob', frac,
'isasync', isasync); %#ok<AGROW>
1080 function warnUnsupported(lsn)
1081 if any(full(lsn.calltype) == CallType.ASYNC)
1082 line_warning(mfilename, ['Asynchronous calls are represented by LQN2QN as ' ...
1083 'non-blocking
visits:
the caller releases its server but remains ' ...
1084 'serialised behind
the callee.']);
1086 if isfield(lsn, 'hasretrieval') && any(lsn.hasretrieval)
1087 line_warning(mfilename, 'Delayed-hit retrieval on
the cache miss path
is not represented by LQN2QN.');
1091 function tf = taskHasAndFork(tidx_)
1092 % True if any activity of
the task
is the head of an AND-fork
1093 % branch, i.e.
is marked POST_AND.
1095 if ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
1098 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
1099 if lsn.parent(a_) == tidx_ && a_ <= length(lsn.actposttype) && ...
1100 full(lsn.actposttype(a_)) == ActivityPrecedenceType.POST_AND
1107 function v = ifempty(x, d)