LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getPerctRespT.m
1function [PercRT, PercTable] = getPerctRespT(self, percentiles, jobclass, method)
2% [PERCRT, PERCTABLE] = GETPERCTRESPT(SELF, PERCENTILES, JOBCLASS, METHOD)
3% Extract response time percentiles from CDF or solver-specific results
4%
5% Parameters:
6% self - NetworkSolver instance
7% percentiles - Array of percentile values (e.g., [0.90, 0.95, 0.99] or [90, 95, 99])
8% jobclass - (optional) specific job class to retrieve percentiles for
9% method - (optional) 'default' extracts the percentiles from getCdfRespT;
10% 'forktail' predicts the fork-join request tail latency with the
11% ForkTail approximation (fj_tail_forktail), which needs only the
12% mean and variance of the per-branch task response times and so
13% applies to heterogeneous branches and mixed service laws
14%
15% Returns:
16% PercRT - Struct array with fields for each class:
17% .class - class name
18% .percentiles - percentile levels (as percentages)
19% .values - percentile values (response times)
20% .method - extraction method ('cdf' or solver-specific)
21% PercTable - Formatted table for display
22%
23% This method provides a generic interface for extracting response time
24% percentiles. Solvers that compute CDFs via getCdfRespT can use this
25% method to extract percentile values. Solvers may override this method
26% to provide more efficient or accurate percentile computation.
27%
28% Copyright (c) 2012-2026, Imperial College London
29% All rights reserved.
30
31if nargin < 4 || isempty(method)
32 method = 'default';
33end
34
35% Normalize percentiles to [0, 1] range
36if any(percentiles > 1)
37 percentiles = percentiles / 100;
38end
39
40sn = self.getStruct();
41
42self.lastPerctMethod = lower(char(method));
43if strcmpi(method, 'forktail')
44 [PercRT, PercTable] = forktail_percentiles(self, sn, percentiles, ...
45 resolve_classes(sn, jobclass, nargin >= 3 && ~isempty(jobclass)));
46 return
47end
48
49% Determine which classes to return
50if nargin < 3 || isempty(jobclass)
51 % Return all classes
52 classes = 1:sn.nclasses;
53else
54 % Find class index
55 if ischar(jobclass) || isstring(jobclass)
56 % Class name provided
57 classIdx = find(strcmp(sn.classnames, jobclass));
58 if isempty(classIdx)
59 line_error(mfilename, 'Job class "%s" not found in model.', jobclass);
60 end
61 classes = classIdx;
62 else
63 % Class index provided
64 classes = jobclass;
65 end
66end
67
68% Try to get CDF for percentile extraction
69try
70 RD = self.getCdfRespT();
71catch ME
72 line_error(mfilename, ['Unable to compute percentiles. getCdfRespT not available for this solver.\n', ...
73 'Error: %s'], ME.message);
74end
75
76% Build PercRT per (station, class) from the [F(t), t] CDF cell (CDF value
77% first, time second). see _kb/06-solver-catalog.md for rationale
78PercRT = struct([]);
79idx = 0;
80
81for ci = 1:length(classes)
82 r = classes(ci);
83 for ist = 1:sn.nstations
84 cdfData = [];
85 if iscell(RD)
86 if size(RD,2) >= r && size(RD,1) >= ist
87 cdfData = RD{ist, r};
88 elseif size(RD,1) >= r && size(RD,2) == 1
89 cdfData = RD{r}; % per-class cell, as some overrides return
90 end
91 elseif isstruct(RD) && isfield(RD, 'C') && r <= length(RD.C)
92 cdfData = RD.C{r};
93 elseif isstruct(RD) && isfield(RD, 'CDF') && r <= length(RD.CDF)
94 cdfData = RD.CDF{r};
95 end
96 if isempty(cdfData)
97 continue % e.g. the Source station, which has no response time
98 end
99
100 if isstruct(cdfData)
101 if isfield(cdfData, 't') && isfield(cdfData, 'p')
102 times = cdfData.t(:);
103 probs = cdfData.p(:);
104 elseif isfield(cdfData, 'x') && isfield(cdfData, 'f')
105 times = cdfData.x(:);
106 probs = cumsum(cdfData.f(:)); % convert a PDF to a CDF
107 else
108 line_warning(mfilename, 'Unrecognized CDF structure at station %d for class %d.', ist, r);
109 continue;
110 end
111 elseif size(cdfData, 2) >= 2
112 probs = cdfData(:, 1);
113 times = cdfData(:, 2);
114 else
115 line_warning(mfilename, 'Unable to parse CDF data at station %d for class %d.', ist, r);
116 continue;
117 end
118
119 % Ensure the CDF is monotonic and free of repeated levels
120 [probs, sortIdx] = sort(probs);
121 times = times(sortIdx);
122 [probs, uniqueIdx] = unique(probs);
123 times = times(uniqueIdx);
124
125 if length(times) > 1 && length(probs) > 1
126 percValues = max(interp1(probs, times, percentiles, 'linear', 'extrap'), 0);
127 idx = idx + 1;
128 PercRT(idx).station = sn.nodenames{sn.stationToNode(ist)};
129 PercRT(idx).class = sn.classnames{r};
130 PercRT(idx).percentiles = percentiles; % kept in fractional form [0,1]
131 PercRT(idx).values = percValues;
132 PercRT(idx).method = 'cdf';
133 end
134 end
135end
136
137if isempty(PercRT)
138 line_warning(mfilename, 'No usable response time CDF was returned by %s.', class(self));
139end
140
141% Build PercTable for display
142if nargout > 1
143 Station = {};
144 JobClass = {};
145 Percentile = [];
146 ResponseTime = [];
147
148 for idx = 1:length(PercRT)
149 nPercentiles = length(PercRT(idx).percentiles);
150 for p = 1:nPercentiles
151 Station{end+1,1} = PercRT(idx).station;
152 JobClass{end+1,1} = PercRT(idx).class;
153 Percentile(end+1,1) = PercRT(idx).percentiles(p);
154 ResponseTime(end+1,1) = PercRT(idx).values(p);
155 end
156 end
157
158 Station = label(Station);
159 JobClass = label(JobClass);
160 PercTable = Table(Station, JobClass, Percentile, ResponseTime);
161end
162
163end
164
165function classes = resolve_classes(sn, jobclass, given)
166% CLASSES = RESOLVE_CLASSES(SN, JOBCLASS, GIVEN)
167% Class indexes requested by the caller, by name or by index.
168if ~given
169 classes = 1:sn.nclasses;
170 return
171end
172if ischar(jobclass) || isstring(jobclass)
173 classes = find(strcmp(sn.classnames, jobclass));
174 if isempty(classes)
175 line_error(mfilename, 'Job class "%s" not found in model.', jobclass);
176 end
177elseif isobject(jobclass) && isprop(jobclass, 'index')
178 classes = jobclass.index;
179else
180 classes = jobclass;
181end
182end
183
184function [PercRT, PercTable] = forktail_percentiles(self, sn, percentiles, classes)
185% [PERCRT, PERCTABLE] = FORKTAIL_PERCENTILES(SELF, SN, PERCENTILES, CLASSES)
186% Fork-join request tail latency by the ForkTail approximation. Each branch
187% between a fork and its join must be a single station: the approximation is
188% defined on the per-branch task response time, and a multi-station branch has
189% no M/G/1 moments of its own (see fj_tail_forktail).
190forks = find(sn.nodetype == NodeType.Fork)';
191if isempty(forks)
192 line_error(mfilename, 'The forktail method requires a model with a Fork node.');
193end
194if numel(forks) > 1
195 line_error(mfilename, 'The forktail method supports a single fork-join pair; this model has %d forks.', numel(forks));
196end
197f = forks(1);
198joinIdx = find(sn.fj(f,:));
199if isempty(joinIdx)
200 line_error(mfilename, 'The fork node has no matching join; the request response time is undefined.');
201end
202
203branches = find(sn.connmatrix(f,:));
204for b = branches
205 if ~sn.isstation(b)
206 line_error(mfilename, 'Branch node %s is not a station; the forktail method needs one queueing station per branch.', sn.nodenames{b});
207 end
208 if ~sn.connmatrix(b, joinIdx)
209 line_error(mfilename, 'Branch station %s does not feed the join directly; the forktail method needs one station per branch.', sn.nodenames{b});
210 end
211end
212
213[~, UN, ~, TN] = self.getAvg();
214PercRT = struct([]);
215idx = 0;
216for r = classes(:)'
217 ET = zeros(1, numel(branches));
218 VT = ET;
219 rho = ET;
220 ok = true;
221 for bi = 1:numel(branches)
222 ist = sn.nodeToStation(branches(bi));
223 lambda = TN(ist, r);
224 if lambda <= GlobalConstants.FineTol
225 ok = false; % the class does not traverse this fork
226 break
227 end
228 svc = self.model.nodes{branches(bi)}.getService(self.model.classes{r});
229 ES = svc.getMean();
230 VS = svc.getVar();
231 ES2 = VS + ES^2;
232 ES3 = svc.getSkewness()*VS^1.5 + 3*ES*ES2 - 2*ES^3;
233 [ET(bi), VT(bi)] = fj_mg1_respt_moments(lambda, ES, ES2, ES3);
234 rho(bi) = UN(ist, r);
235 end
236 if ~ok
237 continue
238 end
239 % ForkTail is a heavy-traffic result; under-shoots at low load.
240 % see _kb/06-solver-catalog.md for rationale
241 if max(rho) < 0.5
242 line_warning(mfilename, 'ForkTail is a heavy-traffic approximation; the busiest branch is at utilization %.2f, so the tail is likely under-predicted.\n', max(rho));
243 end
244 values = zeros(1, numel(percentiles));
245 for pi = 1:numel(percentiles)
246 values(pi) = fj_tail_forktail(ET, VT, [], percentiles(pi));
247 end
248 idx = idx + 1;
249 PercRT(idx).class = sn.classnames{r};
250 PercRT(idx).percentiles = percentiles;
251 PercRT(idx).values = values;
252 PercRT(idx).method = 'forktail';
253end
254
255if nargout > 1
256 JobClass = {}; Percentile = []; ResponseTime = [];
257 for i = 1:length(PercRT)
258 for p = 1:length(PercRT(i).percentiles)
259 JobClass{end+1,1} = PercRT(i).class; %#ok<AGROW>
260 Percentile(end+1,1) = PercRT(i).percentiles(p); %#ok<AGROW>
261 ResponseTime(end+1,1) = PercRT(i).values(p); %#ok<AGROW>
262 end
263 end
264 JobClass = label(JobClass);
265 PercTable = Table(JobClass, Percentile, ResponseTime);
266end
267end