@mark1russell7/harness 0.9.0 → 0.11.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
@@ -203,11 +203,14 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
203
203
  - The second view is the stamps of the program: from its `ping` line to its `drop` line. The steps are the later pings, each from the reference.
204
204
  - The lines of a client name the socket by its local address (`local`), and the lines of a server by its peer (`peer`). For the server, the lines are the lines of the target. A line whose address is null is about a try that did not open.
205
205
  - A prediction can say how the program drops (`drop`). A close frame (`frame`) is a write, and the socket stays ESTABLISHED after it. A close (`close`) closes the socket in δ_abs: a FIN or an abort. Another drop is a finding. A FIN of the peer that answers a close frame is no close by the program. The run gives the time from the `drop` line to the close by the program (`left`).
206
+ - The client process ends at the end of each run, and its kernel then closes each socket that is still open. A prediction can say that the program is that client process (`exits`). Then a close in δ_abs of that end is no close by the program: `left` stays null, and `exited` gives the time.
207
+ - The end of the process is the first change out of ESTABLISHED of another socket of the client after its last `dial` line (`exitOf`). The program dropped none of these sockets.
206
208
  - A heartbeat is a timer of the program. The retransmit timer of its ping fires in the same interval. Thus a run whose retransmit timer was two jiffies or more behind its wheel slot is void: the VM stopped a vCPU.
207
209
  - **The reconnect** (`reconnectOf`). A prediction with `reconnects` also reads what the client did after the detection.
208
210
  - Each wait goes from a `reconnecting` line, where the program starts its timer, to its next `dial` line, where it makes the socket. A wait outside its band, widened by δ_abs, is a finding. A try that did not come is a finding too.
209
211
  - The kernel gives the time from the detection to the first SYN of a new socket (`syn`). It also gives the time to the first new socket in ESTABLISHED (`established`).
210
212
  - It also gives the time from each `dial` line to its SYN (`connects`): the work of the program before connect(2).
213
+ - A measurement-only prediction can give `reconnects` too. The end of its connection has no band, but each wait has one. A wait outside its band is a finding of its run, and the verdict names that run. The decision stays `measured`.
211
214
  - **A call in flight of a WebSocket client** (`callOf`). The run gives its error or its reply, and the time from the detection to its end.
212
215
  - **A stream.** In a stream (`probe.mode` is `stream`), the client sends `count` requests. The run keeps the time from each send to the last byte of its reply (`samples`). A record without `replied` lines gives the time to the first data.
213
216
  - **Reordering.** A measurement-only run also gives what reordering, duplicates and losses did (`reordering`). At the client, it counts the arrivals beyond a hole and the arrivals of data that it had already, against `rcv_nxt`. From the sender, it gives the most SACKed and lost segments, the DSACKs, the fast retransmissions, and the reordering degree and count of the kernel.
@@ -262,7 +265,7 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
262
265
  - **An HTTP client** (`Verdict.requests`). The verdict counts each outcome of the second request over the counted runs. A measurement-only cell also gives them in its reason.
263
266
  - **The reconnect** (`Verdict.reconnect`). A wait outside its band is a finding of its run, thus the cell fails. The verdict gives the spread of each wait, of the first SYN, of the first ESTABLISHED and of the work before each connect(2). Its reason says the medians.
264
267
  - **A call in flight** (`Verdict.calls`). The verdict counts each outcome of the call in flight of a WebSocket client, and its reason says them.
265
- - **The socket after a drop** (`Verdict.left`). The verdict gives the spread of the time from a drop to the close of its socket by the program. It also counts the runs where the program did not close it.
268
+ - **The socket after a drop** (`Verdict.left`). The verdict gives the spread of the time from a drop to the close of its socket by the program. It also counts the runs where the program did not close it. For the runs where the socket stayed open until the end of the client process, it gives the spread of that end (`exited`).
266
269
 
267
270
  The distribution test needs a simulator, which does not exist at this time.
268
271
 
