@mark1russell7/harness 0.15.0 → 0.15.2

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.
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ProxyHop, A as ImageSource, At as Subject, B as LoadStream, C as Hop, Ct as RunTopology, D as HopWrite, Dt as ShippedImage, E as HopSignal, Et as Shipped, F as LineCount, Ft as isHopPlace, G as Outcomes, H as MiddleboxExpectation, I as LineMatch, It as latestEndOf, J as PausePrediction, K as Pair, L as LoadBand, M as Kafka, Mt as Topology, N as KafkaPause, Nt as Toxic, O as HostSysctl, Ot as Stage, P as KernelRecord, Pt as WsProbe, Q as ProbeEvent, R as LoadPlan, S as GroupRole, St as RunStream, T as HopPlace, Tt as SentinelStage, U as MiddleboxForm, V as MiddleboxEvent, W as Observer, X as PortsPrediction, Y as PinnedImage, Z as Prediction, _ as Fault, _t as RunFabric, a as ClientCapability, at as Reconnects, b as GroupPlace, bt as RunPause, c as ClusterClientAt, ct as ReplicaSet, d as CountBand, dt as RequestPrediction, et as ProxyTime, f as Drill, ft as RouterHop, g as FabricManifest, gt as RunDrill, h as Fabric, ht as RunBackup, i as Client, it as QueuePrediction, j as Jitter, jt as Target, k as HttpProbe, kt as StageBand, l as ClusterStage, lt as ReplicaSetStage, m as DriverStep, mt as Run, n as CLIENT_CAPABILITIES, nt as QueueConnection, o as ClientImage, ot as Recovers, p as DriverBand, pt as RttRange, q as PauseBand, r as Cell, rt as QueueEnd, s as Cluster, st as Reference, t as BuildImage, tt as QueueBand, u as ConntrackPrediction, ut as Replies, v as FaultPlace, vt as RunGroup, w as HopEvent, wt as SentinelEvent, x as GroupPrediction, xt as RunRoute, y as Group, yt as RunPart, z as LoadPrediction } from "./cell-biffSiEJ.js";
1
+ import { $ as ProxyHop, A as ImageSource, At as Subject, B as LoadStream, C as Hop, Ct as RunTopology, D as HopWrite, Dt as ShippedImage, E as HopSignal, Et as Shipped, F as LineCount, Ft as environmentProblems, G as Outcomes, H as MiddleboxExpectation, I as LineMatch, It as isHopPlace, J as PausePrediction, K as Pair, L as LoadBand, Lt as latestEndOf, M as Kafka, Mt as Topology, N as KafkaPause, Nt as Toxic, O as HostSysctl, Ot as Stage, P as KernelRecord, Pt as WsProbe, Q as ProbeEvent, R as LoadPlan, S as GroupRole, St as RunStream, T as HopPlace, Tt as SentinelStage, U as MiddleboxForm, V as MiddleboxEvent, W as Observer, X as PortsPrediction, Y as PinnedImage, Z as Prediction, _ as Fault, _t as RunFabric, a as ClientCapability, at as Reconnects, b as GroupPlace, bt as RunPause, c as ClusterClientAt, ct as ReplicaSet, d as CountBand, dt as RequestPrediction, et as ProxyTime, f as Drill, ft as RouterHop, g as FabricManifest, gt as RunDrill, h as Fabric, ht as RunBackup, i as Client, it as QueuePrediction, j as Jitter, jt as Target, k as HttpProbe, kt as StageBand, l as ClusterStage, lt as ReplicaSetStage, m as DriverStep, mt as Run, n as CLIENT_CAPABILITIES, nt as QueueConnection, o as ClientImage, ot as Recovers, p as DriverBand, pt as RttRange, q as PauseBand, r as Cell, rt as QueueEnd, s as Cluster, st as Reference, t as BuildImage, tt as QueueBand, u as ConntrackPrediction, ut as Replies, v as FaultPlace, vt as RunGroup, w as HopEvent, wt as SentinelEvent, x as GroupPrediction, xt as RunRoute, y as Group, yt as RunPart, z as LoadPrediction } from "./cell-OwQdpSkd.js";
2
2
  import { Quantity } from "@mark1russell7/quantity";
3
3
  import { LoadMeasure, Schedule } from "@mark1russell7/rigor-chaos";
