LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
updateMetricsDefault.m
1function updateMetricsDefault(self, it)
2ensemble = self.ensemble;
3lqn = self.lqn;
4
5% obtain the activity service times
6self.servt = zeros(lqn.nidx,1);
7self.residt = zeros(lqn.nidx,1);
8for r=1:size(self.servt_classes_updmap,1)
9 idx = self.servt_classes_updmap(r,1);
10 aidx = self.servt_classes_updmap(r,2);
11 nodeidx = self.servt_classes_updmap(r,3);
12 classidx = self.servt_classes_updmap(r,4);
13
14 % store the residence times and tput at this layer to become
15 % the servt / tputs of aidx in another layer, as needed
16 iter_min = min(30,ceil(self.options.iter_max/4));
17 wnd_size = (it-self.averagingstart+1);
18
19 % Compute residt from QN/TN_ref instead of WN to avoid
20 % fork+loop visit distortion (WN uses visits from DTMC solve
21 % which are distorted when Fork non-stochastic rows coexist
22 % with loop back-edges in the routing matrix)
23 layerIdx = self.idxhash(idx);
24 layerSn = ensemble{layerIdx}.getStruct();
25 c = find(layerSn.chains(:, classidx), 1);
26 refclass_c = layerSn.refclass(c);
27 refstat_k = layerSn.refstat(classidx);
28
29 if ~isempty(self.averagingstart) && it>=iter_min % assume steady-state
30 self.servt(aidx) = 0;
31 self.residt(aidx) = 0;
32 self.tput(aidx) = 0;
33 for w=1:(wnd_size-1)
34 self.servt(aidx) = self.servt(aidx) + self.results{end-w,layerIdx}.RN(nodeidx,classidx) / wnd_size;
35 TN_ref = self.results{end-w,layerIdx}.TN(refstat_k, refclass_c);
36 if TN_ref > GlobalConstants.FineTol
37 self.residt(aidx) = self.residt(aidx) + self.results{end-w,layerIdx}.QN(nodeidx,classidx) / TN_ref / wnd_size;
38 else
39 self.residt(aidx) = self.residt(aidx) + self.results{end-w,layerIdx}.WN(nodeidx,classidx) / wnd_size;
40 end
41 self.tput(aidx) = self.tput(aidx) + self.results{end-w,layerIdx}.TN(nodeidx,classidx) / wnd_size;
42 end
43 else
44 self.servt(aidx) = self.results{end,layerIdx}.RN(nodeidx,classidx);
45 TN_ref = self.results{end,layerIdx}.TN(refstat_k, refclass_c);
46 if TN_ref > GlobalConstants.FineTol
47 self.residt(aidx) = self.results{end,layerIdx}.QN(nodeidx,classidx) / TN_ref;
48 else
49 self.residt(aidx) = self.results{end,layerIdx}.WN(nodeidx,classidx);
50 end
51 self.tput(aidx) = self.results{end,layerIdx}.TN(nodeidx,classidx);
52 end
53
54 % Fix for async-only entry targets: use RN (response time per visit) for residt
55 % The host layer closed model incorrectly splits residence time (WN) between
56 % activities when an entry only receives async calls (no sync callers).
57 % For async-only entries, use RN instead of WN since the async arrivals
58 % don't share the closed chain's visit ratio - each async arrival gets
59 % the full response time per visit.
60 if aidx > lqn.ashift && aidx <= lqn.ashift + lqn.nacts
61 % This is an activity - find its bound entry
62 for eidx = (lqn.eshift+1):(lqn.eshift+lqn.nentries)
63 if full(lqn.graph(eidx, aidx)) > 0
64 % Found bound entry - check if async-only
65 hasSyncCallers = full(any(lqn.issynccaller(:, eidx)));
66 hasAsyncCallers = full(any(lqn.isasynccaller(:, eidx)));
67 if hasAsyncCallers && ~hasSyncCallers
68 % Async-only target: use RN (response time per visit)
69 % instead of WN (residence time with visit ratio)
70 self.residt(aidx) = self.servt(aidx); % servt already has RN
71 end
72 break;
73 end
74 end
75 end
76
77 % Apply under-relaxation if enabled and not first iteration
78 omega = self.relax_omega;
79 if omega < 1.0 && it > 1
80 if ~isnan(self.servt_prev(aidx))
81 self.servt(aidx) = omega * self.servt(aidx) + (1 - omega) * self.servt_prev(aidx);
82 end
83 if ~isnan(self.residt_prev(aidx))
84 self.residt(aidx) = omega * self.residt(aidx) + (1 - omega) * self.residt_prev(aidx);
85 end
86 if ~isnan(self.tput_prev(aidx))
87 self.tput(aidx) = omega * self.tput(aidx) + (1 - omega) * self.tput_prev(aidx);
88 end
89 end
90 % Store current values for next iteration
91 self.servt_prev(aidx) = self.servt(aidx);
92 self.residt_prev(aidx) = self.residt(aidx);
93 self.tput_prev(aidx) = self.tput(aidx);
94
95 % Safeguard against MVA numerical instability producing extreme values
96 % (matches Python _update_metrics_default max_servt guard)
97 max_servt = 1e10;
98 if self.servt(aidx) > 0 && self.servt(aidx) <= max_servt
99 self.servtproc{aidx} = Exp.fitMean(self.servt(aidx));
100 end
101 self.tputproc{aidx} = Exp.fitRate(self.tput(aidx));
102end
103
104% Phase-2 support: split activity service times by phase
105% Note: Overtaking probability is computed later after entry throughput is available
106if self.hasPhase2
107 % Reset phase-specific arrays
108 self.servt_ph1 = zeros(lqn.nidx, 1);
109 self.servt_ph2 = zeros(lqn.nidx, 1);
110
111 % Split activity service times by phase
112 for a = 1:lqn.nacts
113 aidx = lqn.ashift + a;
114 if lqn.actphase(a) == 1
115 self.servt_ph1(aidx) = self.servt(aidx);
116 else
117 self.servt_ph2(aidx) = self.servt(aidx);
118 end
119 end
120
121 % Aggregate phase service times to entry level
122 for e = 1:lqn.nentries
123 eidx = lqn.eshift + e;
124 acts = lqn.actsof{eidx};
125 for aidx = acts
126 a = aidx - lqn.ashift;
127 if a > 0 && a <= lqn.nacts
128 if lqn.actphase(a) == 1
129 self.servt_ph1(eidx) = self.servt_ph1(eidx) + self.servt_ph1(aidx);
130 else
131 self.servt_ph2(eidx) = self.servt_ph2(eidx) + self.servt_ph2(aidx);
132 end
133 end
134 end
135 end
136end
137
138% obtain throughput for activities in thinkt_classes_updmap (needed for async calls)
139% this ensures tputproc is set for activities that make async calls from client nodes
140for r=1:size(self.thinkt_classes_updmap,1)
141 idx = self.thinkt_classes_updmap(r,1);
142 aidx = self.thinkt_classes_updmap(r,2);
143 nodeidx = self.thinkt_classes_updmap(r,3);
144 classidx = self.thinkt_classes_updmap(r,4);
145
146 % only update if not already set by servt_classes_updmap processing
147 if isempty(self.tputproc) || length(self.tputproc) < aidx || isempty(self.tputproc{aidx})
148 iter_min = min(30,ceil(self.options.iter_max/4));
149 wnd_size = (it-self.averagingstart+1);
150 if ~isempty(self.averagingstart) && it>=iter_min % assume steady-state
151 self.tput(aidx) = 0;
152 for w=1:(wnd_size-1)
153 self.tput(aidx) = self.tput(aidx) + self.results{end-w,self.idxhash(idx)}.TN(nodeidx,classidx) / wnd_size;
154 end
155 else
156 self.tput(aidx) = self.results{end,self.idxhash(idx)}.TN(nodeidx,classidx);
157 end
158 self.tputproc{aidx} = Exp.fitRate(self.tput(aidx));
159 end
160end
161
162% Obtain the join times for AND-Join activities
163% For each AND-join activity, compute the synchronization delay as
164% the maximum of predecessor branch service times.
165self.joint = zeros(lqn.nidx,1);
166joinedacts = find(lqn.actpretype == ActivityPrecedenceType.PRE_AND)';
167for a = joinedacts
168 aidx = lqn.ashift + a;
169 % Find predecessor activities in the activity graph
170 preds = find(lqn.graph(:, aidx) > 0)';
171 pred_servts = [];
172 for pidx = preds
173 if pidx > lqn.ashift && pidx <= lqn.ashift + lqn.nacts
174 pred_servts(end+1) = self.servt(pidx); %#ok<AGROW>
175 end
176 end
177 if ~isempty(pred_servts)
178 % Join time = maximum of predecessor branch service times
179 % This represents the synchronization delay at the AND-join point.
180 % For exponentially distributed branch times with means t_1,...,t_k,
181 % approximate E[max] using the Clark (1961) two-moment method
182 % recursively for k branches.
183 if length(pred_servts) == 1
184 self.joint(aidx) = pred_servts(1);
185 else
186 % Recursive two-moment approximation for max of exponentials
187 % Start with first two branches, then fold in remaining
188 mu_max = pred_servts(1);
189 for bi = 2:length(pred_servts)
190 t1 = mu_max;
191 t2 = pred_servts(bi);
192 if t1 > 0 && t2 > 0
193 % For exponentials: E[max(X1,X2)] = E[X1] + E[X2] - E[min(X1,X2)]
194 % E[min(X1,X2)] = 1/(1/t1 + 1/t2) = t1*t2/(t1+t2)
195 mu_max = t1 + t2 - t1*t2/(t1+t2);
196 else
197 mu_max = max(t1, t2);
198 end
199 end
200 self.joint(aidx) = mu_max;
201 end
202 end
203end
204
205% obtain the call residence time
206self.callservt = zeros(lqn.ncalls,1);
207self.callresidt = zeros(lqn.ncalls,1);
208for r=1:size(self.call_classes_updmap,1)
209 idx = self.call_classes_updmap(r,1);
210 cidx = self.call_classes_updmap(r,2);
211 nodeidx = self.call_classes_updmap(r,3);
212 classidx = self.call_classes_updmap(r,4);
213 if self.call_classes_updmap(r,3) > 1
214 if nodeidx == 1
215 self.callservt(cidx) = 0;
216 else
217 self.callservt(cidx) = self.results{end, self.idxhash(idx)}.RN(nodeidx,classidx) * self.lqn.callproc{cidx}.getMean;
218 self.callresidt(cidx) = self.results{end, self.idxhash(idx)}.WN(nodeidx,classidx);
219 end
220 % Growth rate capping removed - it prevents callservt from converging
221 % to the correct value when initial values are near-zero (Immediate)
222 % Apply under-relaxation to call service times
223 omega = self.relax_omega;
224 if omega < 1.0 && it > 1 && ~isnan(self.callservt_prev(cidx))
225 self.callservt(cidx) = omega * self.callservt(cidx) + (1 - omega) * self.callservt_prev(cidx);
226 end
227 self.callservt_prev(cidx) = self.callservt(cidx);
228 self.callresidt_prev(cidx) = self.callresidt(cidx);
229 end
230end
231
232% then resolve the entry servt summing up these contributions
233entry_servt = self.servtmatrix*[self.residt;self.callresidt(:)];
234entry_servt(1:lqn.eshift) = 0;
235
236% Propagate forwarding calls: add target entry's service time to source entry
237% When e0 forwards to e1 with probability p, callers of e0 see:
238% e0's service + p * e1's service
239% Process in topological order to handle forwarding chains correctly
240for cidx = 1:lqn.ncalls
241 if lqn.calltype(cidx) == CallType.FWD
242 source_eidx = lqn.callpair(cidx, 1);
243 target_eidx = lqn.callpair(cidx, 2);
244 fwd_prob = lqn.callproc{cidx}.getMean();
245
246 % Get target entry's service time
247 target_servt = entry_servt(target_eidx);
248
249 % If target entry doesn't have computed service time (forwarding-only target),
250 % compute it directly from its activities' host demands
251 if target_servt == 0 || isnan(target_servt)
252 target_servt = 0;
253 % Sum host demands of all activities bound to this entry
254 for aidx = lqn.actsof{target_eidx}
255 if ~isempty(lqn.hostdem{aidx})
256 target_servt = target_servt + lqn.hostdem{aidx}.getMean();
257 end
258 end
259 end
260
261 entry_servt(source_eidx) = entry_servt(source_eidx) + fwd_prob * target_servt;
262 end
263end
264
265% this block fixes the problem that ResidT is scaled so that the
266% task has Vtask=1, but in call servt the entries need to have Ventry=1
267for eidx=(lqn.eshift+1):(lqn.eshift+lqn.nentries)
268 tidx = lqn.parent(eidx); % task of entry
269 hidx = lqn.parent(tidx); %host of entry
270 if ~self.ignore(tidx) && ~self.ignore(hidx)
271 % Check if this entry has sync callers (which create closed classes)
272 hasSyncCallers = full(any(lqn.issynccaller(:, eidx)));
273
274 if hasSyncCallers
275 % Original logic for entries with sync callers
276 % get class in host layer of task and entry
277 tidxclass = ensemble{self.idxhash(hidx)}.attribute.tasks(find(ensemble{self.idxhash(hidx)}.attribute.tasks(:,2) == tidx),1);
278 eidxclass = ensemble{self.idxhash(hidx)}.attribute.entries(find(ensemble{self.idxhash(hidx)}.attribute.entries(:,2) == eidx),1);
279 task_tput = sum(self.results{end,self.idxhash(hidx)}.TN(ensemble{self.idxhash(hidx)}.attribute.clientIdx,tidxclass));
280 entry_tput = sum(self.results{end,self.idxhash(hidx)}.TN(ensemble{self.idxhash(hidx)}.attribute.clientIdx,eidxclass));
281 if entry_tput > GlobalConstants.Zero
282 self.servt(eidx) = entry_servt(eidx) * task_tput / entry_tput;
283 self.residt(eidx) = entry_servt(eidx) * task_tput / entry_tput;
284 else
285 self.servt(eidx) = entry_servt(eidx);
286 self.residt(eidx) = entry_servt(eidx);
287 end
288 else
289 % For async-only targets, use entry_servt directly
290 % No throughput ratio scaling needed since there are no closed classes
291 self.servt(eidx) = entry_servt(eidx);
292 self.residt(eidx) = entry_servt(eidx);
293 end
294 end
295end
296
297% Phase-2 support: compute overtaking probability and apply correction
298% This must happen AFTER entry throughput is available (computed above)
299if self.hasPhase2
300 for e = 1:lqn.nentries
301 eidx = lqn.eshift + e;
302 tidx = lqn.parent(eidx);
303
304 if self.servt_ph2(eidx) > GlobalConstants.FineTol
305 % Get entry throughput (use task throughput as approximation if entry not available)
306 if self.tput(eidx) > GlobalConstants.FineTol
307 entry_tput = self.tput(eidx);
308 elseif self.tput(tidx) > GlobalConstants.FineTol
309 entry_tput = self.tput(tidx);
310 else
311 entry_tput = 0;
312 end
313
314 % Compute overtaking probability now that throughput is available
315 if entry_tput > GlobalConstants.FineTol
316 self.prOvertake(e) = self.overtake_prob(eidx);
317 else
318 self.prOvertake(e) = 0;
319 end
320
321 % Caller's response time = phase-1 only + P(overtake) * phase-2
322 % Phase-2 only delays if overtaking occurs
323 overtake_delay = self.prOvertake(e) * self.servt_ph2(eidx);
324
325 % The caller sees phase-1 + overtaking correction (not full phase-2)
326 % self.servt(eidx) remains unchanged (phase-1 + phase-2) for utilization calculation
327 self.residt(eidx) = self.servt_ph1(eidx) + overtake_delay;
328 end
329 end
330end
331
332%self.servt(lqn.eshift+1:lqn.eshift+lqn.nentries) = entry_servt(lqn.eshift+1:lqn.eshift+lqn.nentries);
333%entry_servt((lqn.ashift+1):end) = 0;
334for r=1:size(self.call_classes_updmap,1)
335 cidx = self.call_classes_updmap(r,2);
336 eidx = lqn.callpair(cidx,2);
337 if self.call_classes_updmap(r,3) > 1
338 if self.servt(eidx) > 0
339 self.servtproc{eidx} = Exp.fitMean(self.servt(eidx));
340 end
341 end
342end
343
344% determine call response times processes
345for r=1:size(self.call_classes_updmap,1)
346 cidx = self.call_classes_updmap(r,2);
347 eidx = lqn.callpair(cidx,2);
348 if self.call_classes_updmap(r,3) > 1
349 if it==1
350 % note that respt is per visit, so number of calls is 1
351 self.callservt(cidx) = self.servt(eidx);
352 self.callservtproc{cidx} = self.servtproc{eidx};
353 else
354 % note that respt is per visit, so number of calls is 1
355 if self.callservt(cidx) > 0
356 self.callservtproc{cidx} = Exp.fitMean(self.callservt(cidx));
357 end
358 end
359 end
360end
361
362self.ptaskcallers = zeros(size(self.ptaskcallers));
363% determine ptaskcallers for direct callers to tasks
364for t = 1:lqn.ntasks
365 tidx = lqn.tshift + t;
366 if ~lqn.isref(tidx)
367 [calling_idx, ~] = find(lqn.iscaller(:, lqn.entriesof{tidx})); %#ok<ASGLU>
368 callers = intersect(lqn.tshift+(1:lqn.ntasks), unique(calling_idx)');
369 caller_tput = zeros(1,lqn.ntasks);
370 for caller_idx=callers(:)'
371 caller_idxclass = self.ensemble{self.idxhash(tidx)}.attribute.tasks(1+find(self.ensemble{self.idxhash(tidx)}.attribute.tasks(2:end,2) == caller_idx),1);
372 caller_tput(caller_idx-lqn.tshift) = sum(self.results{end,self.idxhash(tidx)}.TN(self.ensemble{self.idxhash(tidx)}.attribute.clientIdx,caller_idxclass));
373 end
374 self.ptaskcallers(tidx,(lqn.tshift+1):(lqn.tshift+lqn.ntasks))= caller_tput / max(GlobalConstants.Zero, sum(caller_tput));
375 end
376end
377
378% determine ptaskcallers for direct callers to hosts
379for hidx = 1:lqn.nhosts
380 if ~self.ignore(tidx) && ~self.ignore(hidx)
381 caller_tput = zeros(1,lqn.ntasks);
382 callers = lqn.tasksof{hidx};
383 for caller_idx=callers
384 caller_idxclass = self.ensemble{self.idxhash(hidx)}.attribute.tasks(find(self.ensemble{self.idxhash(hidx)}.attribute.tasks(:,2) == caller_idx),1);
385 caller_tput(caller_idx-lqn.tshift) = caller_tput(caller_idx-lqn.tshift) + sum(self.results{end,self.idxhash(hidx)}.TN(self.ensemble{self.idxhash(hidx)}.attribute.clientIdx,caller_idxclass));
386 end
387 self.ptaskcallers(hidx,(lqn.tshift+1):(lqn.tshift+lqn.ntasks)) = caller_tput / max(GlobalConstants.Zero, sum(caller_tput));
388 end
389end
390
391% impute call probability using a DTMC random walk on the taskcaller graph
392P = self.ptaskcallers;
393P = dtmc_makestochastic(P); % hold mass at reference stations when there
394self.ptaskcallers_step{1} = P; % configure step 1
395for h = 1:lqn.nhosts
396 hidx=h;
397 for tidx = lqn.tasksof{hidx}
398 % initialize the probability mass on tidx
399 x0 = zeros(length(self.ptaskcallers),1);
400 x0(hidx) = 1;
401 x0=x0(:)';
402 % start the walk backward to impute probability of indirect callers
403 x = x0*P; % skip since pcallers already calculated in this case
404 for step=2:self.nlayers % upper bound on maximum dag height
405 x = x*P;
406 % here self.ptaskcallers_step{step}(tidx,remidx) is the
407 % probability that the request to tidx comes from remidx
408 self.ptaskcallers_step{step}(tidx,:) = x(:);
409 % here self.ptaskcallers_step{step}(hidx,remidx) is the
410 % probability that the request to hidx comes from remidx
411 % through the call that tidx puts on hidx, which is
412 % weighted by the relative tput of tidx on the tasks running on
413 % hidx
414 self.ptaskcallers_step{step}(hidx,:) = self.ptaskcallers(hidx,tidx)*x(:)';
415 if sum(x(find(lqn.isref)))>1.0-self.options.tol %#ok<FNDSB>
416 % if all the probability mass has reached backwards the
417 % reference stations, then stop
418 break;
419 end
420 self.ptaskcallers(:,tidx) = max([self.ptaskcallers(:,tidx), x(:)],[],2);
421 end
422 end
423end
424self.ensemble = ensemble;
425end
Definition mmt.m:93