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=0:(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 QN_val = self.results{end,layerIdx}.QN(nodeidx,classidx);
47 if TN_ref > GlobalConstants.FineTol
48 self.residt(aidx) = QN_val / TN_ref;
49 else
50 self.residt(aidx) = self.results{end,layerIdx}.WN(nodeidx,classidx);
51 end
52 self.tput(aidx) = self.results{end,layerIdx}.TN(nodeidx,classidx);
53 end
54
55 % An activity think time sits in series with its host demand and is held at
56 % the task's server, so it belongs to the activity's residence as well as its
57 % service: entry_servt below sums residt, so adding it to servt alone would
58 % never reach the entry. The processor layer is unaffected, since the host
59 % demand it serves is untouched.
60 zt_act = lqn_act_thinktime(lqn, aidx);
61 if zt_act > 0
62 self.servt(aidx) = self.servt(aidx) + zt_act;
63 self.residt(aidx) = self.residt(aidx) + zt_act;
64 end
65
66 % Fix for async-only entry targets: use RN (response time per visit) for residt
67 % The host layer closed model incorrectly splits residence time (WN) between
68 % activities when an entry only receives async calls (no sync callers).
69 % For async-only entries, use RN instead of WN since the async arrivals
70 % don't share the closed chain's visit ratio - each async arrival gets
71 % the full response time per visit.
72 if aidx > lqn.ashift && aidx <= lqn.ashift + lqn.nacts
73 % This is an activity - find its bound entry
74 for eidx = (lqn.eshift+1):(lqn.eshift+lqn.nentries)
75 if full(lqn.graph(eidx, aidx)) > 0
76 % Found bound entry - check if async-only
77 hasSyncCallers = full(any(lqn.issynccaller(:, eidx)));
78 hasAsyncCallers = full(any(lqn.isasynccaller(:, eidx)));
79 if hasAsyncCallers && ~hasSyncCallers
80 % Async-only target: use RN (response time per visit)
81 % instead of WN (residence time with visit ratio)
82 self.residt(aidx) = self.servt(aidx); % servt already has RN
83 end
84 break;
85 end
86 end
87 end
88
89 % Recover from Inf/NaN: snap back to previous iteration's value
90 if it > 1
91 if (isinf(self.servt(aidx)) || isnan(self.servt(aidx))) && ~isnan(self.servt_prev(aidx))
92 self.servt(aidx) = self.servt_prev(aidx);
93 end
94 if (isinf(self.residt(aidx)) || isnan(self.residt(aidx))) && ~isnan(self.residt_prev(aidx))
95 self.residt(aidx) = self.residt_prev(aidx);
96 end
97 if (isinf(self.tput(aidx)) || isnan(self.tput(aidx))) && ~isnan(self.tput_prev(aidx))
98 self.tput(aidx) = self.tput_prev(aidx);
99 end
100 end
101
102 % Apply under-relaxation if enabled and not first iteration
103 omega = self.relax_omega;
104 if omega < 1.0 && it > 1
105 if ~isnan(self.servt_prev(aidx))
106 self.servt(aidx) = omega * self.servt(aidx) + (1 - omega) * self.servt_prev(aidx);
107 end
108 if ~isnan(self.residt_prev(aidx))
109 self.residt(aidx) = omega * self.residt(aidx) + (1 - omega) * self.residt_prev(aidx);
110 end
111 if ~isnan(self.tput_prev(aidx))
112 self.tput(aidx) = omega * self.tput(aidx) + (1 - omega) * self.tput_prev(aidx);
113 end
114 end
115 % Store current values for next iteration
116 self.servt_prev(aidx) = self.servt(aidx);
117 self.residt_prev(aidx) = self.residt(aidx);
118 self.tput_prev(aidx) = self.tput(aidx);
119
120 % Safeguard against MVA numerical instability producing extreme values
121 % (matches Python _update_metrics_default max_servt guard)
122 max_servt = 1e10;
123 if self.servt(aidx) > 0 && self.servt(aidx) <= max_servt
124 self.servtproc{aidx} = Exp.fitMean(self.servt(aidx));
125 end
126 self.tputproc{aidx} = Exp.fitRate(self.tput(aidx));
127end
128
129% Phase-2 support: split activity service times by phase
130% Note: Overtaking probability is computed later after entry throughput is available
131if self.hasPhase2
132 % Reset phase-specific arrays
133 self.servt_ph1 = zeros(lqn.nidx, 1);
134 self.servt_ph2 = zeros(lqn.nidx, 1);
135
136 % Split activity service times by phase
137 for a = 1:lqn.nacts
138 aidx = lqn.ashift + a;
139 if lqn.actphase(a) == 1
140 self.servt_ph1(aidx) = self.servt(aidx);
141 else
142 self.servt_ph2(aidx) = self.servt(aidx);
143 end
144 end
145
146 % Aggregate phase service times to entry level
147 for e = 1:lqn.nentries
148 eidx = lqn.eshift + e;
149 acts = lqn.actsof{eidx};
150 for aidx = acts
151 a = aidx - lqn.ashift;
152 if a > 0 && a <= lqn.nacts
153 if lqn.actphase(a) == 1
154 self.servt_ph1(eidx) = self.servt_ph1(eidx) + self.servt_ph1(aidx);
155 else
156 self.servt_ph2(eidx) = self.servt_ph2(eidx) + self.servt_ph2(aidx);
157 end
158 end
159 end
160 end
161end
162
163% obtain throughput for activities in thinkt_classes_updmap (needed for async calls)
164% this ensures tputproc is set for activities that make async calls from client nodes
165for r=1:size(self.thinkt_classes_updmap,1)
166 idx = self.thinkt_classes_updmap(r,1);
167 aidx = self.thinkt_classes_updmap(r,2);
168 nodeidx = self.thinkt_classes_updmap(r,3);
169 classidx = self.thinkt_classes_updmap(r,4);
170
171 % only update if not already set by servt_classes_updmap processing
172 if isempty(self.tputproc) || length(self.tputproc) < aidx || isempty(self.tputproc{aidx})
173 iter_min = min(30,ceil(self.options.iter_max/4));
174 wnd_size = (it-self.averagingstart+1);
175 if ~isempty(self.averagingstart) && it>=iter_min % assume steady-state
176 self.tput(aidx) = 0;
177 for w=0:(wnd_size-1)
178 self.tput(aidx) = self.tput(aidx) + self.results{end-w,self.idxhash(idx)}.TN(nodeidx,classidx) / wnd_size;
179 end
180 else
181 self.tput(aidx) = self.results{end,self.idxhash(idx)}.TN(nodeidx,classidx);
182 end
183 self.tputproc{aidx} = Exp.fitRate(self.tput(aidx));
184 end
185end
186
187% Obtain the join times for AND-Join activities
188% For each AND-join activity, compute the synchronization delay as
189% the maximum of predecessor branch service times.
190self.joint = zeros(lqn.nidx,1);
191joinedacts = find(lqn.actpretype == ActivityPrecedenceType.PRE_AND)';
192for aidx = joinedacts
193 % Find predecessor activities in the activity graph
194 preds = find(lqn.graph(:, aidx) > 0)';
195 pred_servts = [];
196 for pidx = preds
197 if pidx > lqn.ashift && pidx <= lqn.ashift + lqn.nacts
198 pred_servts(end+1) = self.servt(pidx); %#ok<AGROW>
199 end
200 end
201 if ~isempty(pred_servts)
202 % Join time = completion time of the join, i.e. the k-th smallest of the
203 % predecessor branch times, where k is the quorum of the join. A join with
204 % no quorum waits for all its branches, so k equals the branch count and the
205 % join time is the maximum.
206 nbranches = length(pred_servts);
207 quorum = nbranches;
208 if isfield(lqn, 'actquorum') && aidx <= length(lqn.actquorum)
209 q = full(lqn.actquorum(aidx));
210 if q >= 1 && q <= nbranches
211 quorum = q;
212 end
213 end
214 if nbranches == 1
215 self.joint(aidx) = pred_servts(1);
216 elseif quorum >= nbranches
217 % No quorum: recursive two-moment approximation for the max of
218 % exponentials, E[max(X1,X2)] = E[X1] + E[X2] - E[min(X1,X2)] with
219 % E[min(X1,X2)] = t1*t2/(t1+t2) (Clark 1961).
220 mu_max = pred_servts(1);
221 for bi = 2:nbranches
222 t1 = mu_max;
223 t2 = pred_servts(bi);
224 if t1 > 0 && t2 > 0
225 mu_max = t1 + t2 - t1*t2/(t1+t2);
226 else
227 mu_max = max(t1, t2);
228 end
229 end
230 self.joint(aidx) = mu_max;
231 else
232 % Genuine quorum k < n: the k-th order statistic of the branch times.
233 % Branch times are taken as exponential, so the variance is the square
234 % of the mean, matching the assumption used in the max case above.
235 self.joint(aidx) = fj_quorum_moments(pred_servts, pred_servts.^2, quorum);
236 end
237 end
238end
239
240% obtain the call residence time
241self.callservt = zeros(lqn.ncalls,1);
242self.callresidt = zeros(lqn.ncalls,1);
243for r=1:size(self.call_classes_updmap,1)
244 idx = self.call_classes_updmap(r,1);
245 cidx = self.call_classes_updmap(r,2);
246 nodeidx = self.call_classes_updmap(r,3);
247 classidx = self.call_classes_updmap(r,4);
248 if self.call_classes_updmap(r,3) > 1
249 if nodeidx == 1
250 self.callservt(cidx) = 0;
251 else
252 self.callservt(cidx) = self.results{end, self.idxhash(idx)}.RN(nodeidx,classidx) * self.lqn.callproc{cidx}.getMean;
253 self.callresidt(cidx) = self.results{end, self.idxhash(idx)}.WN(nodeidx,classidx);
254 end
255 % Recover from Inf/NaN (e.g. a transiently unstable open chain in a
256 % layer): snap back to the previous iteration's value, otherwise the
257 % under-relaxation below makes Inf absorbing.
258 if (isinf(self.callservt(cidx)) || isnan(self.callservt(cidx)))
259 if it > 1 && isfinite(self.callservt_prev(cidx))
260 self.callservt(cidx) = self.callservt_prev(cidx);
261 else
262 self.callservt(cidx) = 0;
263 end
264 end
265 if (isinf(self.callresidt(cidx)) || isnan(self.callresidt(cidx)))
266 if it > 1 && isfinite(self.callresidt_prev(cidx))
267 self.callresidt(cidx) = self.callresidt_prev(cidx);
268 else
269 self.callresidt(cidx) = 0;
270 end
271 end
272 % Growth rate capping removed - it prevents callservt from converging
273 % to the correct value when initial values are near-zero (Immediate)
274 % Apply under-relaxation to call service times
275 omega = self.relax_omega;
276 if omega < 1.0 && it > 1 && ~isnan(self.callservt_prev(cidx))
277 self.callservt(cidx) = omega * self.callservt(cidx) + (1 - omega) * self.callservt_prev(cidx);
278 end
279 self.callservt_prev(cidx) = self.callservt(cidx);
280 self.callresidt_prev(cidx) = self.callresidt(cidx);
281 end
282end
283
284% then resolve the entry servt summing up these contributions
285entry_servt = self.servtmatrix*[self.residt;self.callresidt(:)];
286entry_servt(1:lqn.eshift) = 0;
287
288% Forwarding is represented by caller-side pseudo rendezvous calls added
289% by lqn_fwd_rendezvous (LQNS phase.cc addForwardingRendezvous port), so
290% FWD calls carry no blocking here: callservt/callresidt of FWD calls
291% remain zero and no chain delay is charged into the SYNC calls.
292
293
294
295% this block fixes the problem that ResidT is scaled so that the
296% task has Vtask=1, but in call servt the entries need to have Ventry=1
297for eidx=(lqn.eshift+1):(lqn.eshift+lqn.nentries)
298 tidx = lqn.parent(eidx); % task of entry
299 hidx = lqn.parent(tidx); %host of entry
300 if ~self.ignore(tidx) && ~self.ignore(hidx)
301 % Check if this entry has sync callers (which create closed classes)
302 hasSyncCallers = full(any(lqn.issynccaller(:, eidx)));
303
304 if hasSyncCallers
305 % Original logic for entries with sync callers
306 % get class in host layer of task and entry
307 tidxclass = ensemble{self.idxhash(hidx)}.attribute.tasks(find(ensemble{self.idxhash(hidx)}.attribute.tasks(:,2) == tidx),1);
308 eidxclass = ensemble{self.idxhash(hidx)}.attribute.entries(find(ensemble{self.idxhash(hidx)}.attribute.entries(:,2) == eidx),1);
309 task_tput = sum(self.results{end,self.idxhash(hidx)}.TN(ensemble{self.idxhash(hidx)}.attribute.clientIdx,tidxclass));
310 entry_tput = sum(self.results{end,self.idxhash(hidx)}.TN(ensemble{self.idxhash(hidx)}.attribute.clientIdx,eidxclass));
311 if entry_tput > GlobalConstants.Zero
312 self.servt(eidx) = entry_servt(eidx) * task_tput / entry_tput;
313 self.residt(eidx) = entry_servt(eidx) * task_tput / entry_tput;
314 else
315 self.servt(eidx) = entry_servt(eidx);
316 self.residt(eidx) = entry_servt(eidx);
317 end
318 else
319 % For async-only targets, use entry_servt directly
320 % No throughput ratio scaling needed since there are no closed classes
321 self.servt(eidx) = entry_servt(eidx);
322 self.residt(eidx) = entry_servt(eidx);
323 end
324 end
325end
326
327% Phase-2 support: compute overtaking probability and apply correction
328% This must happen AFTER entry throughput is available (computed above)
329if self.hasPhase2
330 for e = 1:lqn.nentries
331 eidx = lqn.eshift + e;
332 tidx = lqn.parent(eidx);
333
334 if self.servt_ph2(eidx) > GlobalConstants.FineTol
335 if lqn.isref(tidx) || ~full(any(lqn.issynccaller(:, eidx)))
336 self.residt(eidx) = self.servt(eidx);
337 continue;
338 end
339 % Get entry throughput (use task throughput as approximation if entry not available)
340 if self.tput(eidx) > GlobalConstants.FineTol
341 entry_tput = self.tput(eidx);
342 elseif self.tput(tidx) > GlobalConstants.FineTol
343 entry_tput = self.tput(tidx);
344 else
345 entry_tput = 0;
346 end
347
348 % Compute overtaking probability now that throughput is available.
349 % Layer-1 LQNS phased-server Markov chain (aligns with lqns -t
350 % overtaking); see overtake_prob_markov / lqn_overtake_markov.
351 if entry_tput > GlobalConstants.FineTol
352 self.prOvertake(e) = self.overtake_prob_markov(eidx);
353 else
354 self.prOvertake(e) = 0;
355 end
356
357 % Caller's response time = phase-1 only + P(overtake) * phase-2
358 % Phase-2 only delays if overtaking occurs
359 overtake_delay = self.prOvertake(e) * self.servt_ph2(eidx);
360
361 % The caller sees phase-1 + overtaking correction (not full phase-2)
362 % self.servt(eidx) remains unchanged (phase-1 + phase-2) for utilization calculation
363 self.residt(eidx) = self.servt_ph1(eidx) + overtake_delay;
364 end
365 end
366end
367
368%self.servt(lqn.eshift+1:lqn.eshift+lqn.nentries) = entry_servt(lqn.eshift+1:lqn.eshift+lqn.nentries);
369%entry_servt((lqn.ashift+1):end) = 0;
370for r=1:size(self.call_classes_updmap,1)
371 cidx = self.call_classes_updmap(r,2);
372 eidx = lqn.callpair(cidx,2);
373 if self.call_classes_updmap(r,3) > 1
374 if self.servt(eidx) > 0
375 self.servtproc{eidx} = Exp.fitMean(self.servt(eidx));
376 end
377 end
378end
379
380% determine call response times processes
381for r=1:size(self.call_classes_updmap,1)
382 cidx = self.call_classes_updmap(r,2);
383 eidx = lqn.callpair(cidx,2);
384 if self.call_classes_updmap(r,3) > 1
385 if it==1
386 % note that respt is per visit, so number of calls is 1
387 self.callservt(cidx) = self.servt(eidx);
388 self.callservtproc{cidx} = self.servtproc{eidx};
389 else
390 % note that respt is per visit, so number of calls is 1
391 if self.callservt(cidx) > 0
392 self.callservtproc{cidx} = Exp.fitMean(self.callservt(cidx));
393 end
394 end
395 end
396end
397
398% Overtaking correction for SYNC calls into a phase-2 TARGET (server) entry.
399% In LQN semantics the caller is released at the reply (end of phase 1) and is
400% NOT blocked for the target's phase-2 execution, except when overtaking makes
401% a subsequent request wait behind a previous phase-2. Hence the effective
402% service the caller charges for the call reduces from the full server service
403% to residt(target) = servt_ph1 + prOt * servt_ph2. Anchoring the call service
404% process to this value propagates the overtaking correction into the caller
405% layer (both its throughput and its response time) at the next SolverLN
406% iteration; residt(target) is recomputed from prOt each iteration, so this is
407% a stable fixed point (no feedback double-counting). See overtake_prob_markov.
408if self.hasPhase2
409 for cidx = 1:lqn.ncalls
410 if lqn.calltype(cidx) ~= CallType.SYNC
411 continue;
412 end
413 target_eidx = lqn.callpair(cidx, 2);
414 e_tgt = target_eidx - lqn.eshift;
415 if e_tgt >= 1 && e_tgt <= lqn.nentries ...
416 && self.servt_ph2(target_eidx) > GlobalConstants.FineTol
417 callmean = lqn.callproc{cidx}.getMean();
418 eff = self.residt(target_eidx); % servt_ph1 + prOt * servt_ph2
419 if eff > 0
420 self.callservt(cidx) = eff * callmean;
421 self.callresidt(cidx) = eff * callmean;
422 self.callservtproc{cidx} = Exp.fitMean(eff * callmean);
423 end
424 end
425 end
426end
427
428self.ensemble = ensemble;
429end
Definition Station.m:245