4
4
  import { StartedTestContainer } from "testcontainers";
@@ -181,6 +181,35 @@ export declare const streamsLoadOf: (events: readonly ProbeEvent[], streams: rea
181
181
  readonly pool: RunLoad;
182
182
  readonly streams: Readonly<Record<string, RunLoad>>;
183
183
  } | null;
184
+ /** This function gives the pool of some measures: it sums the counts and joins the times, in the order of the measures. */
185
+ export declare const poolOf: (measures: readonly RunLoad[]) => RunLoad & {
186
+ readonly failed: number;
187
+ };
188
+ /**
189
+ * This time is the tolerance of a closed-loop estimate, in ms. A send later than its expected time
190
+ * by more than this tolerance is a pause of the client: a driver gap.
191
+ */
192
+ export declare const CLOSED_TOLERANCE = 5;
193
+ /**
194
+ * A run estimate is the latency of the open loop that a closed-loop stream hid (SYS-91). The package
195
+ * rigor-chaos puts back each send that a stall or a pause of the client hid (`closedLoopPlan`).
196
+ * `hidden` counts those sends, and `driverGaps` counts the pauses of the client. Its times are in ms.
197
+ */
198
+ type RunEstimate = LoadMeasure & {
199
+ readonly hidden: number;
200
+ readonly driverGaps: number;
201
+ readonly driverGapTotal: number;
202
+ };
203
+ /**
204
+ * This function gives the estimate of one closed-loop stream from its requests, in the order of their
205
+ * sends. Each time is a time of a line of the client. The client waits `interval` ms after each reply,
206
+ * thus the interval is also the pause of the driver. The function gives undefined for no request, and
207
+ * for a request with no reply before the last request.
208
+ */
209
+ export declare const estimateOf: (requests: ReadonlyArray<{
210
+ readonly sent: number;
211
+ readonly done: number | null;
212
+ }>, end: number, interval: number) => RunEstimate | undefined;
184
213
  /** A load spread is a part of the distribution of one measure of an open-loop cell, over its counted runs. */
185
214
  interface LoadSpread {
186
215
  readonly p50: Quantity;
@@ -224,6 +253,22 @@ export declare const loadVerdict: (prediction: LoadPrediction, runs: ReadonlyArr
224
253
  readonly name: string;
225
254
  readonly load: RunLoad | undefined;
226
255
  }>) => LoadJudgement;
256
+ /**
257
+ * An estimate verdict sums the closed-loop estimates of a cell over its counted runs. Its latency
258
+ * is an estimate of the open loop, not a measure of it: a record of an open-loop client stays the
259
+ * better measure. Each hidden send after a request with no reply counts at its age at the end.
260
+ */
261
+ interface EstimateVerdict {
262
+ readonly planned: number;
263
+ readonly hidden: number;
264
+ readonly driverGaps: number;
265
+ readonly driverGapTotal: Quantity;
266
+ readonly latency: LoadSpread | null;
267
+ /** This text gives the estimate in words, for the verdict of the cell. */
268
+ readonly words: string;
269
+ }
270
+ /** This function gives the estimate verdict of the counted runs, or undefined when no run has an estimate. */
271
+ export declare const estimateVerdict: (estimates: ReadonlyArray<RunEstimate | undefined>) => EstimateVerdict | undefined;
227
272
  //#endregion
228
273
  //#region src/driver.d.ts
229
274
  /**
@@ -785,6 +830,8 @@ interface Verdict {
785
830
  readonly reordering?: Reordering;
786
831
  /** This record is the load of an open-loop cell over its counted runs. */
787
832
  readonly load?: LoadVerdict;
833
+ /** This record is the estimate of the open loop of a closed-loop stream over its counted runs (SYS-91). */
834
+ readonly estimate?: EstimateVerdict;
788
835
  /** This record is the time of the proxy of a calibration cell, in each direction. */
