LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getStruct.m
1function lsn = getStruct(self, regenerate)
2% LSN = GETSTRUCT(SELF, regenerate)
3%
4%
5% Copyright 2012-2026, Imperial College London
6if nargin<2
7 regenerate = false;
8end
9if ~isempty(self.lsn) && ~regenerate
10 lsn = self.lsn;
11 return
12end
13lsn = LayeredNetworkStruct();
14lsn.nidx = 0; % total number of hosts, tasks, entries, and activities, except the reference tasks
15lsn.hshift = 0;
16lsn.nhosts = length(self.hosts);
17lsn.ntasks = length(self.tasks);
18lsn.nentries = length(self.entries);
19lsn.nacts = length(self.activities);
20lsn.tshift = lsn.nhosts;
21lsn.eshift = lsn.nhosts + lsn.ntasks;
22lsn.ashift = lsn.nhosts + lsn.ntasks + lsn.nentries;
23lsn.cshift = lsn.nhosts + lsn.ntasks + lsn.nentries + lsn.nacts;
24
25%% analyze static properties
26lsn.nidx = lsn.nhosts + lsn.ntasks + lsn.nentries + lsn.nacts;
27idx = 1;
28lsn.tasksof = cell(lsn.nhosts,1);
29lsn.entriesof = cell(lsn.nhosts+lsn.ntasks,1);
30lsn.actsof = cell(lsn.nhosts+lsn.ntasks+lsn.nentries,1);
31lsn.callsof = cell(lsn.nacts,1);
32lsn.hostdem = {};
33lsn.hostdem_type = zeros(lsn.nidx, 1);
34lsn.hostdem_params = cell(lsn.nidx, 1);
35lsn.hostdem_mean = nan(lsn.nidx, 1);
36lsn.hostdem_scv = nan(lsn.nidx, 1);
37lsn.hostdem_proc = cell(lsn.nidx, 1);
38
39lsn.actthink = {};
40lsn.actthink_type = zeros(lsn.nidx, 1);
41lsn.actthink_params = cell(lsn.nidx, 1);
42lsn.actthink_mean = nan(lsn.nidx, 1);
43lsn.actthink_scv = nan(lsn.nidx, 1);
44lsn.actthink_proc = cell(lsn.nidx, 1);
45
46lsn.think = {};
47lsn.think_type = zeros(lsn.nidx, 1);
48lsn.think_params = cell(lsn.nidx, 1);
49lsn.think_mean = nan(lsn.nidx, 1);
50lsn.think_scv = nan(lsn.nidx, 1);
51lsn.think_proc = cell(lsn.nidx, 1);
52lsn.sched = [];
53lsn.names = {};
54lsn.hashnames = {};
55%lsn.shortnames = {};
56lsn.mult = zeros(lsn.nhosts+lsn.ntasks,1);
57lsn.maxmult = zeros(lsn.nhosts+lsn.ntasks,1);
58lsn.repl = zeros(lsn.nhosts+lsn.ntasks,1);
59lsn.type = zeros(lsn.nidx,1);
60lsn.graph = zeros(lsn.nidx,lsn.nidx);
61loop_back_edges = false(lsn.nidx,lsn.nidx);
62loop_info_list = []; % Nx2 matrix: [loopstartaidx, loopendaidx] per row
63%lsn.replies = [];
64lsn.replygraph = false(lsn.nacts,lsn.nentries);
65lsn.actphase = ones(lsn.nacts,1); % Phase for each activity (default=1)
66
67lsn.nitems = zeros(lsn.nhosts+lsn.ntasks+lsn.nentries,1);
68lsn.itemcap = {};
69lsn.itemproc = {};
70lsn.itemproc_type = zeros(lsn.nidx, 1);
71lsn.itemproc_params = cell(lsn.nidx, 1);
72lsn.itemproc_mean = nan(lsn.nidx, 1);
73lsn.itemproc_scv = nan(lsn.nidx, 1);
74lsn.itemproc_proc = cell(lsn.nidx, 1);
75
76lsn.iscache = zeros(lsn.nhosts+lsn.ntasks,1);
77lsn.hasretrieval = zeros(lsn.nhosts+lsn.ntasks,1);
78lsn.setuptime = cell(lsn.nhosts+lsn.ntasks,1);
79lsn.setuptime_type = zeros(lsn.nidx, 1);
80lsn.setuptime_params = cell(lsn.nidx, 1);
81lsn.setuptime_mean = nan(lsn.nidx, 1);
82lsn.setuptime_scv = nan(lsn.nidx, 1);
83lsn.setuptime_proc = cell(lsn.nidx, 1);
84
85lsn.delayofftime = {};
86lsn.delayofftime_type = zeros(lsn.nidx, 1);
87lsn.delayofftime_params = cell(lsn.nidx, 1);
88lsn.delayofftime_mean = nan(lsn.nidx, 1);
89lsn.delayofftime_scv = nan(lsn.nidx, 1);
90lsn.delayofftime_proc = cell(lsn.nidx, 1);
91
92lsn.isfunction = zeros(lsn.nhosts+lsn.ntasks,1);
93
94% Open arrival distributions
95lsn.arrival = {};
96lsn.arrival_type = zeros(lsn.nidx, 1);
97lsn.arrival_params = cell(lsn.nidx, 1);
98lsn.arrival_mean = nan(lsn.nidx, 1);
99lsn.arrival_scv = nan(lsn.nidx, 1);
100lsn.arrival_proc = cell(lsn.nidx, 1);
101
102lsn.parent = [];
103for p=1:lsn.nhosts % for every processor, scheduling, multiplicity, replication, names, type
104 lsn.sched(idx,1) = SchedStrategy.fromText(self.hosts{p}.scheduling);
105 lsn.mult(idx,1) = self.hosts{p}.multiplicity;
106 lsn.repl(idx,1) = self.hosts{p}.replication;
107 lsn.names{idx,1} = self.hosts{p}.name;
108 lsn.hashnames{idx,1} = ['P:',lsn.names{idx,1}];
109 %lsn.shortnames{idx,1} = ['P',num2str(p)];
110 lsn.type(idx,1) = LayeredNetworkElement.HOST; % processor
111 idx = idx + 1;
112end
113
114for t=1:lsn.ntasks
115 lsn.sched(idx,1) = SchedStrategy.fromText(self.tasks{t}.scheduling);
116 % hostdem for tasks is Immediate
117 immDist = Immediate.getInstance();
118 lsn.hostdem{idx,1} = immDist;
119 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(immDist);
120 % think time
121 thinkDist = self.tasks{t}.thinkTime;
122 lsn.think{idx,1} = thinkDist;
123 [lsn.think_type(idx), lsn.think_params{idx}, lsn.think_mean(idx), lsn.think_scv(idx), lsn.think_proc{idx}] = extractDistParams(thinkDist);
124 lsn.mult(idx,1) = self.tasks{t}.multiplicity;
125 lsn.repl(idx,1) = self.tasks{t}.replication;
126 lsn.names{idx,1} = self.tasks{t}.name;
127 switch lsn.sched(idx,1)
128 case SchedStrategy.REF
129 lsn.hashnames{idx,1} = ['R:',lsn.names{idx,1}];
130 %lsn.shortnames{idx,1} = ['R',num2str(idx-tshift)];
131 otherwise
132 lsn.hashnames{idx,1} = ['T:',lsn.names{idx,1}];
133 %lsn.shortnames{idx,1} = ['T',num2str(idx-tshift)];
134 end
135 switch class(self.tasks{t})
136 case 'CacheTask'
137 lsn.nitems(idx,1) = self.tasks{t}.items;
138 lsn.itemcap{idx,1} = self.tasks{t}.itemLevelCap;
139 lsn.replacestrat(idx,1) = self.tasks{t}.replacestrategy;
140 lsn.hasretrieval(idx,1) = self.tasks{t}.retrieval;
141 lsn.hashnames{idx,1} = ['C:',lsn.names{idx,1}];
142 case 'FunctionTask'
143 setupDist = self.tasks{t}.SetupTime;
144 lsn.setuptime{idx,1} = setupDist;
145 [lsn.setuptime_type(idx), lsn.setuptime_params{idx}, lsn.setuptime_mean(idx), lsn.setuptime_scv(idx), lsn.setuptime_proc{idx}] = extractDistParams(setupDist);
146 delayoffDist = self.tasks{t}.DelayOffTime;
147 lsn.delayofftime{idx,1} = delayoffDist;
148 [lsn.delayofftime_type(idx), lsn.delayofftime_params{idx}, lsn.delayofftime_mean(idx), lsn.delayofftime_scv(idx), lsn.delayofftime_proc{idx}] = extractDistParams(delayoffDist);
149 lsn.hashnames{idx,1} = ['F:',lsn.names{idx,1}];
150 %lsn.shortnames{idx,1} = ['C',num2str(idx-tshift)];
151 end
152 pidx = find(cellfun(@(x) strcmp(x.name, self.tasks{t}.parent.name), self.hosts));
153 lsn.parent(idx) = pidx;
154 lsn.graph(idx, pidx) = 1;
155 lsn.type(idx) = LayeredNetworkElement.TASK; % task
156 idx = idx + 1;
157end
158
159% Adjust task replication to account for host processor replication.
160% In LQN, task repl >= host repl. If task repl is 1 (default), inherit host repl.
161for t=1:lsn.ntasks
162 tidx = lsn.tshift + t;
163 pidx = lsn.parent(tidx);
164 lsn.repl(tidx,1) = max(lsn.repl(tidx,1), lsn.repl(pidx,1));
165end
166
167for p=1:lsn.nhosts % for every processor
168 pidx = p;
169 lsn.tasksof{pidx} = find(lsn.parent == pidx);
170end
171
172for e=1:lsn.nentries
173 lsn.names{idx,1} = self.entries{e}.name;
174
175 % Extract open arrival distribution if present
176 if ~isempty(self.entries{e}.arrival) && isa(self.entries{e}.arrival, 'Distribution')
177 arrDist = self.entries{e}.arrival;
178 lsn.arrival{idx,1} = arrDist;
179 [lsn.arrival_type(idx), lsn.arrival_params{idx}, lsn.arrival_mean(idx), lsn.arrival_scv(idx), lsn.arrival_proc{idx}] = extractDistParams(arrDist);
180 end
181
182 switch class(self.entries{e})
183 case 'Entry'
184 lsn.hashnames{idx,1} = ['E:',lsn.names{idx,1}];
185 for a=1:length(self.entries{e}.replyActivity)
186 ractname = self.entries{e}.replyActivity{a};
187 ractidx = find(cellfun(@(x) strcmp(x.name, ractname), self.activities));
188 lsn.replygraph(ractidx,e)=true;
189 end
190 %lsn.shortnames{idx,1} = ['E',num2str(idx-eshift)];
191 case 'ItemEntry'
192 lsn.hashnames{idx,1} = ['I:',lsn.names{idx,1}];
193 %lsn.shortnames{idx,1} = ['I',num2str(idx-eshift)];
194 lsn.nitems(idx,1) = self.entries{e}.cardinality;
195 itemDist = self.entries{e}.popularity;
196 lsn.itemproc{idx,1} = itemDist;
197 [lsn.itemproc_type(idx), lsn.itemproc_params{idx}, lsn.itemproc_mean(idx), lsn.itemproc_scv(idx), lsn.itemproc_proc{idx}] = extractDistParams(itemDist);
198 end
199 % hostdem for entries is Immediate
200 immDist = Immediate.getInstance();
201 lsn.hostdem{idx,1} = immDist;
202 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(immDist);
203 tidx = lsn.nhosts + find(cellfun(@(x) strcmp(x.name, self.entries{e}.parent.name), self.tasks));
204 lsn.parent(idx) = tidx;
205 lsn.graph(tidx,idx) = 1;
206 lsn.entriesof{tidx}(end+1) = idx;
207 lsn.type(idx) = LayeredNetworkElement.ENTRY; % entries
208 idx = idx + 1;
209end
210
211for a=1:lsn.nacts
212 lsn.names{idx,1} = self.activities{a}.name;
213 lsn.hashnames{idx,1} = ['A:',lsn.names{idx,1}];
214 %lsn.shortnames{idx,1} = ['A',num2str(idx - lsn.ashift)];
215 % hostDemand for activities
216 hostdemDist = self.activities{a}.hostDemand;
217 lsn.hostdem{idx,1} = hostdemDist;
218 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(hostdemDist);
219 % thinkTime for activities
220 actthinkDist = self.activities{a}.thinkTime;
221 lsn.actthink{idx,1} = actthinkDist;
222 [lsn.actthink_type(idx), lsn.actthink_params{idx}, lsn.actthink_mean(idx), lsn.actthink_scv(idx), lsn.actthink_proc{idx}] = extractDistParams(actthinkDist);
223 tidx = lsn.nhosts + find(cellfun(@(x) strcmp(x.name, self.activities{a}.parent.name), self.tasks));
224 lsn.parent(idx) = tidx;
225 lsn.actsof{tidx}(end+1) = idx;
226 lsn.type(idx) = LayeredNetworkElement.ACTIVITY; % activities
227 lsn.actphase(a) = self.activities{a}.phase; % Store activity phase
228 idx = idx + 1;
229end
230
231nidx = idx - 1; % number of indices
232lsn.graph(nidx,nidx) = 0;
233
234tasks = self.tasks;
235%% now analyze calls
236cidx = 0;
237lsn.calltype = sparse([],lsn.nidx,1);
238lsn.iscaller = sparse(lsn.nidx,lsn.nidx);
239lsn.issynccaller = sparse(lsn.nidx,lsn.nidx);
240lsn.isasynccaller = sparse(lsn.nidx,lsn.nidx);
241lsn.callpair = [];
242lsn.callproc = {};
243lsn.callproc_type = [];
244lsn.callproc_params = {};
245lsn.callproc_mean = [];
246lsn.callproc_scv = [];
247lsn.callproc_proc = {};
248lsn.callnames = {};
249lsn.callhashnames = {};
250%lsn.callshortnames = {};
251lsn.taskgraph = sparse(lsn.tshift+lsn.ntasks, lsn.tshift+lsn.ntasks);
252lsn.actpretype = sparse(lsn.nidx,1);
253lsn.actposttype = sparse(lsn.nidx,1);
254% Quorum count of an AND-join, indexed by the join target activity. Equals the
255% number of predecessors when the join waits for all of them.
256lsn.actquorum = sparse(lsn.nidx,1);
257
258% Track boundToEntry mappings to validate uniqueness
259boundEntries = {};
260boundActivities = {};
261
262for t = 1:lsn.ntasks
263 tidx = lsn.tshift+t;
264 for a=1:length(self.tasks{t}.activities)
265 aidx = findstring(lsn.hashnames, ['A:',tasks{t}.activities(a).name]);
266 lsn.callsof{aidx} = [];
267 boundToEntry = tasks{t}.activities(a).boundToEntry;
268 %for b=1:length(boundToEntry)
269 eidx = findstring(lsn.hashnames, ['E:',boundToEntry]);
270 if eidx<0
271 eidx = findstring(lsn.hashnames, ['I:',boundToEntry]);
272 end
273 if eidx>0
274 lsn.graph(eidx, aidx) = 1;
275
276 % Check if this entry is already bound to another activity
277 activityName = tasks{t}.activities(a).name;
278 existingIdx = find(strcmp(boundEntries, boundToEntry), 1);
279 if ~isempty(existingIdx)
280 line_error(mfilename, sprintf('Multiple activities (%s, %s) are bound to the same entry: %s', ...
281 boundActivities{existingIdx}, activityName, boundToEntry));
282 else
283 boundEntries{end+1} = boundToEntry;
284 boundActivities{end+1} = activityName;
285 end
286 end
287 %end
288
289 for s=1:length(tasks{t}.activities(a).syncCallDests)
290 target_eidx = findstring(lsn.hashnames, ['E:',tasks{t}.activities(a).syncCallDests{s}]);
291 if target_eidx < 0
292 target_eidx = findstring(lsn.hashnames, ['I:',tasks{t}.activities(a).syncCallDests{s}]);
293 end
294 target_tidx = lsn.parent(target_eidx);
295 cidx = cidx + 1;
296 lsn.calltype(cidx,1) = CallType.SYNC;
297 lsn.callpair(cidx,1:2) = [aidx,target_eidx];
298 if tidx == target_tidx
299 line_error(mfilename, 'An entry on a task cannot call another entry on the same task.');
300 end
301 lsn.callnames{cidx,1} = [lsn.names{aidx},'=>',lsn.names{target_eidx}];
302 lsn.callhashnames{cidx,1} = [lsn.hashnames{aidx},'=>',lsn.hashnames{target_eidx}];
303 %lsn.callshortnames{cidx,1} = [lsn.shortnames{aidx},'=>',lsn.shortnames{target_eidx}];
304 callDist = Geometric(1/tasks{t}.activities(a).syncCallMeans(s)); % synch
305 lsn.callproc{cidx,1} = callDist;
306 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
307 lsn.callsof{aidx}(end+1) = cidx;
308 lsn.iscaller(tidx, target_tidx) = true;
309 lsn.iscaller(aidx, target_tidx) = true;
310 lsn.iscaller(tidx, target_eidx) = true;
311 lsn.iscaller(aidx, target_eidx) = true;
312 lsn.issynccaller(tidx, target_tidx) = true;
313 lsn.issynccaller(aidx, target_tidx) = true;
314 lsn.issynccaller(tidx, target_eidx) = true;
315 lsn.issynccaller(aidx, target_eidx) = true;
316 lsn.taskgraph(tidx, target_tidx) = 1;
317 lsn.graph(aidx, target_eidx) = 1;
318 end
319
320 for s=1:length(tasks{t}.activities(a).asyncCallDests)
321 target_entry_name = tasks{t}.activities(a).asyncCallDests{s};
322 target_eidx = findstring(lsn.hashnames,['E:',target_entry_name]);
323 if target_eidx < 0
324 target_eidx = findstring(lsn.hashnames, ['I:',target_entry_name]);
325 end
326 % Validate that the target entry exists
327 if target_eidx <= 0
328 line_error(mfilename, sprintf('Activity "%s" has an async call to non-existent entry "%s".', tasks{t}.activities(a).name, target_entry_name));
329 end
330 target_tidx = lsn.parent(target_eidx);
331 % Check for self-referential async calls (task calling itself)
332 if tidx == target_tidx
333 line_error(mfilename, sprintf('Activity "%s" in task "%s" has an async call to an entry on the same task. Async self-calls are not supported.', tasks{t}.activities(a).name, tasks{t}.name));
334 end
335 cidx = cidx + 1;
336 lsn.callpair(cidx,1:2) = [aidx,target_eidx];
337 lsn.calltype(cidx,1) = CallType.ASYNC; % async
338 lsn.callnames{cidx,1} = [lsn.names{aidx},'->',lsn.names{target_eidx}];
339 lsn.callhashnames{cidx,1} = [lsn.hashnames{aidx},'->',lsn.hashnames{target_eidx}];
340 %lsn.callshortnames{cidx,1} = [lsn.shortnames{aidx},'->',lsn.shortnames{target_eidx}];
341 callDist = Geometric(1/tasks{t}.activities(a).asyncCallMeans(s)); % asynch
342 lsn.callproc{cidx,1} = callDist;
343 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
344 lsn.callsof{aidx}(end+1) = cidx;
345 lsn.iscaller(aidx, target_tidx) = true;
346 lsn.iscaller(aidx, target_eidx) = true;
347 lsn.iscaller(tidx, target_tidx) = true;
348 lsn.iscaller(tidx, target_eidx) = true;
349 lsn.isasynccaller(tidx, target_tidx) = true;
350 lsn.isasynccaller(tidx, target_eidx) = true;
351 lsn.isasynccaller(aidx, target_tidx) = true;
352 lsn.isasynccaller(aidx, target_eidx) = true;
353 lsn.taskgraph(tidx, target_tidx) = 1;
354 lsn.graph(aidx, target_eidx) = 1;
355 end
356 end
357
358 for ap=1:length(tasks{t}.precedences)
359 pretype = tasks{t}.precedences(ap).preType;
360 posttype = tasks{t}.precedences(ap).postType;
361 preacts = tasks{t}.precedences(ap).preActs;
362 postacts = tasks{t}.precedences(ap).postActs;
363
364 % Validate PRE_AND activities exist before processing
365 if pretype == ActivityPrecedenceType.PRE_AND
366 if isempty(preacts)
367 line_error(mfilename, sprintf('PRE_AND precedence in task "%s" has no pre activities.', tasks{t}.name));
368 end
369 for prea = 1:length(preacts)
370 preaidx = findstring(lsn.hashnames, ['A:',preacts{prea}]);
371 if preaidx <= 0
372 line_error(mfilename, sprintf('PRE_AND precedence references non-existent activity "%s" in task "%s".', preacts{prea}, tasks{t}.name));
373 end
374 if preaidx > 0 && lsn.parent(preaidx) ~= tidx
375 line_error(mfilename, sprintf('PRE_AND precedence in task "%s" references activity "%s" from a different task.', tasks{t}.name, preacts{prea}));
376 end
377 end
378 end
379
380 % Validate POST_AND activities exist before processing
381 if posttype == ActivityPrecedenceType.POST_AND
382 if isempty(postacts)
383 line_error(mfilename, sprintf('POST_AND precedence in task "%s" has no post activities.', tasks{t}.name));
384 end
385 for posta = 1:length(postacts)
386 postaidx = findstring(lsn.hashnames, ['A:',postacts{posta}]);
387 if postaidx <= 0
388 line_error(mfilename, sprintf('POST_AND precedence references non-existent activity "%s" in task "%s".', postacts{posta}, tasks{t}.name));
389 end
390 if postaidx > 0 && lsn.parent(postaidx) ~= tidx
391 line_error(mfilename, sprintf('POST_AND precedence in task "%s" references activity "%s" from a different task.', tasks{t}.name, postacts{posta}));
392 end
393 end
394 end
395
396 % An AND-join carries a quorum count k in preParams: the join fires once k of its
397 % predecessors complete. It is recorded on the join target in actquorum rather than
398 % folded into the graph edge weights, which must stay 1 for an AND-join so they are
399 % not mistaken for routing probabilities.
400 quorumCount = 0;
401 if pretype == ActivityPrecedenceType.PRE_AND
402 quorum = tasks{t}.precedences(ap).preParams;
403 if isempty(quorum) || numel(quorum) ~= 1 || quorum < 1 || quorum > length(preacts)
404 quorumCount = length(preacts);
405 else
406 quorumCount = round(quorum);
407 end
408 end
409
410 for prea = 1:length(preacts)
411 preaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).preActs{prea}]);
412 preParam = 1.0;
413 switch posttype
414 case ActivityPrecedenceType.POST_OR
415 for posta = 1:length(postacts)
416 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
417 probs = tasks{t}.precedences(ap).postParams;
418 postParam = probs(posta);
419 lsn.graph(preaidx, postaidx) = preParam * postParam;
420 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
421 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
422 end
423 case ActivityPrecedenceType.POST_AND
424 for posta = 1:length(postacts)
425 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
426 lsn.graph(preaidx, postaidx) = 1;
427 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
428 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
429 end
430 case ActivityPrecedenceType.POST_LOOP
431 counts = tasks{t}.precedences(ap).postParams;
432 % add the end activity
433 enda = length(postacts);
434 loopentryaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).preActs{1}]);
435 loopstartaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{1}]);
436 loopendaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{enda}]);
437
438 if counts < 1
439 % When expected iterations < 1, we may skip loop entirely
440 % E[iterations] = counts means P(enter loop) = counts
441 lsn.graph(loopentryaidx, loopstartaidx) = counts;
442 lsn.graph(loopentryaidx, loopendaidx) = 1.0 - counts;
443 % Process activities inside the loop as serial chain
444 curaidx = loopstartaidx;
445 for posta = 2:(length(postacts)-1)
446 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
447 lsn.graph(curaidx, postaidx) = 1.0;
448 lsn.actposttype(postaidx) = sparse((tasks{t}.precedences(ap).postType));
449 curaidx = postaidx;
450 end
451 % After loop body, always exit to end (no looping back)
452 lsn.graph(curaidx, loopendaidx) = 1.0;
453 lsn.actposttype(loopstartaidx) = sparse((tasks{t}.precedences(ap).postType));
454 else
455 % When expected iterations >= 1, always enter loop
456 % E[iterations] = 1/(1-p) = counts => p = 1 - 1/counts
457 curaidx = loopentryaidx;
458 for posta = 1:(length(postacts)-1) % last one is 'end' of loop activity
459 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
460 lsn.graph(curaidx, postaidx) = 1.0;
461 lsn.actposttype(postaidx) = sparse((tasks{t}.precedences(ap).postType));
462 curaidx = postaidx;
463 end
464 loop_back_edges(curaidx, loopstartaidx) = true;
465 lsn.graph(curaidx, loopstartaidx) = 1.0 - 1.0 / counts;
466 lsn.graph(curaidx, loopendaidx) = 1.0 / counts;
467 loop_info_list(end+1,:) = [loopstartaidx, loopendaidx];
468 end
469 lsn.actposttype(loopendaidx) = sparse((tasks{t}.precedences(ap).postType));
470 otherwise
471 for posta = 1:length(postacts)
472 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
473 postParam = 1.0;
474 lsn.graph(preaidx, postaidx) = preParam * postParam;
475 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
476 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
477 if quorumCount > 0
478 lsn.actquorum(postaidx) = quorumCount;
479 end
480 end
481 end
482 end
483 end
484end
485
486%% Process forwarding calls from entries
487for e = 1:length(self.entries)
488 entry = self.entries{e};
489 eidx = findstring(lsn.hashnames, ['E:', entry.name]);
490 if eidx <= 0
491 eidx = findstring(lsn.hashnames, ['I:', entry.name]);
492 end
493 if eidx <= 0
494 continue;
495 end
496 source_tidx = lsn.parent(eidx);
497
498 for fw = 1:length(entry.forwardingDests)
499 target_entry_name = entry.forwardingDests{fw};
500 target_eidx = findstring(lsn.hashnames, ['E:', target_entry_name]);
501 if target_eidx <= 0
502 target_eidx = findstring(lsn.hashnames, ['I:', target_entry_name]);
503 end
504 if target_eidx <= 0
505 line_error(mfilename, sprintf('Entry "%s" forwards to non-existent entry "%s".', entry.name, target_entry_name));
506 end
507 target_tidx = lsn.parent(target_eidx);
508
509 % Validate: cannot forward to same task
510 if source_tidx == target_tidx
511 line_error(mfilename, sprintf('Entry "%s" cannot forward to entry "%s" on the same task.', entry.name, target_entry_name));
512 end
513
514 cidx = cidx + 1;
515 lsn.calltype(cidx, 1) = CallType.FWD;
516 lsn.callpair(cidx, 1:2) = [eidx, target_eidx];
517 lsn.callnames{cidx, 1} = [lsn.names{eidx}, '~>', lsn.names{target_eidx}];
518 lsn.callhashnames{cidx, 1} = [lsn.hashnames{eidx}, '~>', lsn.hashnames{target_eidx}];
519
520 % Forwarding probability (stored as mean calls)
521 fwdProb = entry.forwardingProbs(fw);
522 callDist = Geometric(1.0 / fwdProb);
523 lsn.callproc{cidx, 1} = callDist;
524 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
525
526 % Update task graph to reflect forwarding relationship
527 lsn.taskgraph(source_tidx, target_tidx) = 1;
528 lsn.graph(eidx, target_eidx) = 1;
529
530 % NOTE: We do NOT set issynccaller for forwarding calls because:
531 % - Forwarding is NOT a sync call - the forwarder does NOT wait for the target
532 % - The forwarder sends the request to the target and the target replies
533 % directly to the original caller (not back to the forwarder)
534 % - The forwarding target's workload comes from the forwarding probability,
535 % not from a separate closed class in the queueing network decomposition
536 %
537 % Setting issynccaller here was causing incorrect throughput calculations
538 % because it created separate closed classes for forwarding targets.
539 end
540end
541
542% Check for entries without boundTo activities
543unbound = find(all(~lsn.graph(lsn.eshift+1 : lsn.eshift+lsn.nentries, ...
544 lsn.ashift+1 : lsn.ashift+lsn.nacts), 2)); %#ok<EFIND>
545if ~isempty(unbound)
546 line_error(mfilename, 'An entry does not have any boundTo activity.');
547end
548
549%lsn.replies = false(1,lsn.nacts);
550%lsn.replygraph = 0*lsn.graph;
551% Snapshot which entries have explicit replies (from repliesTo calls)
552% before adding implicit ones. This way OrFork branches all get marked.
553hasExplicitReply = any(lsn.replygraph, 1);
554for t = 1:lsn.ntasks
555 tidx = lsn.tshift+t;
556 for aidx = lsn.actsof{tidx}
557 postaidxs = find(lsn.graph(aidx, :));
558 isreply = true;
559 % if no successor is an action of tidx
560 for postaidx = postaidxs
561 if any(lsn.actsof{tidx} == postaidx)
562 isreply = false;
563 end
564 end
565 if isreply
566 % this is a leaf node, search backward for the parent entry,
567 % which is assumed to be unique
568 %lsn.replies(aidx-lsn.nacts) = true;
569 parentidx = aidx;
570 while lsn.type(parentidx) ~= LayeredNetworkElement.ENTRY
571 ancestors = find(lsn.graph(:,parentidx));
572 parentidx = at(ancestors,1); % only choose first ancestor
573 end
574 if lsn.type(parentidx) == LayeredNetworkElement.ENTRY
575 eidx = parentidx - lsn.eshift;
576 % Only add implicit reply if no explicit reply exists for this entry
577 % This supports Phase-2 activities (activities after an explicit reply)
578 if ~hasExplicitReply(eidx)
579 lsn.replygraph(aidx-lsn.ashift, eidx) = true;
580 end
581 end
582 end
583 end
584end
585lsn.ncalls = size(lsn.calltype,1);
586
587% correct multiplicity for infinite server stations
588for tidx = find(lsn.sched == SchedStrategy.INF)' % transpose to iterate over each element
589 if lsn.type(tidx) == LayeredNetworkElement.TASK
590 callers = find(lsn.taskgraph(:, tidx));
591 callers_inf = strcmp(lsn.mult(callers), SchedStrategy.INF);
592 if any(callers_inf)
593 % if a caller is also inf, then we would need to recursively
594 % determine the maximum multiplicity, we instead use a
595 % heuristic value
596 lsn.mult(tidx) = sum(lsn.mult(~callers_inf)) + sum(callers_inf)*max(lsn.mult);
597 else
598 lsn.mult(tidx) = sum(lsn.mult(callers));
599 end
600 end
601end
602
603lsn.refset = zeros(lsn.nidx,1);
604[conncomps, roots]=graph_connected_components(lsn.taskgraph(lsn.nhosts+1:end, lsn.nhosts+1:end));
605lsn.conntasks = conncomps;
606for r=1:length(roots)
607 lsn.conntasks(find(lsn.conntasks == r)) = lsn.tshift+roots(r);
608end
609
610lsn.isref = lsn.sched == SchedStrategy.REF;
611lsn.iscache(1:(lsn.tshift+lsn.ntasks)) = lsn.nitems(1:(lsn.tshift+lsn.ntasks))>0;
612lsn.isfunction(1:(lsn.tshift+lsn.ntasks)) = ~cellfun(@isempty, lsn.setuptime(1:(lsn.tshift+lsn.ntasks)));
613
614% Build fan-out matrix from Task objects' fanOutDest/fanOutValue
615% fanout(source_task_idx, dest_task_idx) = fan-out value (0 means not set)
616lsn.fanout = zeros(lsn.nidx, lsn.nidx);
617taskNameToIdx = containers.Map();
618for t = 1:lsn.ntasks
619 tidx = lsn.tshift + t;
620 taskNameToIdx(self.tasks{t}.name) = tidx;
621end
622for t = 1:lsn.ntasks
623 tidx = lsn.tshift + t;
624 task = self.tasks{t};
625 for f = 1:length(task.fanOutDest)
626 destName = task.fanOutDest{f};
627 if taskNameToIdx.isKey(destName)
628 destIdx = taskNameToIdx(destName);
629 lsn.fanout(tidx, destIdx) = task.fanOutValue(f);
630 end
631 end
632end
633
634% the dag differs from the graph:
635% - dag swaps the direction of entry-task edges
636% - dag removes loop edges
637dag = lsn.graph;
638n = size(dag, 1);
639% Reverse edges from TASK to ENTRY if not a reference
640for i = 1:n
641 if lsn.type(i) == LayeredNetworkElement.TASK && ~lsn.isref(i)
642 for j = 1:n
643 if lsn.type(j) == LayeredNetworkElement.ENTRY && dag(i,j)
644 dag(i,j) = 0;
645 dag(j,i) = 1;
646 end
647 end
648 end
649end
650
651% Compute entry-to-activity reachability within the same task
652for eoff = 1:lsn.nentries
653 eidx = lsn.eshift + eoff; % global entry index
654 tidx = lsn.parent(eidx); % parent task index
655 visited = false(1,nidx); % global visit mask
656 stack = eidx;
657 visited(eidx) = true;
658 while ~isempty(stack)
659 v = stack(end);
660 stack(end) = [];
661 nbrs = find(lsn.graph(v,:));
662 nbrs = nbrs(~visited(nbrs));
663 visited(nbrs) = true;
664 stack = [stack nbrs]; %#ok<AGROW>
665 end
666 acts = find(visited & ...
667 lsn.type' == LayeredNetworkElement.ACTIVITY & ...
668 lsn.parent == tidx);
669 lsn.actsof{lsn.eshift+eoff} = acts;
670end
671
672%% check for errors
673dag(loop_back_edges(:)) = 0;
674lsn.dag = dag;
675% Compute bounds on multiplicies for host processors and non-ref tasks
676if is_dag(dag)
677 lsn.maxmult = lsn_max_multiplicity(lsn);
678 lsn.maxmult = lsn.maxmult(1:(lsn.tshift+lsn.ntasks));
679else
680 line_error(mfilename, 'A cycle exists in an activity graph.');
681end
682% Check for non-terminal reply activities
683% An activity that replies to an entry should not have Phase 1 successor activities
684% Phase 2 successors are allowed (post-reply processing)
685for a = 1:lsn.nacts
686 if any(lsn.replygraph(a, :)) % activity 'a' replies to some entry
687 aidx = lsn.ashift + a; % global activity index
688 successors = find(lsn.graph(aidx, :));
689 for succ = successors
690 if succ > lsn.eshift + lsn.nentries % successor is an activity
691 succ_act_idx = succ - lsn.ashift; % convert to activity array index
692 if lsn.actphase(succ_act_idx) == 1
693 line_error(mfilename, 'Unsupported replyTo in non-terminal activity.');
694 end
695 end
696 end
697 end
698end
699
700% Check calls: an entry must not be called both synchronously and
701% asynchronously. FWD calls are skipped (as in JLINE): forwarding into an
702% entry that also has direct sync callers is legal (lqns interlock-forwarded
703% pattern, e.g. 18-interlock).
704if ~isempty(lsn.callpair)
705 target_eidxs = unique(lsn.callpair(:,2));
706 for eidx=target_eidxs(:)'
707 call_types_to_eidx = lsn.calltype(find(lsn.callpair(:,2) == eidx),1);
708 call_types_to_eidx(call_types_to_eidx == CallType.FWD) = [];
709 if ~isempty(call_types_to_eidx) && ~all(call_types_to_eidx == call_types_to_eidx(1))
710 line_error(mfilename, 'An entry is called both synchronously and asynchronously.');
711 end
712 end
713end
714
715self.lsn = lsn;
716end
717
718function [dtype, params, mean_val, scv_val, proc] = extractDistParams(dist)
719% EXTRACTDISTPARAMS Extract primitive parameters from a Distribution object
720%
721% Returns:
722% dtype - ProcessType enum value
723% params - Vector/cell of primitive parameters
724% mean_val - Precomputed mean (NaN if unavailable)
725% scv_val - Precomputed SCV (NaN if unavailable)
726% proc - Process representation {D0, D1} or [] for non-Markovian
727
728if isempty(dist)
729 dtype = ProcessType.DISABLED;
730 params = [];
731 mean_val = NaN;
732 scv_val = NaN;
733 proc = {};
734 return
735end
736
737% Get mean and SCV
738try
739 mean_val = dist.getMean();
740catch
741 mean_val = NaN;
742end
743
744try
745 scv_val = dist.getSCV();
746catch
747 scv_val = NaN;
748end
749
750% Get process representation if available
751try
752 proc = dist.getRepres();
753 if isempty(proc)
754 proc = {};
755 end
756catch
757 proc = {};
758end
759
760% Extract parameters based on distribution type
761distClass = class(dist);
762switch distClass
763 case 'Disabled'
764 dtype = ProcessType.DISABLED;
765 params = [];
766
767 case 'Immediate'
768 dtype = ProcessType.IMMEDIATE;
769 params = [];
770 mean_val = 0;
771 scv_val = 0;
772
773 case 'Exp'
774 dtype = ProcessType.EXP;
775 params = dist.getParam(1).paramValue; % lambda
776
777 case 'Erlang'
778 dtype = ProcessType.ERLANG;
779 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [k; mu]
780
781 case 'HyperExp'
782 dtype = ProcessType.HYPEREXP;
783 % p1, lambda1, lambda2
784 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue; dist.getParam(3).paramValue];
785
786 case 'Coxian'
787 dtype = ProcessType.COXIAN;
788 mu = dist.getParam(1).paramValue;
789 phi = dist.getParam(2).paramValue;
790 params = [length(mu); mu(:); phi(:)];
791
792 case 'Cox2'
793 dtype = ProcessType.COX2;
794 mu1 = dist.getParam(1).paramValue;
795 mu2 = dist.getParam(2).paramValue;
796 phi = dist.getParam(3).paramValue;
797 params = [mu1; mu2; phi];
798
799 case 'APH'
800 dtype = ProcessType.APH;
801 alpha = dist.getParam(1).paramValue;
802 T = dist.getParam(2).paramValue;
803 n = length(alpha);
804 params = [n; alpha(:); T(:)];
805
806 case 'PH'
807 dtype = ProcessType.PH;
808 alpha = dist.getParam(1).paramValue;
809 T = dist.getParam(2).paramValue;
810 n = length(alpha);
811 params = [n; alpha(:); T(:)];
812
813 case 'Det'
814 dtype = ProcessType.DET;
815 params = dist.getParam(1).paramValue;
816
817 case 'Uniform'
818 dtype = ProcessType.UNIFORM;
819 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [a; b]
820
821 case 'Gamma'
822 dtype = ProcessType.GAMMA;
823 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
824
825 case 'Pareto'
826 dtype = ProcessType.PARETO;
827 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
828
829 case 'Weibull'
830 dtype = ProcessType.WEIBULL;
831 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
832
833 case 'Lognormal'
834 dtype = ProcessType.LOGNORMAL;
835 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [mu; sigma]
836
837 case 'MAP'
838 dtype = ProcessType.MAP;
839 D0 = dist.getParam(1).paramValue;
840 D1 = dist.getParam(2).paramValue;
841 n = size(D0,1);
842 params = [n; D0(:); D1(:)];
843
844 case 'MMPP2'
845 dtype = ProcessType.MMPP2;
846 lambda0 = dist.getParam(1).paramValue;
847 lambda1 = dist.getParam(2).paramValue;
848 sigma0 = dist.getParam(3).paramValue;
849 sigma1 = dist.getParam(4).paramValue;
850 params = [lambda0; lambda1; sigma0; sigma1];
851
852 case 'Geometric'
853 dtype = ProcessType.GEOMETRIC;
854 params = dist.getParam(1).paramValue; % p (success probability)
855
856 case 'Poisson'
857 dtype = ProcessType.POISSON;
858 params = dist.getParam(1).paramValue; % lambda
859
860 case 'Binomial'
861 dtype = ProcessType.BINOMIAL;
862 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [n; p]
863
864 case 'Bernoulli'
865 dtype = ProcessType.BERNOULLI;
866 params = dist.getParam(1).paramValue; % p
867
868 case {'Replayer', 'Trace'}
869 dtype = ProcessType.REPLAYER;
870 params = []; % Trace data not stored as primitive
871
872 case 'DiscreteSampler'
873 dtype = ProcessType.GEOMETRIC; % Approximation
874 params = [];
875
876 otherwise
877 % Fallback for unhandled types
878 dtype = ProcessType.DISABLED;
879 params = [];
880 proc = {};
881end
882end
Definition Station.m:245