package/build/index.d.ts CHANGED
@@ -426,7 +426,8 @@ interface Reconnects {
426
426
  * A cell whose outcome is a distribution, for example under netem loss, has no band. Its
427
427
  * prediction is `detects: null` with the reason, and its runs are measurements only. A
428
428
  * measurement-only cell that names a reference (`from`) also records the time from it to the end
429
- * of the connection, or to the last data.
429
+ * of the connection, or to the last data. It can also give the waits of the reconnect after that
430
+ * end (`reconnects`).
430
431
  *
431
432
  * For a group cell, the band is the first write that a primary accepts after the fault, and its
432
433
  * errno is `OK`. `group` tells which primary, and the stages of the group on the way.
@@ -467,6 +468,12 @@ type Prediction = {
467
468
  * ESTABLISHED at once (`close`).
468
469
  */
469
470
  readonly drop?: "frame" | "close";
471
+ /**
472
+ * This flag tells that the program of the observer is the client process, which ends at the
473
+ * end of the run. Its kernel then closes each socket that is still open. A close of the
474
+ * socket at that end is not a close by the program, thus `left` stays null for it (`exited`).
475
+ */
476
+ readonly exits?: true;
470
477
  } | {
471
478
  readonly detects: false;
472
479
  readonly window: Quantity;
@@ -491,6 +498,11 @@ type Prediction = {
491
498
  readonly ends?: "own-close";
492
499
  readonly group?: GroupPrediction;
493
500
  readonly request?: RequestPrediction;
501
+ /**
502
+ * These are the waits of the reconnect after the end of the connection, from the document of
503
+ * the client. The end has no band, but each wait has one. A wait outside its band is a finding of its run.
504
+ */
505
+ readonly reconnects?: Reconnects;
494
506
  };
495
507
  /** A target is the server that a cell connects to: a stock image. */
496
508
  interface Target {
@@ -917,12 +929,29 @@ export declare const programLines: (cell: Cell, run: Run) => readonly ProbeEvent
917
929
  * the peer (CLOSE_WAIT) is not such a close. It is null when the socket did not close itself.
918
930
  */
919
931
  export declare const leftAfter: (socket: readonly KernelRecord[], line: ProbeEvent) => Quantity | null;
932
+ /**
933
+ * This function gives the end of the client process in a run, as the kernel stamps it. The process
934
+ * ends after its last try, and its kernel then closes each socket that is still open, at one
935
+ * instant. Thus the end is the first change out of ESTABLISHED of another socket of the client
936
+ * after its last `dial` line. The program dropped none of these sockets. It is undefined when the
937
+ * record has no such change, for example when the last try did not open.
938
+ */
939
+ export declare const exitOf: (run: Run, cookie: number | undefined) => number | undefined;
940
+ /**
941
+ * This function separates a close of the socket by the program from the end of the client process.
942
+ * `left` is the time from the drop line to the close, and `exit` is the end of the process. A close
943
+ * in δ_abs of that end is the end, not the program: `left` becomes null, and `exited` keeps the time.
944
+ */
945
+ export declare const closeOrExit: (line: ProbeEvent, left: Quantity | null, exit: number | undefined, delta?: Quantity) => {
946
+ readonly left: Quantity | null;
947
+ readonly exited?: Quantity;
948
+ };
920
949
  /**
921
950
  * This function tells how a drop differs from the way that its prediction gives, or gives null.
922
951
  * A close closes the socket in δ_abs. A close frame is a write, and the program does not close
923
952
  * the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
924
953
  */
925
- export declare const dropFinding: (expected: "frame" | "close", drop: ProgramEvent, left: Quantity | null, delta?: Quantity) => string | null;
954
+ export declare const dropFinding: (expected: "frame" | "close", drop: ProgramEvent, left: Quantity | null, delta?: Quantity, exited?: Quantity) => string | null;
926
955
  /** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
927
956
  export declare const pingsOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown, delta?: Quantity) => ProgramEvent[];
928
957
  /**
@@ -1233,9 +1262,15 @@ interface Measured {
1233
1262
  /**
1234
1263
  * This time is from the drop line of the program to the close of its socket by its own side: a
1235
1264
  * FIN or an abort. A prediction that says how the program drops (`drop`) gets it. It is null
1236
- * when the socket did not close itself in the run.
1265
+ * when the socket did not close itself in the run. A prediction can say that the client process
1266
+ * ends with the run (`exits`). Then a close at that end is no close by the program, and `left` is null.
1237
1267
  */
1238
1268
  readonly left?: Quantity | null;
1269
+ /**
1270
+ * This time is from the drop line of the program to the end of the client process. A socket gets
1271
+ * it when it stayed open until that end. The kernel closed it then, not the program (`exits`).
1272
+ */
1273
+ readonly exited?: Quantity;
1239
1274
  /** This text is the reason that the run cannot count, or null. A void run is not a pass and not a finding. */
1240
1275
  readonly void: string | null;
1241
1276
  /** This text tells what in the run disagrees with the prediction, other than its timing, or null. */
@@ -1415,10 +1450,13 @@ interface Verdict {
1415
1450
  /**
1416
1451
  * This record is the time from a drop of the program to the close of its socket by its own side,
1417
1452
  * over the counted runs. It also counts the runs where the socket did not close itself.
1453
+ * `exited` is the spread of the end of the client process, for the runs where the socket stayed
1454
+ * open until that end (`Measured.exited`).
1418
1455
  */
1419
1456
  readonly left?: {
1420
1457
  readonly spread: Spread | null;
1421
1458
  readonly stayed: number;
1459
+ readonly exited?: Spread | null;
1422
1460
  };
1423
1461
  readonly why: string;
1424
1462
  }
package/build/index.js CHANGED
@@ -166,12 +166,15 @@ const withProgram = (verdict, counted) => {
166
166
  const outcomes = {};
167
167
  for (const call of calls) outcomes[call.outcome] = (outcomes[call.outcome] ?? 0) + 1;
168
168
  const gone = lefts.flatMap((each) => each === null ? [] : [each]);
169
+ const ends = counted.flatMap((run) => run.exited === void 0 ? [] : [run.exited]);
169
170
  const left = lefts.length === 0 ? void 0 : {
170
171
  spread: spreadOf(gone),
171
- stayed: lefts.length - gone.length
172
+ stayed: lefts.length - gone.length,
173
+ ...ends.length === 0 ? {} : { exited: spreadOf(ends) }
172
174
  };
175
+ const ended = ends.length === 0 ? "" : `: in ${ends.length} the socket stayed open until the client process ended, median ${ms(spreadOf(ends))} after the drop line`;
173
176
  const said = [
174
- ...left === void 0 ? [] : [`After the drop, the program closed its socket in ${gone.length} runs, median ${ms(left.spread)} after its drop line${left.stayed === 0 ? "" : `, and it did not close it in ${left.stayed}`}.`],
177
+ ...left === void 0 ? [] : [`After the drop, the program closed its socket in ${gone.length} runs, median ${ms(left.spread)} after its drop line${left.stayed === 0 ? "" : `, and it did not close it in ${left.stayed}${ended}`}.`],
175
178
  ...reconnect === void 0 ? [] : [`Reconnect: the waits before each try, median ${reconnect.waits.map(ms).join(", ")}; the first SYN median ${ms(reconnect.syn)} and ESTABLISHED median ${ms(reconnect.established)} after the detection.`],
176
179
  ...calls.length === 0 ? [] : [`Calls in flight: ${Object.entries(outcomes).map(([outcome, n]) => `${n} ${outcome}`).join("; ")}.`]
177
180
  ];
@@ -329,13 +332,15 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
329
332
  }
330
333
  const outcomes = [...kinds].map(([kind, n]) => `${n} ${kind}`).join(", ");
331
334
  const timed = times.length === 0 ? "no time" : `median ${s(middle(times)).value} s from ${prediction.from}, range [${s(times[0]).value}, ${s(times.at(-1)).value}] s`;
335
+ const missed = outside.filter((each) => counted.some((run) => name(run) === each.run && run.reconnect !== void 0));
336
+ const late = missed.length === 0 ? "" : ` ${missed.length} runs had a wait of the reconnect outside its band.`;
332
337
  return {
333
338
  ...empty,
334
339
  decision: "measured",
335
340
  band: null,
336
- outside: [],
341
+ outside: missed,
337
342
  median: times.length === 0 ? null : s(middle(times)),
338
- why: `${counted.length} runs measured: ${outcomes}; ${timed}. ${prediction.why}`
343
+ why: `${counted.length} runs measured: ${outcomes}; ${timed}.${late} ${prediction.why}`
339
344
  };
340
345
  }
341
346
  const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
@@ -498,15 +503,41 @@ const leftAfter = (socket, line) => {
498
503
  return left === void 0 ? null : between$3(line.t, left.mono);
499
504
  };
500
505
  /**
506
+ * This function gives the end of the client process in a run, as the kernel stamps it. The process
507
+ * ends after its last try, and its kernel then closes each socket that is still open, at one
508
+ * instant. Thus the end is the first change out of ESTABLISHED of another socket of the client
509
+ * after its last `dial` line. The program dropped none of these sockets. It is undefined when the
510
+ * record has no such change, for example when the last try did not open.
511
+ */
512
+ const exitOf = (run, cookie) => {
513
+ const last = run.events.filter((line) => line.ev === "dial").at(-1);
514
+ if (last === void 0) return void 0;
515
+ const dropped = new Set(run.events.filter((line) => line.ev === "drop").map((line) => line["local"]));
516
+ return run.kernel.find((record) => record.netns === run.netns && record.cookie !== cookie && record.ev === "state" && oldState$2(record) === 1 && (newState$2(record) === 4 || newState$2(record) === 7) && record.mono >= last.t && !dropped.has(record.src))?.mono;
517
+ };
518
+ /**
519
+ * This function separates a close of the socket by the program from the end of the client process.
520
+ * `left` is the time from the drop line to the close, and `exit` is the end of the process. A close
521
+ * in δ_abs of that end is the end, not the program: `left` becomes null, and `exited` keeps the time.
522
+ */
523
+ const closeOrExit = (line, left, exit, delta = NOISE) => {
524
+ if (left === null || exit === void 0) return { left };
525
+ const at = line.t + valueIn(left, "s") * 1e9;
526
+ return Math.abs(at - exit) <= valueIn(delta, "s") * 1e9 ? {
527
+ left: null,
528
+ exited: left
529
+ } : { left };
530
+ };
531
+ /**
501
532
  * This function tells how a drop differs from the way that its prediction gives, or gives null.
502
533
  * A close closes the socket in δ_abs. A close frame is a write, and the program does not close
503
534
  * the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
504
535
  */
505
- const dropFinding = (expected, drop, left, delta = NOISE) => {
536
+ const dropFinding = (expected, drop, left, delta = NOISE, exited) => {
506
537
  const noise = valueIn(delta, "s");
507
538
  const soon = left !== null && valueIn(left, "s") <= noise;
508
539
  if (expected === "close") {
509
- if (left === null) return "the program did not close its socket after the drop, where a close is at once";
540
+ if (left === null) return `the program did not close its socket after the drop, where a close is at once${exited === void 0 ? "" : ": it stayed open until the client process ended"}`;
510
541
  return soon ? null : `the program closed its socket ${left.value} s after the drop, not at once`;
511
542
  }
512
543
  if (drop.record !== "write") return "the drop wrote no close frame on its socket";
@@ -1674,10 +1705,14 @@ const measureSocket = (cell, run) => {
1674
1705
  toward: "target"
1675
1706
  }
1676
1707
  }, run)) };