789
836
  readonly proxy?: {
790
837
  readonly hop: string;
@@ -974,6 +1021,8 @@ interface Measured {
974
1021
  readonly samples?: readonly Quantity[];
975
1022
  /** This measure is the run of an open-loop cell against its schedule (`Run.schedule`), with its failed replies. Its times are in ms. */
976
1023
  readonly load?: RunLoad;
1024
+ /** This estimate is the open loop that a closed-loop stream of the client of the harness hid (SYS-91). Its times are in ms. */
1025
+ readonly estimate?: RunEstimate;
977
1026
  /** This count is the number of keepalive probes that the kernel sent. */
978
1027
  readonly probes: number;
979
1028
  /** This count is the number of segments that the kernel sent again. */
@@ -1415,9 +1464,11 @@ export declare const targetImageOf: (target: Target) => Promise<string>;
1415
1464
  * A lab in a guest of QEMU writes E4 (`substrateHere`). The other layers do not have a runner at this
1416
1465
  * time.
1417
1466
  */
1418
- type Layer = "E0" | "E1" | "E2" | "E3" | "E4" | "E5" | "E6";
1467
+ export declare const LAYERS: readonly ["E0", "E1", "E2", "E3", "E4", "E5", "E6"];
1468
+ type Layer = (typeof LAYERS)[number];
1419
1469
  /** A cloud is where the runner operates. GitHub can schedule a standard Linux job on Azure or on AWS. */
1420
- type Cloud = "azure" | "aws" | "unknown";
1470
+ export declare const CLOUDS: readonly ["azure", "aws", "unknown"];
1471
+ type Cloud = (typeof CLOUDS)[number];
1421
1472
  /** A host is what a process reads about its own machine. */
1422
1473
  interface Host {
1423
1474
  /** This text is the kernel release. */
@@ -2650,6 +2701,12 @@ export declare const QUIET_MS = 250;
2650
2701
  export declare const LINGER_MS = 1e3;
2651
2702
  /** This function divides a count of runs across environments, as equally as possible. */
2652
2703
  export declare const spread: (runs: number, environments: number) => number[];
2704
+ /**
2705
+ * This function gives the count of runs of each new environment of a cell, for `runs` more runs. A
2706
+ * pair cell has an environment for each run, because its run promotes the standby. Other cells
2707
+ * go across their environments, and at least one for each environment at the same time.
2708
+ */
2709
+ export declare const environmentCounts: (cell: Cell, runs: number) => number[];
2653
2710
  /**
2654
2711
  * This function does the runs of a cell. They go across new environments, and each environment records
2655
2712
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
@@ -2661,5 +2718,5 @@ export declare const spread: (runs: number, environments: number) => number[];
2661
2718
  */
2662
2719
  export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
2663
2720
  //#endregion
2664
- export { type Agent, type BuildImage, type BuildRecord, CLIENT_CAPABILITIES, type CallReading, type Cell, type CellResult, type Client, type ClientCapability, type ClientImage, type ClockPair, type Cloud, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, type ConntrackPrediction, type ConntrackReading, type ContextFile, type CountBand, type Crossings, type Cut, type Decision, type Drill, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fabric, type FabricLab, type FabricManifest, type FabricParts, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type Hop, type HopEvent, type HopPlace, type HopSignal, type HopWrite, type Host, type HostSetting, type HostSysctl, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type Jitter, type Kafka, type KafkaLab, type KafkaMember, type KafkaPause, type KernelRecord, type Lab, type Layer, type LeafViews, type LineCount, type LineMatch, type LoadBand, type LoadJudgement, type LoadPlan, type LoadPrediction, type LoadSpread, type LoadStream, type LoadVerdict, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type NodeLine, type Observer, type OplogRead, type OplogTime, type Outcome, type Outcomes, type Pair, type PairSets, type Pairing, type PartitionView, type PauseBand, type PauseCommit, type PausePrediction, type PauseReading, type Pinnable, type PinnedImage, type PortsPrediction, type PortsReading, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type QueueBand, type QueueConnection, type QueueConnectionReading, type QueueEnd, type QueuePrediction, type QueueReading, type ReconnectReading, type ReconnectVerdict, type Reconnects, type RecordCode, type Recovers, type RecoveryMeasured, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouteLine, type RouterHop, type RttRange, type Run, type RunBackup, type RunDrill, type RunFabric, type RunGroup, type RunLoad, type RunPart, type RunPause, type RunRoute, type RunStream, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type Sibling, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type StallReading, type Stamp, type Status, type StatusRow, type Subject, type Substrate, type SubstrateSource, type Target, type Topology, type Toxic, type Verdict, type WallStamp, type Writes, type WsProbe, isHopPlace, latestEndOf };
2721
+ export { type Agent, type BuildImage, type BuildRecord, CLIENT_CAPABILITIES, type CallReading, type Cell, type CellResult, type Client, type ClientCapability, type ClientImage, type ClockPair, type Cloud, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, type ConntrackPrediction, type ConntrackReading, type ContextFile, type CountBand, type Crossings, type Cut, type Decision, type Drill, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type EstimateVerdict, type Fabric, type FabricLab, type FabricManifest, type FabricParts, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type Hop, type HopEvent, type HopPlace, type HopSignal, type HopWrite, type Host, type HostSetting, type HostSysctl, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type Jitter, type Kafka, type KafkaLab, type KafkaMember, type KafkaPause, type KernelRecord, type Lab, type Layer, type LeafViews, type LineCount, type LineMatch, type LoadBand, type LoadJudgement, type LoadPlan, type LoadPrediction, type LoadSpread, type LoadStream, type LoadVerdict, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type NodeLine, type Observer, type OplogRead, type OplogTime, type Outcome, type Outcomes, type Pair, type PairSets, type Pairing, type PartitionView, type PauseBand, type PauseCommit, type PausePrediction, type PauseReading, type Pinnable, type PinnedImage, type PortsPrediction, type PortsReading, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type QueueBand, type QueueConnection, type QueueConnectionReading, type QueueEnd, type QueuePrediction, type QueueReading, type ReconnectReading, type ReconnectVerdict, type Reconnects, type RecordCode, type Recovers, type RecoveryMeasured, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouteLine, type RouterHop, type RttRange, type Run, type RunBackup, type RunDrill, type RunEstimate, type RunFabric, type RunGroup, type RunLoad, type RunPart, type RunPause, type RunRoute, type RunStream, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type Sibling, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type StallReading, type Stamp, type Status, type StatusRow, type Subject, type Substrate, type SubstrateSource, type Target, type Topology, type Toxic, type Verdict, type WallStamp, type Writes, type WsProbe, environmentProblems, isHopPlace, latestEndOf };
2665
2722
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { A as unlockedManifests, B as isHopPlace, C as programOf, D as subjectProblems, E as subjectClient, F as jarVersions, H as requestFirst, I as locksOf, L as npmVersions, M as LOCK_NAMES, N as cargoVersions, O as targetImageOf, P as goVersions, R as versionsOf, S as planOf, T as shippedSource, V as latestEndOf, _ as kafkaClientImage, a as SHIPPED_NAMES, b as nodeHttpImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as unpinnedBases, k as targetSourceOf, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as mongoClientImage, w as routerImage, x as pgClientImage, y as nodeClientImage, z as CLIENT_CAPABILITIES } from "./image-BxVTFZ1o.js";
1
+ import { A as unlockedManifests, B as environmentProblems, C as programOf, D as subjectProblems, E as subjectClient, F as jarVersions, H as latestEndOf, I as locksOf, L as npmVersions, M as LOCK_NAMES, N as cargoVersions, O as targetImageOf, P as goVersions, R as versionsOf, S as planOf, T as shippedSource, U as requestFirst, V as isHopPlace, _ as kafkaClientImage, a as SHIPPED_NAMES, b as nodeHttpImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as unpinnedBases, k as targetSourceOf, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as mongoClientImage, w as routerImage, x as pgClientImage, y as nodeClientImage, z as CLIENT_CAPABILITIES } from "./image-B8yh_FF7.js";
2
2
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
3
  import { median, medianInterval as medianInterval$1, quantile, quantileSorted } from "@mark1russell7/rigor-statistics";
4
4
  import { rungVerdict } from "@mark1russell7/rigor-bench";
5
- import { measureLoad, schedule, scheduleHeld } from "@mark1russell7/rigor-chaos";
5
+ import { closedLoopPlan, measureLoad, schedule, scheduleHeld } from "@mark1russell7/rigor-chaos";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
7
  import { execFile } from "node:child_process";
8
8
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -99,21 +99,56 @@ const streamsLoadOf = (events, streams) => {
99
99
  const start = events.find((event) => event.ev === "scheduled")?.t;
100
100
  if (start === void 0) return null;
101
101
  const each = streams.map((stream) => [stream.key, measureOf(events, start, stream.schedule, stream.key)]);
102
- const measures = each.map(([, measure]) => measure);
102
+ return {
103
+ pool: poolOf(each.map(([, measure]) => measure)),
104
+ streams: Object.fromEntries(each)
105
+ };
106
+ };
107
+ /** This function gives the pool of some measures: it sums the counts and joins the times, in the order of the measures. */
108
+ const poolOf = (measures) => {
103
109
  const sum = (count) => measures.reduce((total, measure) => total + count(measure), 0);
104
110
  const join = (times) => measures.flatMap(times);
105
111
  return {
106
- pool: {
107
- planned: sum((measure) => measure.planned),
108
- unsent: sum((measure) => measure.unsent),
109
- unfinished: sum((measure) => measure.unfinished),
110
- failed: sum((measure) => measure.failed ?? 0),
111
- latency: join((measure) => measure.latency),
112
- service: join((measure) => measure.service),
113
- lag: join((measure) => measure.lag),
114
- censored: join((measure) => measure.censored)
115
- },
116
- streams: Object.fromEntries(each)
112
+ planned: sum((measure) => measure.planned),
113
+ unsent: sum((measure) => measure.unsent),
114
+ unfinished: sum((measure) => measure.unfinished),
115
+ failed: sum((measure) => measure.failed ?? 0),
116
+ latency: join((measure) => measure.latency),
117
+ service: join((measure) => measure.service),
118
+ lag: join((measure) => measure.lag),
119
+ censored: join((measure) => measure.censored)
120
+ };
121
+ };
122
+ /**
123
+ * This time is the tolerance of a closed-loop estimate, in ms. A send later than its expected time
124
+ * by more than this tolerance is a pause of the client: a driver gap.
125
+ */
126
+ const CLOSED_TOLERANCE = 5;
127
+ /**
128
+ * This function gives the estimate of one closed-loop stream from its requests, in the order of their
129
+ * sends. Each time is a time of a line of the client. The client waits `interval` ms after each reply,
130
+ * thus the interval is also the pause of the driver. The function gives undefined for no request, and
131
+ * for a request with no reply before the last request.
132
+ */
133
+ const estimateOf = (requests, end, interval) => {
134
+ const start = requests[0]?.sent;
135
+ if (start === void 0 || requests.slice(0, -1).some((request) => request.done === null)) return void 0;
136
+ const loop = requests.map((request) => ({
137
+ sent: fromStart(start, request.sent),
138
+ done: request.done === null ? null : fromStart(start, request.done)
139
+ }));
140
+ const stop = Math.max(fromStart(start, end), ...loop.map((request) => request.done ?? request.sent), interval);
141
+ const run = closedLoopPlan(loop, {
142
+ interval,
143
+ end: stop,
144
+ tolerance: 5,
145
+ pause: interval
146
+ });
147
+ return {
148
+ ...measureLoad(run.plan, run.completions, stop),
149
+ hidden: run.hidden,
150
+ driverGaps: run.driverGaps,
151
+ driverGapTotal: run.driverGapTotal
117
152
  };
118
153
  };
119
154
  /** This function gives the spread of a list of times in ms, or null for no time. */
@@ -246,6 +281,28 @@ const loadVerdict = (prediction, runs) => {
246
281
  }
247
282
  };
248
283
  };
284
+ /** This function gives the estimate verdict of the counted runs, or undefined when no run has an estimate. */
285
+ const estimateVerdict = (estimates) => {
286
+ const runs = estimates.filter((estimate) => estimate !== void 0);
287
+ if (runs.length === 0) return void 0;
288
+ const pool = poolOf(runs);
289
+ const total = (count) => runs.reduce((sum, estimate) => sum + count(estimate), 0);
290
+ const hidden = total((estimate) => estimate.hidden);
291
+ const driverGaps = total((estimate) => estimate.driverGaps);
292
+ const driverGapTotal = total((estimate) => estimate.driverGapTotal);
293
+ const latency = spreadOf$1([...pool.latency, ...pool.censored]);
294
+ const seconds = (time) => (valueIn(time, "ms") / 1e3).toFixed(3);
295
+ const spread = latency === null ? "no latency" : `median ${seconds(latency.p50)} s, p99 ${seconds(latency.p99)} s, slowest ${seconds(latency.max)} s`;
296
+ const gaps = driverGaps === 0 ? "no pause of the client" : `${driverGaps} pauses of the client, ${Math.round(driverGapTotal)} ms in all`;
297
+ return {
298
+ planned: pool.planned,
299
+ hidden,
300
+ driverGaps,
301
+ driverGapTotal: quantity(driverGapTotal, "ms"),
302
+ latency,
303
+ words: `The estimate of the open loop puts back ${hidden} hidden sends, with ${gaps}: ${spread}.`
304
+ };
305
+ };
249
306
  //#endregion
250
307
  //#region src/verdict.ts
251
308
  /** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
@@ -687,7 +744,9 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
687
744
  reordering.fast,
688
745
  reordering.seen
689
746
  ].some((count) => (count ?? 0) > 0) ? ` Reordering: ${reorderingWords(reordering)}.` : "";
690
- const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(median(samples)).value} s, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered} ${prediction.why}`;
747
+ const estimate = estimateVerdict(counted.map((run) => run.estimate));
748
+ const estimated = estimate === void 0 ? "" : ` ${estimate.words}`;
749
+ const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(median(samples)).value} s, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered}${estimated} ${prediction.why}`;
691
750
  return {
692
751
  ...empty,
693
752
  decision: "measured",
@@ -696,6 +755,7 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
696
755
  median: samples.length === 0 ? null : s(median(samples)),
697
756
  ...samples.length === 0 ? {} : { slowest: s(samples.at(-1)) },
698
757
  ...reordering === void 0 ? {} : { reordering },
758
+ ...estimate === void 0 ? {} : { estimate },
699
759
  why
700
760
  };
701
761
  }
@@ -3019,11 +3079,18 @@ const measureSocket = (cell, run) => {
3019
3079
  };
3020
3080
  const sends = load === void 0 ? events.filter((event) => event.ev === "sent").slice(1) : [];
3021
3081
  const answers = events.some((event) => event.ev === "replied") ? "replied" : "data";
3022
- const samples = sends.flatMap((send, n) => {
3082
+ const requests = sends.map((send, n) => {
3023
3083
  const next = sends[n + 1]?.t ?? Infinity;
3024
- const reply = events.find((event) => event.ev === answers && event.t > send.t && event.t < next);
3025
- return reply === void 0 ? [] : [between(send.t, reply.t)];
3084
+ return {
3085
+ send,
3086
+ reply: events.find((event) => event.ev === answers && event.t > send.t && event.t < next)
3087
+ };
3026
3088
  });
3089
+ const samples = requests.flatMap(({ send, reply }) => reply === void 0 ? [] : [between(send.t, reply.t)]);
3090
+ const estimate = cell.probe.mode === "stream" && cell.probe.driver === void 0 && cell.probe.interval !== void 0 ? estimateOf(requests.map(({ send, reply }) => ({
3091
+ sent: send.t,
3092
+ done: reply?.t ?? null
3093
+ })), events.reduce((latest, event) => Math.max(latest, event.t), 0), valueIn(cell.probe.interval, "ms")) : void 0;
3027
3094
  const from = prediction.from === void 0 ? void 0 : referenceAt(prediction.from);
3028
3095
  const until = ended ?? after.at(-1)?.t;
3029
3096
  const peerRecords = peerCookie === void 0 ? [] : kernel.filter((record) => record.netns === peerNetns && record.cookie === peerCookie);
@@ -3056,6 +3123,7 @@ const measureSocket = (cell, run) => {
3056
3123
  steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
3057
3124
  samples,
3058
3125
  ...load === void 0 ? {} : { load },
3126
+ ...estimate === void 0 ? {} : { estimate },
3059
3127
  reordering: reorderingOf(socket.filter((record) => record.mono > request.t), peerRecords.filter((record) => record.mono > request.t)),
3060
3128
  ...crossings,
3061
3129
  void: voided
@@ -3664,6 +3732,27 @@ const openFabric = async (parts) => {
3664
3732
  };
3665
3733
  //#endregion
3666
3734
  //#region src/substrate.ts
3735
+ /**
3736
+ * A layer is a rung of the laboratory (`docs/S7.md`). E1 is containers on one kernel, and E2 is a
3737
+ * cluster of containers on one kernel. A lab with `Topology.fabric` writes E3: routers in containers.
3738
+ * A lab in a guest of QEMU writes E4 (`substrateHere`). The other layers do not have a runner at this
3739
+ * time.
3740
+ */
3741
+ const LAYERS = [
3742
+ "E0",
3743
+ "E1",
3744
+ "E2",
3745
+ "E3",
3746
+ "E4",
3747
+ "E5",
3748
+ "E6"
3749
+ ];
3750
+ /** A cloud is where the runner operates. GitHub can schedule a standard Linux job on Azure or on AWS. */
3751
+ const CLOUDS = [
3752
+ "azure",
3753
+ "aws",
3754
+ "unknown"
3755
+ ];
3667
3756
  /** GitHub announced Linux jobs on AWS on this date. Each earlier hosted job ran on Azure. */
3668
3757
  const AWS_FROM = "2026-10-28";
3669
3758
  /** This function gives the cloud of a DMI vendor. A kernel name does not tell: one image can start on each cloud. */
@@ -7493,6 +7582,19 @@ const clusterOnce = async (lab, cell, environment, index) => {
7493
7582
  * not in the namespace that the prober watches. The record of a run is the lines of the client and
7494
7583
  * the whole partition, read to its high watermark after the group converged.
7495
7584
  */
7585
+ /**
7586
+ * This function gives the record of the Kafka client for a manifest: its image, its source and its
7587
+ * build record. The pins check refuses a record of a built client without its build record.
7588
+ */
7589
+ const kafkaClientRecord = (image) => {
7590
+ const source = shippedSource("kafka");
7591
+ return {
7592
+ kind: "image",
7593
+ image,
7594
+ source,
7595
+ ...buildOf(source)
7596
+ };
7597
+ };
7496
7598
  /** This path is the class path of the Kafka image: the JVM expands the star itself. */
7497
7599
  const CLASS_PATH = "/opt/harness/kafka/classes:/opt/harness/kafka/lib/*";
7498
7600
  /** This option is the heap of the client and of the admin. It has no source: the two hold a producer or a read of one partition. */
@@ -7892,12 +7994,10 @@ const openKafkaLab = async (kafka) => {
7892
7994
  "apk",
7893
7995
  "info",
7894
7996
  "-v"
7895
- ])).split("\n").sort()
7896
- },
7897
- client: {
7898
- kind: "image",
7899
- image: clientImage
7997
+ ])).split("\n").sort(),
7998
+ ...harnessRecord()
7900
7999
  },
