@mark1russell7/kind 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -559,6 +559,20 @@ interface BacklogTerm<A = Quantity> {
559
559
  * Without it, the drain adds only r, as in SYS-42.
560
560
  */
561
561
  readonly drains?: boolean | undefined;
562
+ /**
563
+ * This time (SYS-45) is the largest drain time of one slow session. In a run, at most one session on
564
+ * the target drains longer than d.hi, up to this time. Thus the window closes up to
565
+ * (n − 1) × d.hi + this time after the band end (`backlogEnd`). With `drains`, each session
566
+ * can fail more requests in the drain (`drainBand`). Without it, each session drains in d.
567
+ */
568
+ readonly slowest?: A | undefined;
569
+ /**
570
+ * With this flag (SYS-45), the band of the last failed request (`overshootBand`) opens at the
571
+ * low end of the end band, plus n × d.lo. The last failed request can start up to one
572
+ * turn before the window closes. The end band also opens one turn before the band end. The judge
573
+ * already checks the last failure from that low end. Without it, that band opens at n × d.lo.
574
+ */
575
+ readonly early?: boolean | undefined;
562
576
  }
563
577
  /** A replica is one pod of the service at the start of a run. */
564
578
  interface Replica {
@@ -648,8 +662,17 @@ declare const showBand: (band: Band) => string;
648
662
  /**
649
663
  * This function gives the end band of a window with the backlog term (`BacklogTerm`). It adds n × d
650
664
  * to each end of `end`, the end band without the term. The value n is the sessions on the target.
665
+ * A term with `slowest` (SYS-45) adds (n − 1) × d.hi + `slowest` to the high end instead.
666
+ */
667
+ declare const backlogEnd: (end: Band, term: Pick<BacklogTerm, "drain"> & Partial<Pick<BacklogTerm, "slowest">>, sessions: number) => Band;
668
+ /**
669
+ * This function gives the band of the start of the last failed request of a window with the backlog
670
+ * term, from the band end (the high end of `end`). This is the overshoot of a run. Its high end is
671
+ * the high end of `backlogEnd`. Without `early`, its low end is n × d.lo (SYS-42). With `early`
672
+ * (SYS-45), the last failed request can start up to one turn before the window closes. Its low end
673
+ * is then the low end of `end` plus n × d.lo. The end band opens one turn before the band end.
651
674
  */
652
- declare const backlogEnd: (end: Band, term: Pick<BacklogTerm, "drain">, sessions: number) => Band;
675
+ declare const overshootBand: (end: Band, term: Pick<BacklogTerm, "drain"> & Partial<Pick<BacklogTerm, "slowest" | "early">>, sessions: number) => Band;
653
676
  /** A count band is a range of counts of requests. */
654
677
  interface CountBand {
655
678
  readonly lo: number;
@@ -660,9 +683,10 @@ interface CountBand {
660
683
  * request each turn until the window closes. With n sessions on the target, the window closes up to
661
684
  * n × d.hi after the band end (`backlogEnd`). Thus one session fails 0 to ceil(n × d.hi / turn)
662
685
  * requests in the drain. The turn is the mean period of a client (`mean`) times its sessions
663
- * (`connections`). The term r is not in this band.
686
+ * (`connections`). The term r is not in this band. With `slowest` (SYS-45), the window closes up to
687
+ * (n − 1) × d.hi + `slowest` after the band end. Thus one session fails up to that time over the turn.
664
688
  */
665
- declare const drainBand: (term: Pick<BacklogTerm, "drain">, mean: Quantity, connections: number, sessions: number) => CountBand;
689
+ declare const drainBand: (term: Pick<BacklogTerm, "drain"> & Partial<Pick<BacklogTerm, "slowest">>, mean: Quantity, connections: number, sessions: number) => CountBand;
666
690
  /**
667
691
  * This function gives the failed requests that the backlog term predicts (`BacklogTerm`). Each of
668
692
  * the n sessions on the target gets a request each turn. The turn is the measured mean period of a
@@ -672,7 +696,7 @@ declare const drainBand: (term: Pick<BacklogTerm, "drain">, mean: Quantity, conn
672
696
  *
673
697
  * A term with `drains` (SYS-43) also adds the drain failures of each session (`drainBand`).
674
698
  */
675
- declare const requestBand: (term: Pick<BacklogTerm, "requests"> & Partial<Pick<BacklogTerm, "drain" | "drains">>, span: Quantity, mean: Quantity, connections: number, sessions: number) => CountBand;
699
+ declare const requestBand: (term: Pick<BacklogTerm, "requests"> & Partial<Pick<BacklogTerm, "drain" | "drains" | "slowest">>, span: Quantity, mean: Quantity, connections: number, sessions: number) => CountBand;
676
700
  //#endregion
677
701
  //#region src/names.d.ts
678
702
  /** These are the ports of the app: its server, and its health server for the probes. */
@@ -686,11 +710,6 @@ declare const CLIENT = "client";
686
710
  declare const REDIS = "redis";
687
711
  //#endregion
688
712
  //#region src/observe.d.ts
689
- /**
690
- * This module reads a run from its lines. Each time is in milliseconds on CLOCK_MONOTONIC. All the
691
- * containers of the VM share that clock: the clients, the service, the agents, the injector, the
692
- * watcher and the prober.
693
- */
694
713
  /**
695
714
  * A line is one JSON line of a source of a run, with its stamp `t` in milliseconds on
696
715
  * CLOCK_MONOTONIC. It is a request of a client, a record of the service, a line of grpc-go or a probe answer. It is
@@ -1296,16 +1315,23 @@ declare const readRecord: (data: unknown) => KindCellRecord;
1296
1315
  //#region src/stats.d.ts
1297
1316
  /** This fraction, δ_rel, is how far the median of a cell can be outside its band and stay equivalent. */
1298
1317
  declare const RELATIVE = .05;
1299
- /** This function gives the median of sorted values: the middle value, or the mean of the two middle values. */
1318
+ /**
1319
+ * This function gives the median of sorted values: the middle value, or the mean of the two middle values.
1320
+ * It is `median` of @mark1russell7/rigor-statistics, which throws a RangeError for an empty list.
1321
+ */
1300
1322
  declare const middle: (sorted: readonly number[]) => number;
1301
- /** This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). */
1323
+ /**
1324
+ * This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). It is
1325
+ * `quantileSorted` of @mark1russell7/rigor-statistics with the method `"nearest-rank"`.
1326
+ */
1302
1327
  declare const quantileOf: (sorted: readonly number[], p: number) => number;
1303
1328
  /**
1304
1329
  * This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
1305
1330
  * largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
1306
- * at 98.8 %.
1331
+ * at 98.8 %. It is `medianInterval` of @mark1russell7/rigor-statistics at the level 0.95. An
1332
+ * empty list throws a RangeError.
1307
1333
  */
1308
1334
  declare const medianInterval: (sorted: readonly number[]) => [number, number];
1309
1335
  //#endregion
1310
- export { unitOf as $, scenarioProblems as $n, REDIS as $t, TriggerV1 as A, Trigger as An, recordsOf as At, RunCheck as B, NODE_IMAGES as Bn, sliceReadyAt as Bt, KindCellResult as C, Prediction as Cn, intakeOf as Ct, PredictionV1 as D, ResetReuse as Dn, periodOf as Dt, LoadSample as E, Replica as En, outcomesOf as Et, KIND_NOISE as F, Dataplane as Fn, sessionsAt as Ft, factsOf as G, cidrOverlap as Gn, transitionOf as Gt, checkRun as H, ZONE_KEPT as Hn, slicesOf as Ht, KindDecision as I, KUBE_PROXY_MODES as In, signalOf as It, observationEnd as J, isKubeProxyMode as Jn, ztunnelReplaced as Jt, judgeKind as K, cidrRange as Kn, tunnelsOf as Kt, KindVerdict as L, KindConfig as Ln, signalsOf as Lt, upgrade as M, WindowPrediction as Mn, restartOf as Mt, upgradeCell as N, Workload as Nn, restartsAt as Nt, RunCheckV1 as O, Stack as On, podsAt as Ot, Deleted as P, DATAPLANES as Pn, rulesOf as Pt, reusesReset as Q, scenario as Qn, PORT as Qt, LAST_SLACK as R, KindScenario as Rn, sliceDropOf as Rt, KindCellRecordV1 as S, PodSettings as Sn, failuresOf as St, KindVerdictV1 as T, Reference as Tn, kernelCounts as Tt, cyclesOf as U, ZONE_LABEL as Un, socketsOf as Ut, RunFacts as V, NodeImage as Vn, sliceReadyOf as Vt, deletionsOf as W, ZONE_LOST as Wn, stoppingOf as Wt, propagationSource as X, kubeProxyPatch as Xn, HEALTH_PORT as Xt, propagationOf as Y, kindConfig as Yn, CLIENT as Yt, referenceOf as Z, nodeNames as Zn, LOG_LABEL as Zt, ClusterManifest as _, KubeletProbe as _n, byClients as _t, STAGES as a, requestBand as an, PodLine as at, FORMAT_V1 as b, MeshMode as bn, dropOf as bt, readRecord as c, showBand as cn, Request as ct, calibrationOf as d, Cycle as dn, SliceLine as dt, SERVER as en, zoneOf as er, updatesOf as et, cellsTable as f, Declaration as fn, SocketLine as ft, BandV1 as g, KindRun as gn, backendsAt as gt, timeSpread as h, KindCell as hn, ZtunnelPodLine as ht, quantileOf as i, drainBand as in, Period as it, judge as j, TriggerKind as jn, requestsOf as jt, RunFactsV1 as k, Template as kn, podsOf as kt, Judged as l, BacklogTerm as ln, RulesLine as lt, predictionText as m, Flap as mn, TunnelLine as mt, medianInterval as n, backlogEnd as nn, HBONE_PORT as nt, kindCellRecordSchema as o, seconds as on, PropagationSource as ot, medianText as p, EdgeProtocol as pn, TCP_STATE as pt, observationAfter as q, goDuration as qn, ztunnelPodsOf as qt, middle as r, bandOf as rn, Line as rt, kindCellRecordV1Schema as s, secondsOf as sn, RecordLine as st, RELATIVE as t, CountBand as tn, Endpoint as tt, TimeSpread as u, Band as un, Sessions as ut, EnvironmentManifest as v, Mesh as vn, byNode as vt, KindCellV1 as w, RedisDeclaration as wn, isTunnelState as wt, KindCellRecord as x, NonePrediction as xn, emptyOf as xt, FORMAT as y, MeshFlavor as yn, deletionOf as yt, Propagated as z, KubeProxyMode as zn, sliceEmptyOf as zt };
1311
- //# sourceMappingURL=judge-BbzdXnBz.d.ts.map
1336
+ export { unitOf as $, scenario as $n, REDIS as $t, TriggerV1 as A, Template as An, recordsOf as At, RunCheck as B, KubeProxyMode as Bn, sliceReadyAt as Bt, KindCellResult as C, PodSettings as Cn, intakeOf as Ct, PredictionV1 as D, Replica as Dn, periodOf as Dt, LoadSample as E, Reference as En, outcomesOf as Et, KIND_NOISE as F, DATAPLANES as Fn, sessionsAt as Ft, factsOf as G, ZONE_LOST as Gn, transitionOf as Gt, checkRun as H, NodeImage as Hn, slicesOf as Ht, KindDecision as I, Dataplane as In, signalOf as It, observationEnd as J, goDuration as Jn, ztunnelReplaced as Jt, judgeKind as K, cidrOverlap as Kn, tunnelsOf as Kt, KindVerdict as L, KUBE_PROXY_MODES as Ln, signalsOf as Lt, upgrade as M, TriggerKind as Mn, restartOf as Mt, upgradeCell as N, WindowPrediction as Nn, restartsAt as Nt, RunCheckV1 as O, ResetReuse as On, podsAt as Ot, Deleted as P, Workload as Pn, rulesOf as Pt, reusesReset as Q, nodeNames as Qn, PORT as Qt, LAST_SLACK as R, KindConfig as Rn, sliceDropOf as Rt, KindCellRecordV1 as S, NonePrediction as Sn, failuresOf as St, KindVerdictV1 as T, RedisDeclaration as Tn, kernelCounts as Tt, cyclesOf as U, ZONE_KEPT as Un, socketsOf as Ut, RunFacts as V, NODE_IMAGES as Vn, sliceReadyOf as Vt, deletionsOf as W, ZONE_LABEL as Wn, stoppingOf as Wt, propagationSource as X, kindConfig as Xn, HEALTH_PORT as Xt, propagationOf as Y, isKubeProxyMode as Yn, CLIENT as Yt, referenceOf as Z, kubeProxyPatch as Zn, LOG_LABEL as Zt, ClusterManifest as _, KindRun as _n, byClients as _t, STAGES as a, overshootBand as an, PodLine as at, FORMAT_V1 as b, MeshFlavor as bn, dropOf as bt, readRecord as c, secondsOf as cn, Request as ct, calibrationOf as d, Band as dn, SliceLine as dt, SERVER as en, scenarioProblems as er, updatesOf as et, cellsTable as f, Cycle as fn, SocketLine as ft, BandV1 as g, KindCell as gn, backendsAt as gt, timeSpread as h, Flap as hn, ZtunnelPodLine as ht, quantileOf as i, drainBand as in, Period as it, judge as j, Trigger as jn, requestsOf as jt, RunFactsV1 as k, Stack as kn, podsOf as kt, Judged as l, showBand as ln, RulesLine as lt, predictionText as m, EdgeProtocol as mn, TunnelLine as mt, medianInterval as n, backlogEnd as nn, HBONE_PORT as nt, kindCellRecordSchema as o, requestBand as on, PropagationSource as ot, medianText as p, Declaration as pn, TCP_STATE as pt, observationAfter as q, cidrRange as qn, ztunnelPodsOf as qt, middle as r, bandOf as rn, Line as rt, kindCellRecordV1Schema as s, seconds as sn, RecordLine as st, RELATIVE as t, CountBand as tn, zoneOf as tr, Endpoint as tt, TimeSpread as u, BacklogTerm as un, Sessions as ut, EnvironmentManifest as v, KubeletProbe as vn, byNode as vt, KindCellV1 as w, Prediction as wn, isTunnelState as wt, KindCellRecord as x, MeshMode as xn, emptyOf as xt, FORMAT as y, Mesh as yn, deletionOf as yt, Propagated as z, KindScenario as zn, sliceEmptyOf as zt };
1337
+ //# sourceMappingURL=judge-B_wVAwjx.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { format, valueIn } from "@mark1russell7/quantity";
2
+ import { median, medianInterval, quantileSorted } from "@mark1russell7/rigor-statistics";
2
3
  import { extentSchema, quantitySchema } from "@mark1russell7/quantity/schema";
3
4
  import { z } from "zod";
4
5
  //#region src/bands.ts
@@ -26,13 +27,38 @@ const showBand = (band) => {
26
27
  /** This function rounds a time to the microsecond, thus a band has no float residue. */
27
28
  const us = (value) => Math.round(value * 1e6) / 1e6;
28
29
  /**
30
+ * This function gives the largest time after the band end at which the window of n sessions closes,
31
+ * in seconds. Each session drains in d, thus it is n × d.hi. With `slowest` (SYS-45), one of the n
32
+ * sessions can drain up to that time, thus it is (n − 1) × d.hi + the larger of `slowest` and d.hi.
33
+ */
34
+ const closingOf = (term, sessions) => {
35
+ const hi = valueIn(term.drain.hi, "s");
36
+ if (term.slowest === void 0 || sessions < 1) return sessions * hi;
37
+ return (sessions - 1) * hi + Math.max(valueIn(term.slowest, "s"), hi);
38
+ };
39
+ /**
29
40
  * This function gives the end band of a window with the backlog term (`BacklogTerm`). It adds n × d
30
41
  * to each end of `end`, the end band without the term. The value n is the sessions on the target.
42
+ * A term with `slowest` (SYS-45) adds (n − 1) × d.hi + `slowest` to the high end instead.
31
43
  */
32
44
  const backlogEnd = (end, term, sessions) => {
33
45
  const ends = secondsOf(end);
34
46
  const drain = secondsOf(term.drain);
35
- return bandOf(us(ends.lo + sessions * drain.lo), us(ends.hi + sessions * drain.hi));
47
+ return bandOf(us(ends.lo + sessions * drain.lo), us(ends.hi + closingOf(term, sessions)));
48
+ };
49
+ /**
50
+ * This function gives the band of the start of the last failed request of a window with the backlog
51
+ * term, from the band end (the high end of `end`). This is the overshoot of a run. Its high end is
52
+ * the high end of `backlogEnd`. Without `early`, its low end is n × d.lo (SYS-42). With `early`
53
+ * (SYS-45), the last failed request can start up to one turn before the window closes. Its low end
54
+ * is then the low end of `end` plus n × d.lo. The end band opens one turn before the band end.
55
+ */
56
+ const overshootBand = (end, term, sessions) => {
57
+ const ends = secondsOf(end);
58
+ const closes = secondsOf(backlogEnd(end, term, sessions));
59
+ const drain = secondsOf(term.drain);
60
+ const lo = term.early === true ? closes.lo - ends.hi : sessions * drain.lo;
61
+ return bandOf(us(lo), us(closes.hi - ends.hi));
36
62
  };
37
63
  /** This function gives a count rounded to six places, thus 10 s / 20 ms gives 500 and not 499.99999. */
38
64
  const counted = (value) => Math.round(value * 1e6) / 1e6;
@@ -47,13 +73,14 @@ const turnOf = (mean, connections) => {
47
73
  * request each turn until the window closes. With n sessions on the target, the window closes up to
48
74
  * n × d.hi after the band end (`backlogEnd`). Thus one session fails 0 to ceil(n × d.hi / turn)
49
75
  * requests in the drain. The turn is the mean period of a client (`mean`) times its sessions
50
- * (`connections`). The term r is not in this band.
76
+ * (`connections`). The term r is not in this band. With `slowest` (SYS-45), the window closes up to
77
+ * (n − 1) × d.hi + `slowest` after the band end. Thus one session fails up to that time over the turn.
51
78
  */
52
79
  const drainBand = (term, mean, connections, sessions) => {
53
80
  const turn = turnOf(mean, connections);
54
81
  return {
55
82
  lo: 0,
56
- hi: Math.ceil(counted(sessions * valueIn(term.drain.hi, "s") / turn))
83
+ hi: Math.ceil(counted(closingOf(term, sessions) / turn))
57
84
  };
58
85
  };
59
86
  /**
@@ -69,7 +96,10 @@ const requestBand = (term, span, mean, connections, sessions) => {
69
96
  const turn = turnOf(mean, connections);
70
97
  const each = counted(valueIn(span, "s") / turn);
71
98
  if (term.drains === true && term.drain === void 0) throw new RangeError("a term with drains has no drain time d");
72
- const drained = term.drains === true ? drainBand({ drain: term.drain }, mean, connections, sessions) : {
99
+ const drained = term.drains === true ? drainBand({
100
+ drain: term.drain,
101
+ slowest: term.slowest
102
+ }, mean, connections, sessions) : {
73
103
  lo: 0,
74
104
  hi: 0
75
105
  };
@@ -90,7 +120,36 @@ const SERVER = "lab";
90
120
  const CLIENT = "client";
91
121
  const REDIS = "redis";
92
122
  //#endregion
123
+ //#region src/stats.ts
124
+ /** This fraction, δ_rel, is how far the median of a cell can be outside its band and stay equivalent. */
125
+ const RELATIVE = .05;
126
+ /**
127
+ * This function gives the median of sorted values: the middle value, or the mean of the two middle values.
128
+ * It is `median` of @mark1russell7/rigor-statistics, which throws a RangeError for an empty list.
129
+ */
130
+ const middle = (sorted) => median(sorted);
131
+ /**
132
+ * This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). It is
133
+ * `quantileSorted` of @mark1russell7/rigor-statistics with the method `"nearest-rank"`.
134
+ */
135
+ const quantileOf = (sorted, p) => quantileSorted(sorted, p, "nearest-rank");
136
+ /**
137
+ * This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
138
+ * largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
139
+ * at 98.8 %. It is `medianInterval` of @mark1russell7/rigor-statistics at the level 0.95. An
140
+ * empty list throws a RangeError.
141
+ */
142
+ const medianInterval$1 = (sorted) => {
143
+ const interval = medianInterval(sorted, .95);
144
+ return [interval.low, interval.high];
145
+ };
146
+ //#endregion
93
147
  //#region src/observe.ts
148
+ /**
149
+ * This module reads a run from its lines. Each time is in milliseconds on CLOCK_MONOTONIC. All the
150
+ * containers of the VM share that clock: the clients, the service, the agents, the injector, the
151
+ * watcher and the prober.
152
+ */
94
153
  /** This port is the HBONE port of ztunnel in each pod of the ambient mesh of Istio 1.31.3: the mesh leg ends there. */
95
154
  const HBONE_PORT = 15008;
96
155
  /** These are the states of a TCP socket of Linux that a tunnel line names (include/net/tcp_states.h of Linux v6.17, lines 12–28). */
@@ -152,10 +211,6 @@ const sessionsAt = (lines, at, target, connections) => {
152
211
  byClient
153
212
  };
154
213
  };
155
- const medianOf = (sorted) => {
156
- const middle = Math.floor(sorted.length / 2);
157
- return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
158
- };
159
214
  /**
160
215
  * This function gives the measured period of the clients from their request lines that start from
161
216
  * `from` to `to`. The timer of a client is late by a different amount at each tick. Thus the median
@@ -181,12 +236,12 @@ const periodOf = (lines, from, to) => {
181
236
  const intervals = mine.slice(1).map((t, at) => t - mine[at]);
182
237
  span += mine.at(-1) - mine[0];
183
238
  all.push(...intervals);
184
- byClient[client] = medianOf(intervals.sort((a, b) => a - b));
239
+ byClient[client] = middle(intervals.sort((a, b) => a - b));
185
240
  }
186
241
  if (all.length === 0) return null;
187
242
  all.sort((a, b) => a - b);
188
243
  return {
189
- median: medianOf(all),
244
+ median: middle(all),
190
245
  mean: span / all.length,
191
246
  low: all[0],
192
247
  high: all.at(-1),
@@ -295,38 +350,6 @@ const restartsAt = (lines, at) => {
295
350
  return out;
296
351
  };
297
352
  //#endregion
298
- //#region src/stats.ts
299
- /** This fraction, δ_rel, is how far the median of a cell can be outside its band and stay equivalent. */
300
- const RELATIVE = .05;
301
- /** This function gives the median of sorted values: the middle value, or the mean of the two middle values. */
302
- const middle = (sorted) => {
303
- const half = Math.floor(sorted.length / 2);
304
- return sorted.length % 2 === 1 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
305
- };
306
- /** This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). */
307
- const quantileOf = (sorted, p) => sorted[Math.min(sorted.length, Math.max(1, Math.ceil(p * sorted.length))) - 1];
308
- /** This function gives P(X ≤ j) for X ~ Binomial(n, 1/2), exactly. */
309
- const below = (n, j) => {
310
- let term = 1;
311
- let sum = 0;
312
- for (let i = 0; i <= j; i += 1) {
313
- sum += term;
314
- term = term * (n - i) / (i + 1);
315
- }
316
- return sum / 2 ** n;
317
- };
318
- /**
319
- * This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
320
- * largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
321
- * at 98.8 %.
322
- */
323
- const medianInterval = (sorted) => {
324
- const n = sorted.length;
325
- let k = 0;
326
- while (k + 1 <= n / 2 && below(n, k) <= .025) k += 1;
327
- return k === 0 ? [sorted[0], sorted[n - 1]] : [sorted[k - 1], sorted[n - k]];
328
- };
329
- //#endregion
330
353
  //#region src/verdict.ts
331
354
  /**
332
355
  * This time is δ for kind: each time of a run lands in its band ± δ. Its parts are the interval of
@@ -482,7 +505,8 @@ const windowOf = (prediction) => prediction.kind === "window" ? prediction : pre
482
505
  const observationAfter = (cell) => {
483
506
  const window = windowOf(cell.prediction);
484
507
  if (window === null || window.end === null) return null;
485
- const drained = window.backlog === void 0 ? 0 : window.backlog.most * valueIn(window.backlog.drain.hi, "ms");
508
+ const backlog = window.backlog;
509
+ const drained = backlog === void 0 ? 0 : backlog.slowest === void 0 || backlog.most < 1 ? backlog.most * valueIn(backlog.drain.hi, "ms") : (backlog.most - 1) * valueIn(backlog.drain.hi, "ms") + Math.max(valueIn(backlog.slowest, "ms"), valueIn(backlog.drain.hi, "ms"));
486
510
  return seconds(sec(valueIn(window.end.band.hi, "ms") + drained + valueIn(cell.workload.timeout, "ms") + NOISE));
487
511
  };
488
512
  /**
@@ -1171,7 +1195,7 @@ const judgeKind = (cell, runs) => {
1171
1195
  };
1172
1196
  if (band !== null && values.length > 0) {
1173
1197
  const ends = secondsOf(band);
1174
- const [from, to] = medianInterval(values);
1198
+ const [from, to] = medianInterval$1(values);
1175
1199
  const wide = {
1176
1200
  lo: ends.lo * .95,
1177
1201
  hi: ends.hi * 1.05
@@ -1236,6 +1260,15 @@ const upgradeTrigger = (trigger) => {
1236
1260
  }
1237
1261
  };
1238
1262
  };
1263
+ /** The format `/1` has no backlog term, but its type does. Each time of the term is in seconds. */
1264
+ const upgradeBacklog = (backlog) => {
1265
+ const { slowest, ...rest } = backlog;
1266
+ return {
1267
+ ...rest,
1268
+ drain: band(backlog.drain),
1269
+ ...slowest === void 0 ? {} : { slowest: seconds(slowest) }
1270
+ };
1271
+ };
1239
1272
  const upgradeWindow = (window) => {
1240
1273
  const { afterReset, propagation, backlog, ...rest } = window;
1241
1274
  return {
@@ -1253,10 +1286,7 @@ const upgradeWindow = (window) => {
1253
1286
  outcome: afterReset.outcome
1254
1287
  } },
1255
1288
  ...propagation === void 0 ? {} : { propagation: band(propagation) },
1256
- ...backlog === void 0 ? {} : { backlog: {
1257
- ...backlog,
1258
- drain: band(backlog.drain)
1259
- } }
1289
+ ...backlog === void 0 ? {} : { backlog: upgradeBacklog(backlog) }
1260
1290
  };
1261
1291
  };
1262
1292
  const upgradePrediction = (prediction) => {
@@ -1385,8 +1415,7 @@ const judge = (record) => {
1385
1415
  const timeSpread = (times) => {
1386
1416
  if (times.length === 0) return null;
1387
1417
  const sorted = times.map((each) => valueIn(each, "s")).sort((a, b) => a - b);
1388
- const half = Math.floor(sorted.length / 2);
1389
- const median = sorted.length % 2 === 1 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
1418
+ const median = middle(sorted);
1390
1419
  return {
1391
1420
  count: sorted.length,
1392
1421
  min: seconds(sorted[0]),
@@ -1404,7 +1433,7 @@ const atBand = (from, band) => {
1404
1433
  const predictionText = (prediction) => {
1405
1434
  switch (prediction.kind) {
1406
1435
  case "none": return prediction.outcomes === void 0 ? "no failure" : `no ${prediction.outcomes.join(", ")}`;
1407
- case "window": return `failures only from ${atBand(prediction.start.from, prediction.start.band)}${prediction.end === null ? "" : ` to ${atBand(prediction.end.from, prediction.end.band)}`}${prediction.each === true ? ", of each deleted replica" : ""}${prediction.flaps === true ? ", in each cycle" : ""}${prediction.nodes === "paused" ? ", on the paused node" : ""}${prediction.certain ? ", at least one" : ""}${prediction.outcomes === void 0 ? "" : `, each one ${prediction.outcomes.join(" or ")}`}${prediction.afterReset === void 0 ? "" : `, or a ${prediction.afterReset.outcome} up to ${format(prediction.afterReset.life)} after a reset on its port`}${prediction.propagation === void 0 ? "" : `, the rules drop the replica in ${showBand(prediction.propagation)}`}${prediction.backlog === void 0 ? "" : `, the end n × ${showBand(prediction.backlog.drain)} later and ${prediction.backlog.requests.lo}–${prediction.backlog.requests.hi} more failures for each of the n sessions on the target${prediction.backlog.drains === true ? ", and its failures each turn in the drain of the n sessions" : ""}, counted at the measured period`}`;
1436
+ case "window": return `failures only from ${atBand(prediction.start.from, prediction.start.band)}${prediction.end === null ? "" : ` to ${atBand(prediction.end.from, prediction.end.band)}`}${prediction.each === true ? ", of each deleted replica" : ""}${prediction.flaps === true ? ", in each cycle" : ""}${prediction.nodes === "paused" ? ", on the paused node" : ""}${prediction.certain ? ", at least one" : ""}${prediction.outcomes === void 0 ? "" : `, each one ${prediction.outcomes.join(" or ")}`}${prediction.afterReset === void 0 ? "" : `, or a ${prediction.afterReset.outcome} up to ${format(prediction.afterReset.life)} after a reset on its port`}${prediction.propagation === void 0 ? "" : `, the rules drop the replica in ${showBand(prediction.propagation)}`}${prediction.backlog === void 0 ? "" : `, the end n × ${showBand(prediction.backlog.drain)} later and ${prediction.backlog.requests.lo}–${prediction.backlog.requests.hi} more failures for each of the n sessions on the target${prediction.backlog.drains === true ? ", and its failures each turn in the drain of the n sessions" : ""}, counted at the measured period${prediction.backlog.slowest === void 0 ? "" : `, one session draining up to ${format(prediction.backlog.slowest)}`}${prediction.backlog.early === true ? ", the last failure up to one turn before the end" : ""}`}`;
1408
1437
  case "removal": return `Service empty in ${showBand(prediction.band)}${prediction.flaps === true ? " after each fault, and ready again before the next" : ""}${prediction.window === void 0 ? "" : `; ${predictionText(prediction.window)}`}`;
1409
1438
  case "leaves": return `the replica leaves in ${showBand(prediction.band)}${prediction.window === void 0 ? "" : `; ${predictionText(prediction.window)}`}`;
1410
1439
  case "restarts": return prediction.band === null ? "no restart" : `each replica SIGTERM in ${showBand(prediction.band)}`;
@@ -1711,7 +1740,9 @@ const recordOf = (a, main) => {
1711
1740
  hi: count
1712
1741
  }),
1713
1742
  most: count,
1714
- drains: z.boolean().optional()
1743
+ drains: z.boolean().optional(),
1744
+ slowest: a.optional(),
1745
+ early: z.boolean().optional()
1715
1746
  }).optional(),
1716
1747
  why: z.string()
1717
1748
  });
@@ -1938,6 +1969,6 @@ const readRecord = (data) => {
1938
1969
  throw new Error(`a kind cell record has the format ${FORMAT} or ${FORMAT_V1}, not ${String(format)}`);
1939
1970
  };
1940
1971
  //#endregion
1941
- export { restartsAt as $, RELATIVE as A, dropOf as B, observationEnd as C, drainBand as Ct, reusesReset as D, showBand as Dt, referenceOf as E, secondsOf as Et, TCP_STATE as F, kernelCounts as G, failuresOf as H, backendsAt as I, podsAt as J, outcomesOf as K, byClients as L, middle as M, quantileOf as N, unitOf as O, HBONE_PORT as P, restartOf as Q, byNode as R, observationAfter as S, bandOf as St, propagationSource as T, seconds as Tt, intakeOf as U, emptyOf as V, isTunnelState as W, recordsOf as X, podsOf as Y, requestsOf as Z, checkRun as _, LOG_LABEL as _t, calibrationOf as a, sliceEmptyOf as at, factsOf as b, SERVER as bt, predictionText as c, slicesOf as ct, FORMAT_V1 as d, transitionOf as dt, rulesOf as et, judge as f, tunnelsOf as ft, LAST_SLACK as g, HEALTH_PORT as gt, KIND_NOISE as h, CLIENT as ht, readRecord as i, sliceDropOf as it, medianInterval as j, updatesOf as k, timeSpread as l, socketsOf as lt, upgradeCell as m, ztunnelReplaced as mt, kindCellRecordSchema as n, signalOf as nt, cellsTable as o, sliceReadyAt as ot, upgrade as p, ztunnelPodsOf as pt, periodOf as q, kindCellRecordV1Schema as r, signalsOf as rt, medianText as s, sliceReadyOf as st, STAGES as t, sessionsAt as tt, FORMAT as u, stoppingOf as ut, cyclesOf as v, PORT as vt, propagationOf as w, requestBand as wt, judgeKind as x, backlogEnd as xt, deletionsOf as y, REDIS as yt, deletionOf as z };
1972
+ export { signalsOf as $, HBONE_PORT as A, isTunnelState as B, observationEnd as C, drainBand as Ct, reusesReset as D, secondsOf as Dt, referenceOf as E, seconds as Et, deletionOf as F, podsOf as G, outcomesOf as H, dropOf as I, restartOf as J, recordsOf as K, emptyOf as L, backendsAt as M, byClients as N, unitOf as O, showBand as Ot, byNode as P, signalOf as Q, failuresOf as R, observationAfter as S, bandOf as St, propagationSource as T, requestBand as Tt, periodOf as U, kernelCounts as V, podsAt as W, rulesOf as X, restartsAt as Y, sessionsAt as Z, checkRun as _, LOG_LABEL as _t, calibrationOf as a, socketsOf as at, factsOf as b, SERVER as bt, predictionText as c, tunnelsOf as ct, FORMAT_V1 as d, RELATIVE as dt, sliceDropOf as et, judge as f, medianInterval$1 as ft, LAST_SLACK as g, HEALTH_PORT as gt, KIND_NOISE as h, CLIENT as ht, readRecord as i, slicesOf as it, TCP_STATE as j, updatesOf as k, timeSpread as l, ztunnelPodsOf as lt, upgradeCell as m, quantileOf as mt, kindCellRecordSchema as n, sliceReadyAt as nt, cellsTable as o, stoppingOf as ot, upgrade as p, middle as pt, requestsOf as q, kindCellRecordV1Schema as r, sliceReadyOf as rt, medianText as s, transitionOf as st, STAGES as t, sliceEmptyOf as tt, FORMAT as u, ztunnelReplaced as ut, cyclesOf as v, PORT as vt, propagationOf as w, overshootBand as wt, judgeKind as x, backlogEnd as xt, deletionsOf as y, REDIS as yt, intakeOf as z };
1942
1973
 
1943
- //# sourceMappingURL=judge-45z0yZ-r.js.map
1974
+ //# sourceMappingURL=judge-Bc84w-no.js.map