@mark1russell7/harness 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -25,6 +25,7 @@ A **cell** (`cell.ts`) is all that one measurement needs, as data:
25
25
  - A privileged sidecar in the namespace of the client, for its sysctls and its routes. Docker Desktop keeps `/proc/sys` read-only in a container that operates.
26
26
  - Each hop of the topology, on the network of each of its sides.
27
27
  - An injector sidecar in the namespace of the target, with NET_ADMIN. Thus the image of the target stays stock.
28
+ - The manifest of each environment gives the HZ of its kernel, from /proc/config.gz. A host without that file (a hosted runner) gets HZ from two readings of /proc/timer_list, one second apart: its jiffies against the time of the kernel (`hzOf`). `hzFrom` tells which.
28
29
  - The prober ([`@mark1russell7/prober`](https://www.npmjs.com/package/@mark1russell7/prober)): a privileged container that loads the kernel program. It watches the namespace of the client, of the target and of each hop. Each run keeps its records from its start to its end. A run whose records the prober lost is void.
29
30
 
30
31
  Each command is an argv array.
@@ -186,6 +187,7 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
186
187
  - **The time.** It times the detection from the reference event of the prediction to the error report of the kernel. eBPF stamps the two events. For keepalive, the reference is the last received segment. For the give-up, it is the first retransmission of the RTO timer, not the tail loss probe one RTO before it. For a reset, it is the send of the client, which no kernel record marks. The client stamps the send before its write.
187
188
  - **The other views.** It also gives the same interval from the `TCP_INFO` samples of the client, and the delay before the client saw the error. It also gives the steps of the kernel: each keepalive probe, or each expiry of the RTO timer.
188
189
  - **Void runs.** A run is void when the timer that drives the measurement was two jiffies or more behind its wheel slot in the timed interval. The wheel does a slot in the tick when it is due. Thus such a firing measured the host, which stopped a vCPU, not the kernel. A keepalive timer that found its socket held also makes the run void. A run whose fault came too late is void too.
190
+ - The rule is in time when the run knows its HZ (`Run.hz`, from the manifest). Then the delay must reach two jiffies and δ_abs, the larger of the two (`delayVoids`). Each band includes δ_abs, thus a smaller delay does not change a result. At HZ 250 two jiffies are 8 ms, more than a δ_abs of 4 ms, and the rule stays the same. At HZ 1000 two jiffies are 2 ms. A run without its HZ keeps the rule in jiffies.
189
191
  - **The first round trip.** The measurement gives the first round trip of the socket of the observer (`rtt`). It is the smoothed round trip in the first record of the socket: the sample of the handshake. A run whose first round trip is above the range of its prediction is void, with the reason `rtt-over-assumption` and the value. Such a run measured another path than the band assumes. For example, a round trip of 4.826 ms makes an RTO of 52 jiffies, and a ladder that assumes 1 ms has 51.
190
192
  - **Findings.** A finding is a wrong errno, a detector that did not fire, or a detection where the prediction is silence. A router that forgot the flow in the window of a silent prediction is also a finding.
191
193
  - **A reply.** A silent prediction with `replies` times the send of the request to the first segment with data that comes back. A proxy can acknowledge the request first, with no data.
@@ -241,7 +243,9 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
241
243
 
242
244
  `verdictOf(prediction, runs)` (`verdict.ts`) judges a cell:
243
245
 
244
- - **Containment.** Each run lands in [T_lo − δ_abs, T_hi + δ_abs]. `NOISE` (δ_abs) is 4 ms, from the calibration: the worst reset took 1.141 ms, and δ_abs is one jiffy at the minimum.
246
+ - **Containment.** Each run lands in [T_lo − δ_abs, T_hi + δ_abs]. `NOISE` (δ_abs) is 4 ms, from the calibration of the first environment: the worst reset took 1.141 ms, and δ_abs is one jiffy at the minimum.
247
+ - **δ_abs of an environment.** A cell can give the δ_abs of its environment (`Cell.noise`). Stage 0 measures it in each environment. On a hosted runner of GitHub at HZ 1000, the worst reset took 0.147 ms, thus δ_abs is one jiffy, 1 ms. These read it (`noiseOf`): the bands, the pairing of a program line with its kernel record, and the drop of a program.
248
+ - **The injector has its own bound.** A fault that the injector commits or lifts late makes a run void. That bound is `INJECT` (4 ms), not δ_abs. Stage 0 measures the commit apart. On a hosted runner it took up to 2.75 ms at 4 environments.
245
249
  - **Median equivalence.** The order-statistic interval of the median must be in the band, made 5 % wider.
246
250
  - **Extension.** A median across the edge of the band extends the cell from 11 runs to 22, then 44. Then the verdict is inconclusive.
247
251
  - **Recovery.** The recovery after a stall gets the same test as a detection, by its band. A detection is then a finding.
package/build/index.d.ts CHANGED
@@ -483,6 +483,12 @@ type Prediction = {
483
483
  readonly window: Quantity;
484
484
  readonly why: string;
485
485
  readonly from?: Reference;
486
+ /**
487
+ * This choice ends the measured time at the close of the socket by the observer itself, the
488
+ * change from ESTABLISHED to FIN_WAIT1. Without it, the time ends at a report of the kernel or
489
+ * at the last data. Then an idle pool that closes its own socket gives no end.
490
+ */
491
+ readonly ends?: "own-close";
486
492
  readonly group?: GroupPrediction;
487
493
  readonly request?: RequestPrediction;
488
494
  };
@@ -757,6 +763,12 @@ interface Cell {
757
763
  * many environments the machine holds before its noise increases.
758
764
  */
759
765
  readonly concurrency?: number;
766
+ /**
767
+ * This time is δ_abs of the environment of the cell: the noise of the pipeline. Each band includes
768
+ * it, and a timer must be at least that late to make a run void. Stage 0 measures it in each
769
+ * environment. Without it, the cell gets `NOISE`, the δ_abs of the first environment (4 ms).
770
+ */
771
+ readonly noise?: Quantity;
760
772
  }
761
773
  /** A hop place is a hop of a topology, with the side that the rules of a fault face. */
762
774
  interface HopPlace {
@@ -817,6 +829,11 @@ interface Run {
817
829
  * the run void. Null also makes the run void: no clock line came after the run to tell the count.
818
830
  */
819
831
  readonly lost: number | null;
832
+ /**
833
+ * This rate is the HZ of the kernel of the environment, from its manifest. With it, the rule of a
834
+ * late timer is in time. A run without it keeps the rule in jiffies, as the runs before 0.9.0.
835
+ */
836
+ readonly hz?: number;
820
837
  /** These are the addresses and the namespaces of a topology, which the measurement of a run reads. */
821
838
  readonly topology?: RunTopology;
822
839
  /** These are the lines of the routers of a topology from the start to the end of the run. */
@@ -905,9 +922,9 @@ export declare const leftAfter: (socket: readonly KernelRecord[], line: ProbeEve
905
922
  * A close closes the socket in δ_abs. A close frame is a write, and the program does not close
906
923
  * the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
907
924
  */
908
- export declare const dropFinding: (expected: "frame" | "close", drop: ProgramEvent, left: Quantity | null) => string | null;
925
+ export declare const dropFinding: (expected: "frame" | "close", drop: ProgramEvent, left: Quantity | null, delta?: Quantity) => string | null;
909
926
  /** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
910
- export declare const pingsOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown) => ProgramEvent[];
927
+ export declare const pingsOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown, delta?: Quantity) => ProgramEvent[];
911
928
  /**
912
929
  * This function gives the first ping that got no answer: no segment with data reached the socket
913
930
  * after its write, up to `until`. Its write starts the wait of the heartbeat that the fault met.
@@ -918,7 +935,7 @@ export declare const firstUnanswered: (pings: readonly ProgramEvent[], socket: r
918
935
  * write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
919
936
  * abort.
920
937
  */
921
- export declare const dropOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown) => ProgramEvent | undefined;
938
+ export declare const dropOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown, delta?: Quantity) => ProgramEvent | undefined;
922
939
  /** A reconnect reading is what the client did after the detection, from its lines and from the records of the kernel. */
923
940
  interface ReconnectReading {
924
941
  /** These are the waits of the program before each try: from a `reconnecting` line to the next `dial` line. */
@@ -937,7 +954,7 @@ interface ReconnectReading {
937
954
  */
938
955
  export declare const reconnectOf: (run: Run, detected: number, cookie: number | undefined, from?: number) => ReconnectReading;
939
956
  /** This function tells how the waits of a reconnect differ from their bands, each widened by δ_abs, or gives null. */
940
- export declare const reconnectFinding: (expected: Reconnects, read: ReconnectReading) => string | null;
957
+ export declare const reconnectFinding: (expected: Reconnects, read: ReconnectReading, delta?: Quantity) => string | null;
941
958
  /**
942
959
  * A call reading is what the call in flight of a WebSocket client met: its error, a reply, or
943
960
  * nothing in the run. The time counts from the detection to its end.
@@ -1321,6 +1338,23 @@ export declare const requestWords: (outcome: {
1321
1338
  * the harness, the kernel or Docker.
1322
1339
  */
1323
1340
  export declare const NOISE: Quantity;
1341
+ /**
1342
+ * This time is how late the injector can commit or lift a fault before the run is void. Stage 0
1343
+ * measures the commit apart from δ_abs, and the commit is longer. On a hosted runner it took up to
1344
+ * 2.75 ms at 4 environments, where δ_abs is 1 ms. Thus a cell's δ_abs does not change it.
1345
+ */
1346
+ export declare const INJECT: Quantity;
1347
+ /** This function gives the δ_abs of a cell: its own, or `NOISE` for a cell that gives none. */
1348
+ export declare const noiseOf: (cell: {
1349
+ readonly noise?: Quantity | undefined;
1350
+ }) => Quantity;
1351
+ /**
1352
+ * This function tells if a timer firing came late enough to void a run. A firing two jiffies or
1353
+ * more behind its wheel slot came late because the host stopped a vCPU. With the HZ of the run,
1354
+ * the rule is in time: the delay must also reach δ_abs, because each band already includes that
1355
+ * noise. At HZ 250 two jiffies are 8 ms, more than a δ_abs of 4 ms. At HZ 1000 they are 2 ms.
1356
+ */
1357
+ export declare const delayVoids: (behind: number, hz: number | undefined, noise: Quantity) => boolean;
1324
1358
  /** This fraction, δ_rel, is how far the median of a cell can be outside the prediction and stay equivalent. */
1325
1359
  export declare const RELATIVE = .05;
1326
1360
  /** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
@@ -1472,9 +1506,9 @@ export declare const medianInterval: (sorted: readonly number[]) => [number, num
1472
1506
  * finding. The distribution test (a DKW band) needs a predicted distribution from a simulator,
1473
1507
  * which does not exist at this time.
1474
1508
  */
1475
- export declare const verdictOf: (prediction: Prediction, runs: readonly Measured[]) => Verdict;
1509
+ export declare const verdictOf: (prediction: Prediction, runs: readonly Measured[], noise?: Quantity) => Verdict;
1476
1510
  /** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
1477
- export declare const groupVerdictOf: (prediction: Prediction, counted: readonly Measured[]) => GroupVerdict;
1511
+ export declare const groupVerdictOf: (prediction: Prediction, counted: readonly Measured[], delta?: Quantity) => GroupVerdict;
1478
1512
  //#endregion
1479
1513
  //#region src/lab.d.ts
1480
1514
  /** This function gives the image of the harness. */
@@ -1545,6 +1579,11 @@ interface Manifest {
1545
1579
  /** This text is the architecture of the machine, as uname -m gives it: x86_64. */
1546
1580
  readonly architecture: string;
1547
1581
  readonly hz: string | null;
1582
+ /**
1583
+ * This text tells where the HZ came from: the configuration of the kernel, or a measurement. A
1584
+ * host without /proc/config.gz (a hosted runner) gets a measurement of jiffies against the clock.
1585
+ */
1586
+ readonly hzFrom?: "config" | "measured";
1548
1587
  readonly boot_id: string;
1549
1588
  readonly docker: string;
1550
1589
  /** This record is the target, and its settings as it read them back. */
@@ -1768,6 +1807,18 @@ export declare const routesOf: (hops: readonly Pick<Hop, "kind">[]) => Array<{
1768
1807
  * is not a router. Element 0 is the client, element k is hop k - 1, and the last element is the target.
1769
1808
  */
1770
1809
  export declare const endpointAfter: (hops: readonly Pick<Hop, "kind">[], element: number) => number;
1810
+ /**
1811
+ * This function gives HZ from two readings of /proc/timer_list, one second apart: its `now at`
1812
+ * time in ns and its jiffies. It gives the nearest rate that Linux offers, or null when the
1813
+ * measurement is more than 5 % from each of them.
1814
+ */
1815
+ export declare const hzOf: (first: {
1816
+ now: number;
1817
+ jiffies: number;
1818
+ }, second: {
1819
+ now: number;
1820
+ jiffies: number;
1821
+ }) => string | null;
1771
1822
  /** This function gives the device of a route from its line in `ip -o route get`: "10.0.0.2 dev eth0 src …". */
1772
1823
  export declare const deviceOf: (route: string) => string;
1773
1824
  /**
package/build/index.js CHANGED
@@ -20,6 +20,25 @@ const requestWords = (outcome) => `${outcome.connection}${outcome.retried ? ", a
20
20
  * the harness, the kernel or Docker.
21
21
  */
22
22
  const NOISE = quantity(4, "ms");
23
+ /**
24
+ * This time is how late the injector can commit or lift a fault before the run is void. Stage 0
25
+ * measures the commit apart from δ_abs, and the commit is longer. On a hosted runner it took up to
26
+ * 2.75 ms at 4 environments, where δ_abs is 1 ms. Thus a cell's δ_abs does not change it.
27
+ */
28
+ const INJECT = NOISE;
29
+ /** This function gives the δ_abs of a cell: its own, or `NOISE` for a cell that gives none. */
30
+ const noiseOf = (cell) => cell.noise ?? NOISE;
31
+ /**
32
+ * This function tells if a timer firing came late enough to void a run. A firing two jiffies or
33
+ * more behind its wheel slot came late because the host stopped a vCPU. With the HZ of the run,
34
+ * the rule is in time: the delay must also reach δ_abs, because each band already includes that
35
+ * noise. At HZ 250 two jiffies are 8 ms, more than a δ_abs of 4 ms. At HZ 1000 they are 2 ms.
36
+ */
37
+ const delayVoids = (behind, hz, noise) => {
38
+ if (behind < 2) return false;
39
+ if (hz === void 0 || !(hz > 0)) return true;
40
+ return behind / hz >= Math.max(2 / hz, valueIn(noise, "s"));
41
+ };
23
42
  /** This fraction, δ_rel, is how far the median of a cell can be outside the prediction and stay equivalent. */
24
43
  const RELATIVE = .05;
25
44
  /** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
@@ -116,15 +135,15 @@ const middle = (sorted) => {
116
135
  * finding. The distribution test (a DKW band) needs a predicted distribution from a simulator,
117
136
  * which does not exist at this time.
118
137
  */
119
- const verdictOf = (prediction, runs) => {
120
- const band = verdictOfBand(prediction, runs);
138
+ const verdictOf = (prediction, runs, noise = NOISE) => {
139
+ const band = verdictOfBand(prediction, runs, noise);
121
140
  const counted = runs.filter((run) => run.void === null);
122
141
  const asked = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
123
142
  const verdict = withProgram(asked, counted);
124
143
  if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
125
144
  return {
126
145
  ...verdict,
127
- group: groupVerdictOf(prediction, counted)
146
+ group: groupVerdictOf(prediction, counted, noise)
128
147
  };
129
148
  };
130
149
  /**
@@ -184,8 +203,8 @@ const withRequests = (verdict, counted) => {
184
203
  /** This function gives the median of some counts, or null for none. */
185
204
  const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
186
205
  /** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
187
- const groupVerdictOf = (prediction, counted) => {
188
- const noise = valueIn(NOISE, "s");
206
+ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
207
+ const noise = valueIn(delta, "s");
189
208
  const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
190
209
  const stages = (prediction.group?.stages ?? []).map((band) => {
191
210
  const interval = {
@@ -261,7 +280,7 @@ const groupVerdictOf = (prediction, counted) => {
261
280
  };
262
281
  };
263
282
  /** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
264
- const verdictOfBand = (prediction, runs) => {
283
+ const verdictOfBand = (prediction, runs, delta = NOISE) => {
265
284
  const counted = runs.filter((run) => run.void === null);
266
285
  const voided = runs.length - counted.length;
267
286
  const outside = counted.filter((run) => run.finding !== null).map((run) => ({
@@ -355,7 +374,7 @@ const verdictOfBand = (prediction, runs) => {
355
374
  };
356
375
  }
357
376
  const timed = prediction.detects ? prediction : prediction.recovers ?? prediction.replies;
358
- const noise = valueIn(NOISE, "s");
377
+ const noise = valueIn(delta, "s");
359
378
  const low = valueIn(timed.low, "s");
360
379
  const high = valueIn(timed.high, "s");
361
380
  const band = {
@@ -456,12 +475,12 @@ const onConnection = (line, local) => {
456
475
  return (line["local"] ?? line["peer"]) === local;
457
476
  };
458
477
  /** This function gives the first record of a socket that matches, at the stamp of a line or in δ_abs after it. */
459
- const kernelAt = (socket, line, matches) => {
460
- const noise = valueIn(NOISE, "s") * 1e9;
478
+ const kernelAt = (socket, line, matches, delta) => {
479
+ const noise = valueIn(delta, "s") * 1e9;
461
480
  return socket.find((record) => record.mono >= line.t && record.mono <= line.t + noise && matches(record));
462
481
  };
463
- const eventOf = (socket, line, matches) => {
464
- const record = kernelAt(socket, line, matches);
482
+ const eventOf = (socket, line, matches, delta = NOISE) => {
483
+ const record = kernelAt(socket, line, matches, delta);
465
484
  return {
466
485
  line,
467
486
  at: record?.mono ?? line.t,
@@ -483,8 +502,8 @@ const leftAfter = (socket, line) => {
483
502
  * A close closes the socket in δ_abs. A close frame is a write, and the program does not close
484
503
  * the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
485
504
  */
486
- const dropFinding = (expected, drop, left) => {
487
- const noise = valueIn(NOISE, "s");
505
+ const dropFinding = (expected, drop, left, delta = NOISE) => {
506
+ const noise = valueIn(delta, "s");
488
507
  const soon = left !== null && valueIn(left, "s") <= noise;
489
508
  if (expected === "close") {
490
509
  if (left === null) return "the program did not close its socket after the drop, where a close is at once";
@@ -495,7 +514,7 @@ const dropFinding = (expected, drop, left) => {
495
514
  };
496
515
  const isWrite = (record) => record.ev === "tx";
497
516
  /** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
498
- const pingsOf = (lines, socket, local) => lines.filter((line) => line.ev === "ping" && onConnection(line, local)).map((line) => eventOf(socket, line, isWrite));
517
+ const pingsOf = (lines, socket, local, delta = NOISE) => lines.filter((line) => line.ev === "ping" && onConnection(line, local)).map((line) => eventOf(socket, line, isWrite, delta));
499
518
  /**
500
519
  * This function gives the first ping that got no answer: no segment with data reached the socket
501
520
  * after its write, up to `until`. Its write starts the wait of the heartbeat that the fault met.
@@ -506,9 +525,9 @@ const firstUnanswered = (pings, socket, until = Number.POSITIVE_INFINITY) => pin
506
525
  * write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
507
526
  * abort.
508
527
  */
509
- const dropOf = (lines, socket, local) => {
528
+ const dropOf = (lines, socket, local, delta = NOISE) => {
510
529
  const line = lines.find((each) => each.ev === "drop" && onConnection(each, local));
511
- return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1);
530
+ return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1, delta);
512
531
  };
513
532
  /**
514
533
  * This function reads the reconnect of the client after the detection at `detected`. The socket of
@@ -539,8 +558,8 @@ const reconnectOf = (run, detected, cookie, from = detected) => {
539
558
  };
540
559
  };
541
560
  /** This function tells how the waits of a reconnect differ from their bands, each widened by δ_abs, or gives null. */
542
- const reconnectFinding = (expected, read) => {
543
- const noise = valueIn(NOISE, "s");
561
+ const reconnectFinding = (expected, read, delta = NOISE) => {
562
+ const noise = valueIn(delta, "s");
544
563
  const round = (value) => Math.round(value * 1e6) / 1e6;
545
564
  const found = expected.waits.flatMap((band, at) => {
546
565
  const wait = read.waits[at];
@@ -729,9 +748,9 @@ const settled = (cell, run) => {
729
748
  * that found the socket held by its owner put itself off by HZ/20. Either way, the run measured
730
749
  * something other than the timer.
731
750
  */
732
- const delayed = (place, from, to) => {
751
+ const delayed = (place, from, to, hz, noise) => {
733
752
  const firings = place.records.filter((record) => record.ev === "timer" && record.what === "keepalive" && record.mono >= from && record.mono <= to);
734
- const late = firings.find((record) => (record.behind?.value ?? 0) >= 2);
753
+ const late = firings.find((record) => delayVoids(record.behind?.value ?? 0, hz, noise));
735
754
  if (late !== void 0) return `its keepalive timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
736
755
  return firings.find((record) => record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
737
756
  };
@@ -805,13 +824,13 @@ const measureDriver = (cell, run) => {
805
824
  if (heal === null) voided = "the fault never lifted";
806
825
  else {
807
826
  const late = (heal.t0 - run.fault.t0) / 1e9 - valueIn(cell.hold, "s");
808
- if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the client`;
827
+ if (late > valueIn(INJECT, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the client`;
809
828
  }
810
829
  }
811
830
  const assumed = prediction.rtt;
812
831
  if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
813
832
  const findings = [];
814
- const noise = valueIn(NOISE, "s");
833
+ const noise = valueIn(noiseOf(cell), "s");
815
834
  const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
816
835
  for (const match of driver.quiet ?? []) {
817
836
  const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
@@ -901,7 +920,7 @@ const measureDriver = (cell, run) => {
901
920
  void: voided,
902
921
  finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
903
922
  };
904
- if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended);
923
+ if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended, run.hz, noiseOf(cell));
905
924
  const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
906
925
  if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
907
926
  const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
@@ -1224,14 +1243,14 @@ const measureGroup = (cell, run) => {
1224
1243
  if (heal === null) voided = "the fault never lifted";
1225
1244
  else {
1226
1245
  const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
1227
- if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
1246
+ if (late > valueIn(INJECT, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
1228
1247
  }
1229
1248
  }
1230
1249
  const assumed = prediction.detects === null ? void 0 : prediction.rtt;
1231
1250
  const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}` && record.tcp?.srtt !== void 0);
1232
1251
  const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
1233
1252
  if (voided === null && assumed !== void 0 && rtt !== null && rtt > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
1234
- if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(NOISE, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
1253
+ if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(INJECT, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
1235
1254
  let reference = t0;
1236
1255
  if (prediction.detects === false && prediction.recovers !== void 0) {
1237
1256
  const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
@@ -1253,7 +1272,7 @@ const measureGroup = (cell, run) => {
1253
1272
  if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
1254
1273
  for (const error of expected?.errors ?? []) {
1255
1274
  const met = group.errors.find((each) => each.name === error.name);
1256
- const noise = valueIn(NOISE, "s");
1275
+ const noise = valueIn(noiseOf(cell), "s");
1257
1276
  if (met === void 0) findings.push(`the client never met ${error.name}`);
1258
1277
  else if (met.first.value < valueIn(error.low, "s") - noise || met.first.value > valueIn(error.high, "s") + noise) findings.push(`the client met ${error.name} ${met.first.value} s after the fault, outside [${valueIn(error.low, "s")}, ${valueIn(error.high, "s")}] s`);
1259
1278
  }
@@ -1537,7 +1556,7 @@ const measureSocket = (cell, run) => {
1537
1556
  */
1538
1557
  const delayed = (from, to, timer) => {
1539
1558
  const firings = socket.filter((record) => record.ev === "timer" && record.what === timer && record.mono >= from && record.mono <= to);
1540
- const late = firings.find((record) => (record.behind?.value ?? 0) >= 2);
1559
+ const late = firings.find((record) => delayVoids(record.behind?.value ?? 0, run.hz, noiseOf(cell)));
1541
1560
  if (late !== void 0) return `its ${timer} timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
1542
1561
  return firings.find((record) => timer === "keepalive" && record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
1543
1562
  };
@@ -1600,10 +1619,10 @@ const measureSocket = (cell, run) => {
1600
1619
  const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
1601
1620
  const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
1602
1621
  const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
1603
- const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
1622
+ const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" || cell.prediction.detects === null && cell.prediction.ends === "own-close" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
1604
1623
  const lines = programLines(cell, run);
1605
1624
  const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
1606
- const first = dropOf(lines, socket, local);
1625
+ const first = dropOf(lines, socket, local, noiseOf(cell));
1607
1626
  const drop = first !== void 0 && (kernelEnded === void 0 || first.at <= kernelEnded) ? first : void 0;
1608
1627
  const dropped = drop !== void 0;
1609
1628
  const outcome = dropped ? { kind: "dropped" } : reported !== void 0 ? {
@@ -1616,7 +1635,7 @@ const measureSocket = (cell, run) => {
1616
1635
  const ended = drop?.at ?? kernelEnded;
1617
1636
  const noticed = reported !== void 0 && seen !== void 0 && !dropped ? between(reported.mono, seen.t) : null;
1618
1637
  const { prediction } = cell;
1619
- const pings = pingsOf(lines, socket, local);
1638
+ const pings = pingsOf(lines, socket, local, noiseOf(cell));
1620
1639
  const before = run.fault?.t1 ?? ready.t;
1621
1640
  const referenceAt = (from) => {
1622
1641
  if (from === "request-sent") return request.t;
@@ -1752,7 +1771,7 @@ const measureSocket = (cell, run) => {
1752
1771
  if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
1753
1772
  } else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
1754
1773
  else {
1755
- const noise = valueIn(NOISE, "s");
1774
+ const noise = valueIn(noiseOf(cell), "s");
1756
1775
  const low = valueIn(expected.low, "s") - noise;
1757
1776
  const high = valueIn(expected.high, "s") + noise;
1758
1777
  if (after.value < low || after.value > high) finding = `a router forgot the flow ${after.value} s after the last segment, outside [${Math.round(low * 1e6) / 1e6}, ${Math.round(high * 1e6) / 1e6}] s`;
@@ -1796,9 +1815,9 @@ const measureSocket = (cell, run) => {
1796
1815
  const matches = prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : prediction.errno === "DROPPED" ? outcome.kind === "dropped" : outcome.kind === "error" && outcome.errno === prediction.errno;
1797
1816
  const words = outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : outcome.kind === "dropped" ? "a drop by its own program" : "end of file";
1798
1817
  const reconnect = prediction.reconnects === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
1799
- const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect);
1818
+ const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect, noiseOf(cell));
1800
1819
  const left = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
1801
- const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left);
1820
+ const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left, noiseOf(cell));
1802
1821
  const found = [
1803
1822
  matches ? null : `it ended with ${words}, not ${prediction.errno}`,
1804
1823
  late,
@@ -1812,7 +1831,7 @@ const measureSocket = (cell, run) => {
1812
1831
  const slow = prediction.timer.startsWith("expire_") ? (run.middlebox ?? []).find((line) => line.ev === "pump-slow" && line.t <= ended && line.t + Number(line["took"]) * 1e9 >= ended - lag) : void 0;
1813
1832
  if (slow !== void 0) voided ??= `a listing of the router took ${Math.round(Number(slow["took"]) * 1e4) / 10} ms, longer than the bound of its prediction`;
1814
1833
  const commit = run.fault === null ? 0 : (run.fault.t1 - run.fault.t0) / 1e6;
1815
- if (prediction.from === "fault" && commit > valueIn(NOISE, "ms")) voided ??= `the commit of the fault took ${Math.round(commit * 10) / 10} ms, longer than the noise of the pipeline: its start is not known`;
1834
+ if (prediction.from === "fault" && commit > valueIn(INJECT, "ms")) voided ??= `the commit of the fault took ${Math.round(commit * 10) / 10} ms, longer than the noise of the pipeline: its start is not known`;
1816
1835
  const unanswered = prediction.from === "ping-sent" ? firstUnanswered(pings, socket, ended) : void 0;
1817
1836
  const endLine = drop?.line ?? seen;
1818
1837
  const view = unanswered !== void 0 ? endLine === void 0 ? null : between(unanswered.line.t, endLine.t) : remote ? null : socketVantage(cell, events, ready, request);
@@ -2088,6 +2107,42 @@ const endpointAfter = (hops, element) => {
2088
2107
  return at;
2089
2108
  };
2090
2109
  const STOP = { timeout: 0 };
2110
+ /** These are the values of CONFIG_HZ that Linux offers. */
2111
+ const RATES = [
2112
+ 100,
2113
+ 250,
2114
+ 300,
2115
+ 1e3
2116
+ ];
2117
+ /**
2118
+ * This function gives HZ from two readings of /proc/timer_list, one second apart: its `now at`
2119
+ * time in ns and its jiffies. It gives the nearest rate that Linux offers, or null when the
2120
+ * measurement is more than 5 % from each of them.
2121
+ */
2122
+ const hzOf = (first, second) => {
2123
+ const seconds = (second.now - first.now) / 1e9;
2124
+ if (!(seconds > 0)) return null;
2125
+ const rate = (second.jiffies - first.jiffies) / seconds;
2126
+ const nearest = RATES.reduce((best, each) => Math.abs(each - rate) < Math.abs(best - rate) ? each : best);
2127
+ return Math.abs(nearest - rate) / nearest <= .05 ? String(nearest) : null;
2128
+ };
2129
+ /** This function measures HZ in a privileged container: jiffies against the time of the kernel, one second apart. */
2130
+ const measuredHz = async (container) => {
2131
+ const read = "awk '/^now at/ {now=$3} /^jiffies:/ {print now, $2; exit}' /proc/timer_list";
2132
+ const values = (await output(container, [
2133
+ "sh",
2134
+ "-c",
2135
+ `${read}; sleep 1; ${read}`
2136
+ ]).catch(() => "")).split(/\s+/).filter((each) => each !== "").map(Number);
2137
+ if (values.length !== 4 || values.some((each) => !Number.isFinite(each))) return null;
2138
+ return hzOf({
2139
+ now: values[0],
2140
+ jiffies: values[1]
2141
+ }, {
2142
+ now: values[2],
2143
+ jiffies: values[3]
2144
+ });
2145
+ };
2091
2146
  const output = async (container, argv) => {
2092
2147
  const result = await container.exec(argv);
2093
2148
  if (result.exitCode !== 0) throw new Error(`${argv.join(" ")} exited ${result.exitCode}: ${result.output.trim()}`);
@@ -2723,7 +2778,18 @@ const openLab = async (target, kind = "tcp", topology) => {
2723
2778
  return {
2724
2779
  kernel: await output(client, ["uname", "-r"]),
2725
2780
  architecture: await output(client, ["uname", "-m"]),
2726
- hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
2781
+ ...await (async () => {
2782
+ const configured = /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1];
2783
+ if (configured !== void 0) return {
2784
+ hz: configured,
2785
+ hzFrom: "config"
2786
+ };
2787
+ const measured = await measuredHz(admin);
2788
+ return measured === null ? { hz: null } : {
2789
+ hz: measured,
2790
+ hzFrom: "measured"
2791
+ };
2792
+ })(),
2727
2793
  boot_id: await output(client, ["cat", "/proc/sys/kernel/random/boot_id"]),
2728
2794
  docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
2729
2795
  target: {
@@ -4068,7 +4134,7 @@ const judge = (cell, runs) => {
4068
4134
  const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
4069
4135
  return {
4070
4136
  measured,
4071
- verdict: verdictOf(cell.prediction, measured)
4137
+ verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
4072
4138
  };
4073
4139
  };
4074
4140
  /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
@@ -4624,8 +4690,13 @@ const runCell = async (cell, say = () => {}) => {
4624
4690
  const done = await Promise.all(batch.map(({ count, env }) => inEnvironment(cell, env, count, say)));
4625
4691
  for (const each of done) {
4626
4692
  manifests.push(each.manifest);
4627
- runs.push(...each.runs);
4628
- warmups.push(each.warmup);
4693
+ const hz = each.manifest.hz === null ? void 0 : Number(each.manifest.hz);
4694
+ const withHz = (run) => hz === void 0 ? run : {
4695
+ ...run,
4696
+ hz
4697
+ };
4698
+ runs.push(...each.runs.map(withHz));
4699
+ warmups.push(withHz(each.warmup));
4629
4700
  }
4630
4701
  }
4631
4702
  const { measured, verdict } = judge(cell, runs);
@@ -4646,6 +4717,6 @@ const runCell = async (cell, say = () => {}) => {
4646
4717
  }
4647
4718
  };
4648
4719
  //#endregion
4649
- export { CONVERGE_MS, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, crossingsOf, cutOf, deadlineOf, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
4720
+ export { CONVERGE_MS, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hzOf, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, noiseOf, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
4650
4721
 
4651
4722
  //# sourceMappingURL=index.js.map