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
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
16% PercRT - Struct array with fields
for each class:
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
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.
28% Copyright (c) 2012-2026, Imperial College London
31if nargin < 4 || isempty(method)
35% Normalize percentiles to [0, 1] range
36if any(percentiles > 1)
37 percentiles = percentiles / 100;
42self.lastPerctMethod = lower(
char(method));
43if strcmpi(method, 'forktail')
44 [PercRT, PercTable] = forktail_percentiles(self, sn, percentiles, ...
49% Determine which classes to return
52 classes = 1:sn.nclasses;
57 classIdx = find(strcmp(sn.classnames,
jobclass));
59 line_error(mfilename, 'Job class "%s" not found in model.',
jobclass);
63 % Class index provided
68% Try to get CDF for percentile extraction
70 RD = self.getCdfRespT();
72 line_error(mfilename, ['Unable to compute percentiles. getCdfRespT not available for this solver.\n', ...
73 'Error: %s'], ME.message);
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
81for ci = 1:length(classes)
83 for ist = 1:sn.nstations
86 if size(RD,2) >= r && size(RD,1) >= ist
88 elseif size(RD,1) >= r && size(RD,2) == 1
89 cdfData = RD{r}; % per-
class cell, as some overrides return
91 elseif isstruct(RD) && isfield(RD,
'C') && r <= length(RD.C)
93 elseif isstruct(RD) && isfield(RD,
'CDF') && r <= length(RD.CDF)
97 continue % e.g. the Source station, which has no response time
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
108 line_warning(mfilename, 'Unrecognized CDF structure at station %d for class %d.', ist, r);
111 elseif size(cdfData, 2) >= 2
112 probs = cdfData(:, 1);
113 times = cdfData(:, 2);
115 line_warning(mfilename, 'Unable to parse CDF data at station %d for class %d.', ist, r);
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);
125 if length(times) > 1 && length(probs) > 1
126 percValues = max(interp1(probs, times, percentiles, 'linear', 'extrap'), 0);
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';
138 line_warning(mfilename,
'No usable response time CDF was returned by %s.',
class(self));
141% Build PercTable
for display
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);
159 JobClass = label(JobClass);
160 PercTable = Table(
Station, JobClass, Percentile, ResponseTime);
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.
169 classes = 1:sn.nclasses;
173 classes = find(strcmp(sn.classnames,
jobclass));
175 line_error(mfilename,
'Job class "%s" not found in model.',
jobclass);
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)';
192 line_error(mfilename, 'The forktail method requires a model with a Fork node.');
195 line_error(mfilename, 'The forktail method supports a single fork-join pair; this model has %d forks.', numel(forks));
198joinIdx = find(sn.fj(f,:));
200 line_error(mfilename, 'The fork node has no matching join; the request response time
is undefined.');
203branches = find(sn.connmatrix(f,:));
206 line_error(mfilename, 'Branch node %s
is not a station; the forktail method needs one queueing station per branch.', sn.nodenames{b});
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});
213[~, UN, ~, TN] = self.getAvg();
217 ET = zeros(1, numel(branches));
221 for bi = 1:numel(branches)
222 ist = sn.nodeToStation(branches(bi));
224 if lambda <= GlobalConstants.FineTol
225 ok = false; % the class does not traverse this fork
228 svc = self.model.nodes{branches(bi)}.getService(self.model.classes{r});
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);
239 % ForkTail is a heavy-traffic result; under-shoots at low load.
240 % see _kb/06-solver-catalog.md for rationale
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));
244 values = zeros(1, numel(percentiles));
245 for pi = 1:numel(percentiles)
246 values(pi) = fj_tail_forktail(ET, VT, [], percentiles(pi));
249 PercRT(idx).class = sn.classnames{r};
250 PercRT(idx).percentiles = percentiles;
251 PercRT(idx).values = values;
252 PercRT(idx).method = 'forktail
';
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>
264 JobClass = label(JobClass);
265 PercTable = Table(JobClass, Percentile, ResponseTime);