1708
+ const reconnect = prediction.reconnects === void 0 || ended === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
1709
+ const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect, noiseOf(cell));
1677
1710
  return {
1678
1711
  ...base,
1679
1712
  ...nothing,
1680
1713
  ...chain,
1714
+ ...reconnect === void 0 ? {} : { reconnect },
1715
+ finding: late,
1681
1716
  outcome: ended !== void 0 ? outcome : { kind: "answered" },
1682
1717
  ...from === void 0 || until === void 0 ? {} : { elapsed: between(from, until) },
1683
1718
  noticed: prediction.from === void 0 ? null : noticed,
@@ -1816,8 +1851,12 @@ const measureSocket = (cell, run) => {
1816
1851
  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";
1817
1852
  const reconnect = prediction.reconnects === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
1818
1853
  const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect, noiseOf(cell));
1819
- const left = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
1820
- const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left, noiseOf(cell));
1854
+ const closed = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
1855
+ const { left, exited } = closed === void 0 || drop === void 0 || prediction.exits !== true ? {
1856
+ left: closed,
1857
+ exited: void 0
1858
+ } : closeOrExit(drop.line, closed, exitOf(run, cookie), noiseOf(cell));
1859
+ const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left, noiseOf(cell), exited);
1821
1860
  const found = [
1822
1861
  matches ? null : `it ended with ${words}, not ${prediction.errno}`,
1823
1862
  late,
@@ -1845,6 +1884,7 @@ const measureSocket = (cell, run) => {
1845
1884
  steps,
1846
1885
  ...reconnect === void 0 ? {} : { reconnect },
1847
1886
  ...left === void 0 ? {} : { left },
1887
+ ...exited === void 0 ? {} : { exited },
1848
1888
  ...cell.probe.ws !== void 0 && inflight ? { call: callOf(events, request, ended) } : {},
1849
1889
  void: voided,
1850
1890
  finding
@@ -4154,7 +4194,7 @@ const probeArgs = (cell, lab) => {
4154
4194
  ];
4155
4195
  if (probe.ws !== void 0) {
4156
4196
  const { ws } = probe;
4157
- const waits = (prediction.detects === true ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4197
+ const waits = (prediction.detects !== false ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4158
4198
  args.push("--websocket", ws.socket);
4159
4199
  args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
4160
4200
  if (ws.heartbeat !== null) args.push("--heartbeat-interval-ms", String(valueIn(ws.heartbeat.interval, "ms")), "--heartbeat-timeout-ms", String(valueIn(ws.heartbeat.timeout, "ms")), "--heartbeat-misses", String(ws.heartbeat.misses));
@@ -4531,7 +4571,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
4531
4571
  if (toxicFirst) await lab.toxic(null);
4532
4572
  }
4533
4573
  if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
4534
- const expected = cell.prediction.detects === true ? cell.prediction.reconnects : void 0;
4574
+ const expected = cell.prediction.detects !== false ? cell.prediction.reconnects : void 0;
4535
4575
  if (expected !== void 0) {
4536
4576
  const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4537
4577
  await dials(probe, 1 + expected.waits.length, waits * 2e3 + 5e3);
@@ -4717,6 +4757,6 @@ const runCell = async (cell, say = () => {}) => {
4717
4757
  }
4718
4758
  };
4719
4759
  //#endregion
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 };
4760
+ 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, closeOrExit, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, 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 };
4721
4761
 
4722
4762
  //# sourceMappingURL=index.js.map