@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 +5 -1
- package/build/index.d.ts +57 -6
- package/build/index.js +110 -39
- package/build/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
4628
|
-
|
|
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
|