8000
+ client: kafkaClientRecord(clientImage),
7901
8001
  netns: {
7902
8002
  client: clientNetns,
7903
8003
  target: members[kafka.brokers[0]].netns
@@ -8663,6 +8763,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
8663
8763
  /** This function divides a count of runs across environments, as equally as possible. */
8664
8764
  const spread = (runs, environments) => Array.from({ length: environments }, (_, at) => Math.floor(runs / environments) + (at < runs % environments ? 1 : 0)).filter((count) => count > 0);
8665
8765
  /**
8766
+ * This function gives the count of runs of each new environment of a cell, for `runs` more runs. A
8767
+ * pair cell has an environment for each run, because its run promotes the standby. Other cells
8768
+ * go across their environments, and at least one for each environment at the same time.
8769
+ */
8770
+ const environmentCounts = (cell, runs) => spread(runs, cell.pair !== void 0 ? runs : Math.max(cell.environments, Math.max(1, cell.concurrency ?? 1)));
8771
+ /**
8666
8772
  * This function tells if the fault of a cell ends its environment. A server that stopped cannot serve
8667
8773
  * the next run. Also, a cell can say that its fault changes the environment (`fresh`).
8668
8774
  */
@@ -8799,6 +8905,7 @@ const inReplicaSetEnvironment = async (cell, environment, count, say) => {
8799
8905
  * run. The run promotes the standby, thus a pair cell has an environment for each run.
8800
8906
  */
8801
8907
  const inPairEnvironment = async (cell, environment, count, say) => {
8908
+ if (count !== 1) throw new Error(`${cell.name}: a pair environment has one run, not ${count}: the run promotes the standby`);
8802
8909
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
8803
8910
  try {
8804
8911
  await lab.warm();
@@ -8891,6 +8998,8 @@ const inKafkaEnvironment = async (cell, environment, count, say) => {
8891
8998
  * Each environment does a warm-up first, and the result keeps it apart from the counted runs.
8892
8999
  */
8893
9000
  const runCell = async (cell, say = () => {}) => {
9001
+ const problems = environmentProblems(cell);
9002
+ if (problems.length > 0) throw new Error(problems.join(", "));
8894
9003
  const started = (/* @__PURE__ */ new Date()).toISOString();
8895
9004
  const manifests = [];
8896
9005
  const runs = [];
@@ -8899,7 +9008,7 @@ const runCell = async (cell, say = () => {}) => {
8899
9008
  let target = cell.runs;
8900
9009
  let next = 0;
8901
9010
  for (;;) {
8902
- const counts = spread(target - runs.length, Math.max(cell.environments, concurrency));
9011
+ const counts = environmentCounts(cell, target - runs.length);
8903
9012
  for (let at = 0; at < counts.length; at += concurrency) {
8904
9013
  const batch = counts.slice(at, at + concurrency).map((count) => ({
8905
9014
  count,
@@ -8935,6 +9044,6 @@ const runCell = async (cell, say = () => {}) => {
8935
9044
  }
8936
9045
  };
8937
9046
  //#endregion
8938
- export { AWS_FROM, CLIENT_CAPABILITIES, CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DEBUG, DIRTY, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, INSERT, KAFKA_TAIL_MS, KEPT, LINGER_MS, LISTING, LOAD_FORMAT, LOAD_PATH, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, RESTART, ROUTERS, SCHEDULE_PATH, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STANDBY_READY, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, bootstrapOf, brokerEnv, buildOf, buildRecordOf, callOf, cancelsOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, controllerEnv, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, envOf, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isFenceLine, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, kafkaArgs, kafkaClientImage, kafkaCutOf, kafkaOnce, kafkaPauseOnce, kafkaViewOf, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, loadOf, loadPlanOf, loadVerdict, locksOf, logNameOf, logTimeOf, manifestProblems, matches, measure, measureConntrack, measureDriver, measureGroup, measurePause, measurePorts, measureQueue, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, namesGroup, nodeClientImage, nodeCommand, nodeHttpImage, nodeIdOf, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, numbersOf, onMonotonic, openClusterLab, openFabric, openGroupLab, openKafkaLab, openLab, openReplicaSetLab, openStandby, pairArgs, pairOnce, pairingOf, pairingsOf, pauseReady, peersOf, pgClientImage, pingsOf, plainOf, planOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recordProblems, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, standbyCommand, statusViewOf, stealShare, stopScript, streamsLoadOf, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topicOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
9047
+ export { AWS_FROM, CLIENT_CAPABILITIES, CLIENT_NAMES, CLOSED_TOLERANCE, CLOUDS, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DEBUG, DIRTY, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, INSERT, KAFKA_TAIL_MS, KEPT, LAYERS, LINGER_MS, LISTING, LOAD_FORMAT, LOAD_PATH, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, RESTART, ROUTERS, SCHEDULE_PATH, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STANDBY_READY, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, bootstrapOf, brokerEnv, buildOf, buildRecordOf, callOf, cancelsOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, controllerEnv, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, envOf, environmentCounts, environmentProblems, errorsOf, estimateOf, estimateVerdict, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isFenceLine, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, kafkaArgs, kafkaClientImage, kafkaCutOf, kafkaOnce, kafkaPauseOnce, kafkaViewOf, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, loadOf, loadPlanOf, loadVerdict, locksOf, logNameOf, logTimeOf, manifestProblems, matches, measure, measureConntrack, measureDriver, measureGroup, measurePause, measurePorts, measureQueue, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, namesGroup, nodeClientImage, nodeCommand, nodeHttpImage, nodeIdOf, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, numbersOf, onMonotonic, openClusterLab, openFabric, openGroupLab, openKafkaLab, openLab, openReplicaSetLab, openStandby, pairArgs, pairOnce, pairingOf, pairingsOf, pauseReady, peersOf, pgClientImage, pingsOf, plainOf, planOf, poolOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recordProblems, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, standbyCommand, statusViewOf, stealShare, stopScript, streamsLoadOf, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topicOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
8939
9048
 
8940
9049
  //# sourceMappingURL=index.js.map