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