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