@mark1russell7/rigor-chaos 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,29 @@ A schedule is data. A driver reads `times`, and it sends each arrival at its tim
27
27
  - `service` is the reply less the send. This is what a closed-loop driver measures.
28
28
  - `lag` is the send less the planned time.
29
29
  - `censored` holds the age at the end of each arrival with no reply. Each value is a lower bound of a latency.
30
+ - `seconds` is optional. It holds the samples that the driver read of itself: the processor share, and the p99 and the largest delay of its event loop.
30
31
 
31
32
  `latencyQuantile` reads the latency and the censored values together, by the nearest rank. Thus an arrival with no reply does not leave the measure.
32
33
 
33
- `scheduleHeld` tells if the driver kept the schedule. A run that fails this check measures its driver, not its target. An A/A run with no fault examines this first.
34
+ `scheduleHeld` tells if the driver kept the schedule. A run that fails this check measures its driver, not its target. An A/A run with no fault examines this first. When a run fails this check, `seconds` tells if a full processor or a held event loop made the sends late.
35
+
36
+ ## A closed-loop record
37
+
38
+ `closedLoopPlan` reads a run of a closed-loop driver that sends each `interval` ms. It puts back the sends that each stall hid, by the rule of `recordValueWithExpectedInterval` of HdrHistogram.
39
+
40
+ ```ts
41
+ import { closedLoopPlan, latencyQuantile, measureLoad } from "@mark1russell7/rigor-chaos";
42
+
43
+ const { plan, completions, driverGaps } = closedLoopPlan(requests, { interval: 10, end, tolerance: 2 });
44
+ const p99 = latencyQuantile(measureLoad(plan, completions, end), 0.99);
45
+ ```
46
+
47
+ - A request with a reply later than one interval hides the planned sends at `sent + j × interval` before that reply.
48
+ - Each hidden send gets the reply that ended the stall. Its lag is its wait, and its service is 0.
49
+ - A pause of the driver itself also hides sends. The driver expects its next send at the later of `sent + interval` and the reply.
50
+ - A send later than that time by more than `tolerance` is a driver gap. Each planned send in the gap is hidden, and it gets the real send and its reply.
51
+ - `driverGaps` and `driverGapTotal` count these gaps and their delay in ms. A pause of the driver thus does not make the estimate better than the open loop.
52
+ - The plan starts again at each real send. The driver does not send faster after a stall, thus a later send is not late.
53
+ - After a request with no reply, each hidden send is not sent, and its age at the end is a lower bound.
54
+
55
+ The result is an estimate of the open loop, not a measure of it. A record of an open-loop driver stays the better measure.
@@ -0,0 +1,56 @@
1
+ import { Schedule } from "./schedule.js";
2
+ import { Completion } from "./measure.js";
3
+ /**
4
+ * A closed-loop driver sends its next request only after the reply, thus a stall hides the sends of
5
+ * its interval. This module puts those hidden sends back, by the rule of `recordValueWithExpectedInterval`
6
+ * of HdrHistogram. A request with a reply later than one interval hides the planned sends at
7
+ * `sent + j × interval` before that reply. Each hidden send gets the reply that ended the stall.
8
+ *
9
+ * A pause of the driver itself also hides sends. The driver expects its next send at the later of
10
+ * `sent + interval` and the reply. A send past that time and the tolerance is a driver gap. Each
11
+ * planned send in the gap is hidden too, and it gets the reply of the real send that ended the gap.
12
+ *
13
+ * The plan starts again at each real send. A global plan of `first + n × interval` is not correct for
14
+ * such a driver. The driver does not send faster after a stall, thus one stall makes each later send late.
15
+ */
16
+ /** A request is what a closed-loop driver sent and saw. Its times are in ms from the start of the run. */
17
+ export interface LoopRequest {
18
+ readonly sent: number;
19
+ /** This time is null for a request with no reply at the end of the run. Only the last request has it. */
20
+ readonly done: number | null;
21
+ }
22
+ /** These options give the plan of the driver and the end of the run, in ms. */
23
+ export interface ClosedLoopOptions {
24
+ /** This time is the planned gap between two sends. */
25
+ readonly interval: number;
26
+ /** This time is the end of the run, from the start of the run. */
27
+ readonly end: number;
28
+ /** A send later than its expected time by more than this tolerance is a driver gap. */
29
+ readonly tolerance: number;
30
+ }
31
+ /** This is a closed-loop run in the terms of `measureLoad`. */
32
+ export interface ClosedLoopRun {
33
+ /** This plan has each real send and each hidden send, in order. Its profile is the rate of the interval. */
34
+ readonly plan: Schedule;
35
+ /**
36
+ * These completions are those of the real sends and of the hidden sends that a reply ended. A hidden
37
+ * send of a stall has the reply of the stall as its send. Thus its lag is its wait, and its service is 0.
38
+ * A hidden send of a driver gap has the real send that ended the gap as its send.
39
+ * A hidden send before a request with no reply, or after it, has no completion: `measureLoad` gives it as not sent.
40
+ */
41
+ readonly completions: readonly Completion[];
42
+ /** This count is the number of hidden sends that the function put back, for stalls and driver gaps. */
43
+ readonly hidden: number;
44
+ /** This count is the number of sends later than their expected time by more than the tolerance. */
45
+ readonly driverGaps: number;
46
+ /** This time is the sum of those delays past the expected time, in ms. */
47
+ readonly driverGapTotal: number;
48
+ }
49
+ /**
50
+ * This function gives the plan and the completions of a closed-loop run, for `measureLoad`. The requests
51
+ * are in the order of their sends. The function throws a `RangeError` for a send before the reply of the
52
+ * request before it. It throws the same error for a missing reply before the last request. A time past
53
+ * the end and a bad option also give this error.
54
+ */
55
+ export declare function closedLoopPlan(requests: readonly LoopRequest[], options: ClosedLoopOptions): ClosedLoopRun;
56
+ //# sourceMappingURL=closed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed.d.ts","names":[],"sources":["../src/closed.ts"],"mappings":";;;;;;;;;;;;;;;;iBAkBiB;WACN;;WAEA;;;iBAIM;;WAEN;;WAEA;;WAEA;;;iBAIM;;WAEN,MAAM;;;;;;;WAON,sBAAsB;;WAEtB;;WAEA;;WAEA;;;;;;;;wBAwBK,eAAe,mBAAmB,eAAe,SAAS,oBAAoB"}
@@ -0,0 +1,74 @@
1
+ import { SCHEDULE_FORMAT, constant } from "./schedule.js";
2
+ // src/closed.ts
3
+ /** This function throws a `RangeError` for options or a request list that one closed loop cannot give. */
4
+ function check(requests, { interval, end, tolerance }) {
5
+ if (!(interval > 0 && Number.isFinite(interval))) throw new RangeError(`closedLoopPlan: the interval is more than 0, not ${interval}.`);
6
+ if (!(end > 0 && Number.isFinite(end))) throw new RangeError(`closedLoopPlan: the end is more than 0, not ${end}.`);
7
+ if (!(tolerance >= 0 && Number.isFinite(tolerance))) throw new RangeError(`closedLoopPlan: the tolerance is 0 or more, not ${tolerance}.`);
8
+ let free = 0;
9
+ requests.forEach(({ sent, done }, index) => {
10
+ if (!(sent >= free && Number.isFinite(sent))) throw new RangeError(`closedLoopPlan: the request ${index} has a send at ${sent}, before the reply at ${free}.`);
11
+ if (done === null && index !== requests.length - 1) throw new RangeError(`closedLoopPlan: the request ${index} has no reply, and it is not the last request.`);
12
+ const last = done ?? sent;
13
+ if (!(last >= sent && last <= end)) throw new RangeError(`closedLoopPlan: the request ${index} has a send at ${sent} and a reply at ${done}, in a run that stops at ${end}.`);
14
+ free = last;
15
+ });
16
+ }
17
+ /**
18
+ * This function gives the plan and the completions of a closed-loop run, for `measureLoad`. The requests
19
+ * are in the order of their sends. The function throws a `RangeError` for a send before the reply of the
20
+ * request before it. It throws the same error for a missing reply before the last request. A time past
21
+ * the end and a bad option also give this error.
22
+ */
23
+ function closedLoopPlan(requests, options) {
24
+ check(requests, options);
25
+ const { interval, end, tolerance } = options;
26
+ const times = [];
27
+ const completions = [];
28
+ let hidden = 0;
29
+ let driverGaps = 0;
30
+ let driverGapTotal = 0;
31
+ /** This function adds a hidden send planned at `at`. It has a completion only when `done` is a reply. */
32
+ const hide = (at, send, done) => {
33
+ if (done !== null) completions.push({
34
+ index: times.length,
35
+ sent: send,
36
+ done
37
+ });
38
+ times.push(at);
39
+ hidden += 1;
40
+ };
41
+ let expected = null;
42
+ for (const { sent, done } of requests) {
43
+ if (expected !== null && sent - expected > tolerance) {
44
+ driverGaps += 1;
45
+ driverGapTotal += sent - expected;
46
+ for (let step = 0; expected + step * interval < sent; step += 1) hide(expected + step * interval, sent, done);
47
+ }
48
+ completions.push({
49
+ index: times.length,
50
+ sent,
51
+ done
52
+ });
53
+ times.push(sent);
54
+ const stop = done ?? end;
55
+ for (let step = 1; sent + step * interval < stop; step += 1) hide(sent + step * interval, done ?? sent, done);
56
+ expected = done === null ? null : Math.max(sent + interval, done);
57
+ }
58
+ return {
59
+ plan: {
60
+ format: SCHEDULE_FORMAT,
61
+ arrivals: "uniform",
62
+ seed: null,
63
+ profile: constant(1e3 / interval, end),
64
+ times
65
+ },
66
+ completions,
67
+ hidden,
68
+ driverGaps,
69
+ driverGapTotal
70
+ };
71
+ }
72
+ export { closedLoopPlan };
73
+
74
+ //# sourceMappingURL=closed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed.js","names":[],"sources":["../src/closed.ts"],"sourcesContent":["import type { Completion } from \"./measure.ts\";\nimport { SCHEDULE_FORMAT, constant, type Schedule } from \"./schedule.ts\";\n\n/**\n * A closed-loop driver sends its next request only after the reply, thus a stall hides the sends of\n * its interval. This module puts those hidden sends back, by the rule of `recordValueWithExpectedInterval`\n * of HdrHistogram. A request with a reply later than one interval hides the planned sends at\n * `sent + j × interval` before that reply. Each hidden send gets the reply that ended the stall.\n *\n * A pause of the driver itself also hides sends. The driver expects its next send at the later of\n * `sent + interval` and the reply. A send past that time and the tolerance is a driver gap. Each\n * planned send in the gap is hidden too, and it gets the reply of the real send that ended the gap.\n *\n * The plan starts again at each real send. A global plan of `first + n × interval` is not correct for\n * such a driver. The driver does not send faster after a stall, thus one stall makes each later send late.\n */\n\n/** A request is what a closed-loop driver sent and saw. Its times are in ms from the start of the run. */\nexport interface LoopRequest {\n readonly sent: number;\n /** This time is null for a request with no reply at the end of the run. Only the last request has it. */\n readonly done: number | null;\n}\n\n/** These options give the plan of the driver and the end of the run, in ms. */\nexport interface ClosedLoopOptions {\n /** This time is the planned gap between two sends. */\n readonly interval: number;\n /** This time is the end of the run, from the start of the run. */\n readonly end: number;\n /** A send later than its expected time by more than this tolerance is a driver gap. */\n readonly tolerance: number;\n}\n\n/** This is a closed-loop run in the terms of `measureLoad`. */\nexport interface ClosedLoopRun {\n /** This plan has each real send and each hidden send, in order. Its profile is the rate of the interval. */\n readonly plan: Schedule;\n /**\n * These completions are those of the real sends and of the hidden sends that a reply ended. A hidden\n * send of a stall has the reply of the stall as its send. Thus its lag is its wait, and its service is 0.\n * A hidden send of a driver gap has the real send that ended the gap as its send.\n * A hidden send before a request with no reply, or after it, has no completion: `measureLoad` gives it as not sent.\n */\n readonly completions: readonly Completion[];\n /** This count is the number of hidden sends that the function put back, for stalls and driver gaps. */\n readonly hidden: number;\n /** This count is the number of sends later than their expected time by more than the tolerance. */\n readonly driverGaps: number;\n /** This time is the sum of those delays past the expected time, in ms. */\n readonly driverGapTotal: number;\n}\n\n/** This function throws a `RangeError` for options or a request list that one closed loop cannot give. */\nfunction check(requests: readonly LoopRequest[], { interval, end, tolerance }: ClosedLoopOptions): void {\n if (!(interval > 0 && Number.isFinite(interval))) throw new RangeError(`closedLoopPlan: the interval is more than 0, not ${interval}.`);\n if (!(end > 0 && Number.isFinite(end))) throw new RangeError(`closedLoopPlan: the end is more than 0, not ${end}.`);\n if (!(tolerance >= 0 && Number.isFinite(tolerance))) throw new RangeError(`closedLoopPlan: the tolerance is 0 or more, not ${tolerance}.`);\n let free = 0;\n requests.forEach(({ sent, done }, index) => {\n if (!(sent >= free && Number.isFinite(sent))) throw new RangeError(`closedLoopPlan: the request ${index} has a send at ${sent}, before the reply at ${free}.`);\n if (done === null && index !== requests.length - 1) throw new RangeError(`closedLoopPlan: the request ${index} has no reply, and it is not the last request.`);\n const last = done ?? sent;\n if (!(last >= sent && last <= end)) throw new RangeError(`closedLoopPlan: the request ${index} has a send at ${sent} and a reply at ${done}, in a run that stops at ${end}.`);\n free = last;\n });\n}\n\n/**\n * This function gives the plan and the completions of a closed-loop run, for `measureLoad`. The requests\n * are in the order of their sends. The function throws a `RangeError` for a send before the reply of the\n * request before it. It throws the same error for a missing reply before the last request. A time past\n * the end and a bad option also give this error.\n */\nexport function closedLoopPlan(requests: readonly LoopRequest[], options: ClosedLoopOptions): ClosedLoopRun {\n check(requests, options);\n const { interval, end, tolerance } = options;\n const times: number[] = [];\n const completions: Completion[] = [];\n let hidden = 0;\n let driverGaps = 0;\n let driverGapTotal = 0;\n /** This function adds a hidden send planned at `at`. It has a completion only when `done` is a reply. */\n const hide = (at: number, send: number, done: number | null): void => {\n if (done !== null) completions.push({ index: times.length, sent: send, done });\n times.push(at);\n hidden += 1;\n };\n let expected: number | null = null;\n for (const { sent, done } of requests) {\n if (expected !== null && sent - expected > tolerance) {\n driverGaps += 1;\n driverGapTotal += sent - expected;\n for (let step = 0; expected + step * interval < sent; step += 1) hide(expected + step * interval, sent, done);\n }\n completions.push({ index: times.length, sent, done });\n times.push(sent);\n const stop = done ?? end;\n for (let step = 1; sent + step * interval < stop; step += 1) hide(sent + step * interval, done ?? sent, done);\n expected = done === null ? null : Math.max(sent + interval, done);\n }\n const plan: Schedule = { format: SCHEDULE_FORMAT, arrivals: \"uniform\", seed: null, profile: constant(1000 / interval, end), times };\n return { plan, completions, hidden, driverGaps, driverGapTotal };\n}\n"],"mappings":";;;AAsDA,SAAS,MAAM,UAAkC,EAAE,UAAU,KAAK,aAAsC;CACtG,IAAI,EAAE,WAAW,KAAK,OAAO,SAAS,QAAQ,IAAI,MAAM,IAAI,WAAW,oDAAoD,SAAS,EAAE;CACtI,IAAI,EAAE,MAAM,KAAK,OAAO,SAAS,GAAG,IAAI,MAAM,IAAI,WAAW,+CAA+C,IAAI,EAAE;CAClH,IAAI,EAAE,aAAa,KAAK,OAAO,SAAS,SAAS,IAAI,MAAM,IAAI,WAAW,mDAAmD,UAAU,EAAE;CACzI,IAAI,OAAO;CACX,SAAS,SAAS,EAAE,MAAM,QAAQ,UAAU;EAC1C,IAAI,EAAE,QAAQ,QAAQ,OAAO,SAAS,IAAI,IAAI,MAAM,IAAI,WAAW,+BAA+B,MAAM,iBAAiB,KAAK,wBAAwB,KAAK,EAAE;EAC7J,IAAI,SAAS,QAAQ,UAAU,SAAS,SAAS,GAAG,MAAM,IAAI,WAAW,+BAA+B,MAAM,+CAA+C;EAC7J,MAAM,OAAO,QAAQ;EACrB,IAAI,EAAE,QAAQ,QAAQ,QAAQ,MAAM,MAAM,IAAI,WAAW,+BAA+B,MAAM,iBAAiB,KAAK,kBAAkB,KAAK,2BAA2B,IAAI,EAAE;EAC5K,OAAO;CACT,CAAC;AACH;;;;;;;AAQA,SAAgB,eAAe,UAAkC,SAA2C;CAC1G,MAAM,UAAU,OAAO;CACvB,MAAM,EAAE,UAAU,KAAK,cAAc;CACrC,MAAM,QAAkB,CAAC;CACzB,MAAM,cAA4B,CAAC;CACnC,IAAI,SAAS;CACb,IAAI,aAAa;CACjB,IAAI,iBAAiB;;CAErB,MAAM,QAAQ,IAAY,MAAc,SAA8B;EACpE,IAAI,SAAS,MAAM,YAAY,KAAK;GAAE,OAAO,MAAM;GAAQ,MAAM;GAAM;EAAK,CAAC;EAC7E,MAAM,KAAK,EAAE;EACb,UAAU;CACZ;CACA,IAAI,WAA0B;CAC9B,KAAK,MAAM,EAAE,MAAM,UAAU,UAAU;EACrC,IAAI,aAAa,QAAQ,OAAO,WAAW,WAAW;GACpD,cAAc;GACd,kBAAkB,OAAO;GACzB,KAAK,IAAI,OAAO,GAAG,WAAW,OAAO,WAAW,MAAM,QAAQ,GAAG,KAAK,WAAW,OAAO,UAAU,MAAM,IAAI;EAC9G;EACA,YAAY,KAAK;GAAE,OAAO,MAAM;GAAQ;GAAM;EAAK,CAAC;EACpD,MAAM,KAAK,IAAI;EACf,MAAM,OAAO,QAAQ;EACrB,KAAK,IAAI,OAAO,GAAG,OAAO,OAAO,WAAW,MAAM,QAAQ,GAAG,KAAK,OAAO,OAAO,UAAU,QAAQ,MAAM,IAAI;EAC5G,WAAW,SAAS,OAAO,OAAO,KAAK,IAAI,OAAO,UAAU,IAAI;CAClE;CAEA,OAAO;EAAE,MAAA;GADgB,QAAQ;GAAiB,UAAU;GAAW,MAAM;GAAM,SAAS,SAAS,MAAO,UAAU,GAAG;GAAG;EAChH;EAAG;EAAa;EAAQ;EAAY;CAAe;AACjE"}
package/build/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { Arrivals, Phase, Profile, SCHEDULE_FORMAT, Schedule, ScheduleOptions, checkProfile, constant, expectedArrivals, ramp, schedule, spike, steps } from "./schedule.js";
2
- import { Completion, LoadMeasure, ScheduleCheck, latencyQuantile, measureLoad, scheduleHeld } from "./measure.js";
3
- export { type Arrivals, type Completion, type LoadMeasure, type Phase, type Profile, SCHEDULE_FORMAT, type Schedule, type ScheduleCheck, type ScheduleOptions, checkProfile, constant, expectedArrivals, latencyQuantile, measureLoad, ramp, schedule, scheduleHeld, spike, steps };
2
+ import { Completion, LoadMeasure, ScheduleCheck, SecondSample, latencyQuantile, measureLoad, scheduleHeld } from "./measure.js";
3
+ import { ClosedLoopOptions, ClosedLoopRun, LoopRequest, closedLoopPlan } from "./closed.js";
4
+ export { type Arrivals, type ClosedLoopOptions, type ClosedLoopRun, type Completion, type LoadMeasure, type LoopRequest, type Phase, type Profile, SCHEDULE_FORMAT, type Schedule, type ScheduleCheck, type ScheduleOptions, type SecondSample, checkProfile, closedLoopPlan, constant, expectedArrivals, latencyQuantile, measureLoad, ramp, schedule, scheduleHeld, spike, steps };
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
- import { latencyQuantile, measureLoad, scheduleHeld } from "./measure.js";
2
1
  import { SCHEDULE_FORMAT, checkProfile, constant, expectedArrivals, ramp, schedule, spike, steps } from "./schedule.js";
3
- export { SCHEDULE_FORMAT, checkProfile, constant, expectedArrivals, latencyQuantile, measureLoad, ramp, schedule, scheduleHeld, spike, steps };
2
+ import { closedLoopPlan } from "./closed.js";
3
+ import { latencyQuantile, measureLoad, scheduleHeld } from "./measure.js";
4
+ export { SCHEDULE_FORMAT, checkProfile, closedLoopPlan, constant, expectedArrivals, latencyQuantile, measureLoad, ramp, schedule, scheduleHeld, spike, steps };
@@ -15,6 +15,20 @@ export interface Completion {
15
15
  /** This time is null for an arrival with no reply. */
16
16
  readonly done: number | null;
17
17
  }
18
+ /**
19
+ * A second sample is what a driver process read of itself over one second of a run. It tells if a late
20
+ * send came from a full processor or from a held event loop. Each reading is optional.
21
+ */
22
+ export interface SecondSample {
23
+ /** This time is the start of the second, in ms from the start of the schedule. */
24
+ readonly at: number;
25
+ /** This share is the processor time of the process over the time of the sample. One full core reads 1. */
26
+ readonly cpu?: number;
27
+ /** This time is the p99 of the delay of the event loop in this second, in ms. */
28
+ readonly loopP99?: number;
29
+ /** This time is the largest delay of the event loop in this second, in ms. */
30
+ readonly loopMax?: number;
31
+ }
18
32
  /** This is the measure of one run of a schedule. Each list is in the order of the schedule. */
19
33
  export interface LoadMeasure {
20
34
  readonly planned: number;
@@ -30,13 +44,18 @@ export interface LoadMeasure {
30
44
  readonly lag: readonly number[];
31
45
  /** These times are the age at the end of each arrival with no reply. Each one is a lower bound of a latency. */
32
46
  readonly censored: readonly number[];
47
+ /** These samples are the seconds that the driver read, in order of time. A run with no list of samples has no such field. */
48
+ readonly seconds?: readonly SecondSample[];
33
49
  }
34
50
  /**
35
51
  * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the
36
52
  * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of
37
53
  * one index. It throws the same error for a reply before its send and for a time past `end`.
54
+ *
55
+ * `seconds` is optional, and the measure holds it as given. A sample must be in order and inside the
56
+ * run, and each reading must be finite and not negative. Otherwise the function throws the same error.
38
57
  */
39
- export declare function measureLoad(plan: Schedule, completions: readonly Completion[], end: number): LoadMeasure;
58
+ export declare function measureLoad(plan: Schedule, completions: readonly Completion[], end: number, seconds?: readonly SecondSample[]): LoadMeasure;
40
59
  /**
41
60
  * This function gives a quantile of the latency, with each arrival with no reply at its lower bound.
42
61
  * With a censored value, the result is a lower bound of the true quantile. It uses the nearest rank,
@@ -1 +1 @@
1
- {"version":3,"file":"measure.d.ts","names":[],"sources":["../src/measure.ts"],"mappings":";;;;;;;;;;iBAaiB;;WAEN;WACA;;WAEA;;;iBAIM;WACN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;;;wBAQK,YAAY,MAAM,UAAU,sBAAsB,cAAc,cAAc;;;;;;wBAmC9E,gBAAgB,SAAS,aAAa;;iBAKrC;WACN;;WAEA;;WAEA;WACA;;;;;;;wBAQK,aAAa,SAAS,aAAa,gBAAgB"}
1
+ {"version":3,"file":"measure.d.ts","names":[],"sources":["../src/measure.ts"],"mappings":";;;;;;;;;;iBAaiB;;WAEN;WACA;;WAEA;;;;;;iBAOM;;WAEN;;WAEA;;WAEA;;WAEA;;;iBAIM;WACN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA,mBAAmB;;;;;;;;;;wBAwBd,YAAY,MAAM,UAAU,sBAAsB,cAAc,aAAa,mBAAmB,iBAAiB;;;;;;wBAoCjH,gBAAgB,SAAS,aAAa;;iBAKrC;WACN;;WAEA;;WAEA;WACA;;;;;;;wBAQK,aAAa,SAAS,aAAa,gBAAgB"}
package/build/measure.js CHANGED
@@ -1,11 +1,29 @@
1
1
  import { quantile } from "@mark1russell7/rigor-statistics";
2
2
  // src/measure.ts
3
+ /** This function throws a `RangeError` for a sample that a measure of this run cannot hold. */
4
+ function checkSeconds(seconds, end) {
5
+ let last = Number.NEGATIVE_INFINITY;
6
+ for (const { at, cpu, loopP99, loopMax } of seconds) {
7
+ if (!(at >= 0 && at <= end && at > last)) throw new RangeError(`measureLoad: the sample at ${at} is not in order, in a run that stops at ${end}.`);
8
+ last = at;
9
+ for (const value of [
10
+ cpu,
11
+ loopP99,
12
+ loopMax
13
+ ]) if (value !== void 0 && !(value >= 0 && Number.isFinite(value))) throw new RangeError(`measureLoad: the sample at ${at} has the reading ${value}.`);
14
+ if (loopP99 !== void 0 && loopMax !== void 0 && loopMax < loopP99) throw new RangeError(`measureLoad: the sample at ${at} has a largest delay of ${loopMax} under its p99 of ${loopP99}.`);
15
+ }
16
+ }
3
17
  /**
4
18
  * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the
5
19
  * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of
6
20
  * one index. It throws the same error for a reply before its send and for a time past `end`.
21
+ *
22
+ * `seconds` is optional, and the measure holds it as given. A sample must be in order and inside the
23
+ * run, and each reading must be finite and not negative. Otherwise the function throws the same error.
7
24
  */
8
- function measureLoad(plan, completions, end) {
25
+ function measureLoad(plan, completions, end, seconds) {
26
+ if (seconds !== void 0) checkSeconds(seconds, end);
9
27
  const byIndex = /* @__PURE__ */ new Map();
10
28
  for (const completion of completions) {
11
29
  if (!Number.isInteger(completion.index) || completion.index < 0 || completion.index >= plan.times.length) throw new RangeError(`measureLoad: the index ${completion.index} is not in the schedule of ${plan.times.length} arrivals.`);
@@ -37,7 +55,8 @@ function measureLoad(plan, completions, end) {
37
55
  latency,
38
56
  service,
39
57
  lag,
40
- censored
58
+ censored,
59
+ ...seconds === void 0 ? {} : { seconds }
41
60
  };
42
61
  }
43
62
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"measure.js","names":[],"sources":["../src/measure.ts"],"sourcesContent":["import { quantile } from \"@mark1russell7/rigor-statistics\";\nimport type { Schedule } from \"./schedule.ts\";\n\n/**\n * A closed-loop driver waits for a reply before its next request, thus a stall hides the requests\n * of its interval. This is coordinated omission. This module counts each latency from\n * the planned time, thus a stall counts in each arrival that it delayed.\n *\n * An arrival with no reply at the end of a run does not leave the measure. Its age at the end is a\n * lower bound of its latency, and a quantile reads it.\n */\n\n/** A completion is what a driver saw of one arrival. Its times are in ms from the start of the schedule. */\nexport interface Completion {\n /** This number is the position of the arrival in the schedule. */\n readonly index: number;\n readonly sent: number;\n /** This time is null for an arrival with no reply. */\n readonly done: number | null;\n}\n\n/** This is the measure of one run of a schedule. Each list is in the order of the schedule. */\nexport interface LoadMeasure {\n readonly planned: number;\n /** This count is the number of arrivals that the driver did not send. */\n readonly unsent: number;\n /** This count is the number of arrivals that had no reply at the end of the run. */\n readonly unfinished: number;\n /** These times are the reply less the planned time: what a user waited. */\n readonly latency: readonly number[];\n /** These times are the reply less the send: the measure of a closed-loop driver. */\n readonly service: readonly number[];\n /** These times are the send less the planned time, for each arrival that the driver sent. */\n readonly lag: readonly number[];\n /** These times are the age at the end of each arrival with no reply. Each one is a lower bound of a latency. */\n readonly censored: readonly number[];\n}\n\n/**\n * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the\n * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of\n * one index. It throws the same error for a reply before its send and for a time past `end`.\n */\nexport function measureLoad(plan: Schedule, completions: readonly Completion[], end: number): LoadMeasure {\n const byIndex = new Map<number, Completion>();\n for (const completion of completions) {\n if (!Number.isInteger(completion.index) || completion.index < 0 || completion.index >= plan.times.length)\n throw new RangeError(`measureLoad: the index ${completion.index} is not in the schedule of ${plan.times.length} arrivals.`);\n if (byIndex.has(completion.index)) throw new RangeError(`measureLoad: the index ${completion.index} has two completions.`);\n const last = completion.done ?? completion.sent;\n if (!(completion.sent <= last && last <= end))\n throw new RangeError(`measureLoad: the index ${completion.index} has a send at ${completion.sent} and a reply at ${completion.done}, in a run that stops at ${end}.`);\n byIndex.set(completion.index, completion);\n }\n const latency: number[] = [];\n const service: number[] = [];\n const lag: number[] = [];\n const censored: number[] = [];\n let unfinished = 0;\n plan.times.forEach((planned, index) => {\n const completion = byIndex.get(index);\n if (completion !== undefined) lag.push(completion.sent - planned);\n if (completion === undefined || completion.done === null) {\n if (completion !== undefined) unfinished += 1;\n censored.push(Math.max(0, end - planned));\n return;\n }\n latency.push(completion.done - planned);\n service.push(completion.done - completion.sent);\n });\n return { planned: plan.times.length, unsent: plan.times.length - byIndex.size, unfinished, latency, service, lag, censored };\n}\n\n/**\n * This function gives a quantile of the latency, with each arrival with no reply at its lower bound.\n * With a censored value, the result is a lower bound of the true quantile. It uses the nearest rank,\n * thus the result is a measured value. It throws a `RangeError` for a run with no arrivals.\n */\nexport function latencyQuantile(measure: LoadMeasure, p: number): number {\n return quantile([...measure.latency, ...measure.censored], p, \"nearest-rank\");\n}\n\n/** This is the check of a driver: the schedule held, or the reason that it did not. */\nexport interface ScheduleCheck {\n readonly held: boolean;\n /** This time is the largest size of a lag, or null with no send. */\n readonly worst: number | null;\n /** This count is the number of sends past the limit. */\n readonly late: number;\n readonly reason: string;\n}\n\n/**\n * This function tells if a driver kept the schedule: it sent each arrival, each within `limit` ms of\n * its planned time. A run that fails this check measures its driver, not the target, thus an A/A run\n * with no fault reads it first.\n */\nexport function scheduleHeld(measure: LoadMeasure, limit: number): ScheduleCheck {\n const worst = measure.lag.reduce<number | null>((most, lag) => Math.max(most ?? 0, Math.abs(lag)), null);\n const late = measure.lag.filter((lag) => Math.abs(lag) > limit).length;\n const held = measure.unsent === 0 && late === 0;\n const facts = `${measure.planned} planned, ${measure.unsent} not sent, ${late} past ${limit} ms, the worst ${worst ?? \"none\"} ms`;\n return { held, worst, late, reason: `The driver ${held ? \"kept\" : \"did not keep\"} the schedule (${facts}).` };\n}\n"],"mappings":";;;;;;;AA2CA,SAAgB,YAAY,MAAgB,aAAoC,KAA0B;CACxG,MAAM,0BAAU,IAAI,IAAwB;CAC5C,KAAK,MAAM,cAAc,aAAa;EACpC,IAAI,CAAC,OAAO,UAAU,WAAW,KAAK,KAAK,WAAW,QAAQ,KAAK,WAAW,SAAS,KAAK,MAAM,QAChG,MAAM,IAAI,WAAW,0BAA0B,WAAW,MAAM,6BAA6B,KAAK,MAAM,OAAO,WAAW;EAC5H,IAAI,QAAQ,IAAI,WAAW,KAAK,GAAG,MAAM,IAAI,WAAW,0BAA0B,WAAW,MAAM,sBAAsB;EACzH,MAAM,OAAO,WAAW,QAAQ,WAAW;EAC3C,IAAI,EAAE,WAAW,QAAQ,QAAQ,QAAQ,MACvC,MAAM,IAAI,WAAW,0BAA0B,WAAW,MAAM,iBAAiB,WAAW,KAAK,kBAAkB,WAAW,KAAK,2BAA2B,IAAI,EAAE;EACtK,QAAQ,IAAI,WAAW,OAAO,UAAU;CAC1C;CACA,MAAM,UAAoB,CAAC;CAC3B,MAAM,UAAoB,CAAC;CAC3B,MAAM,MAAgB,CAAC;CACvB,MAAM,WAAqB,CAAC;CAC5B,IAAI,aAAa;CACjB,KAAK,MAAM,SAAS,SAAS,UAAU;EACrC,MAAM,aAAa,QAAQ,IAAI,KAAK;EACpC,IAAI,eAAe,KAAA,GAAW,IAAI,KAAK,WAAW,OAAO,OAAO;EAChE,IAAI,eAAe,KAAA,KAAa,WAAW,SAAS,MAAM;GACxD,IAAI,eAAe,KAAA,GAAW,cAAc;GAC5C,SAAS,KAAK,KAAK,IAAI,GAAG,MAAM,OAAO,CAAC;GACxC;EACF;EACA,QAAQ,KAAK,WAAW,OAAO,OAAO;EACtC,QAAQ,KAAK,WAAW,OAAO,WAAW,IAAI;CAChD,CAAC;CACD,OAAO;EAAE,SAAS,KAAK,MAAM;EAAQ,QAAQ,KAAK,MAAM,SAAS,QAAQ;EAAM;EAAY;EAAS;EAAS;EAAK;CAAS;AAC7H;;;;;;AAOA,SAAgB,gBAAgB,SAAsB,GAAmB;CACvE,OAAO,SAAS,CAAC,GAAG,QAAQ,SAAS,GAAG,QAAQ,QAAQ,GAAG,GAAG,cAAc;AAC9E;;;;;;AAiBA,SAAgB,aAAa,SAAsB,OAA8B;CAC/E,MAAM,QAAQ,QAAQ,IAAI,QAAuB,MAAM,QAAQ,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,CAAC,GAAG,IAAI;CACvG,MAAM,OAAO,QAAQ,IAAI,QAAQ,QAAQ,KAAK,IAAI,GAAG,IAAI,KAAK,CAAC,CAAC;CAChE,MAAM,OAAO,QAAQ,WAAW,KAAK,SAAS;CAC9C,MAAM,QAAQ,GAAG,QAAQ,QAAQ,YAAY,QAAQ,OAAO,aAAa,KAAK,QAAQ,MAAM,iBAAiB,SAAS,OAAO;CAC7H,OAAO;EAAE;EAAM;EAAO;EAAM,QAAQ,cAAc,OAAO,SAAS,eAAe,iBAAiB,MAAM;CAAI;AAC9G"}
1
+ {"version":3,"file":"measure.js","names":[],"sources":["../src/measure.ts"],"sourcesContent":["import { quantile } from \"@mark1russell7/rigor-statistics\";\nimport type { Schedule } from \"./schedule.ts\";\n\n/**\n * A closed-loop driver waits for a reply before its next request, thus a stall hides the requests\n * of its interval. This is coordinated omission. This module counts each latency from\n * the planned time, thus a stall counts in each arrival that it delayed.\n *\n * An arrival with no reply at the end of a run does not leave the measure. Its age at the end is a\n * lower bound of its latency, and a quantile reads it.\n */\n\n/** A completion is what a driver saw of one arrival. Its times are in ms from the start of the schedule. */\nexport interface Completion {\n /** This number is the position of the arrival in the schedule. */\n readonly index: number;\n readonly sent: number;\n /** This time is null for an arrival with no reply. */\n readonly done: number | null;\n}\n\n/**\n * A second sample is what a driver process read of itself over one second of a run. It tells if a late\n * send came from a full processor or from a held event loop. Each reading is optional.\n */\nexport interface SecondSample {\n /** This time is the start of the second, in ms from the start of the schedule. */\n readonly at: number;\n /** This share is the processor time of the process over the time of the sample. One full core reads 1. */\n readonly cpu?: number;\n /** This time is the p99 of the delay of the event loop in this second, in ms. */\n readonly loopP99?: number;\n /** This time is the largest delay of the event loop in this second, in ms. */\n readonly loopMax?: number;\n}\n\n/** This is the measure of one run of a schedule. Each list is in the order of the schedule. */\nexport interface LoadMeasure {\n readonly planned: number;\n /** This count is the number of arrivals that the driver did not send. */\n readonly unsent: number;\n /** This count is the number of arrivals that had no reply at the end of the run. */\n readonly unfinished: number;\n /** These times are the reply less the planned time: what a user waited. */\n readonly latency: readonly number[];\n /** These times are the reply less the send: the measure of a closed-loop driver. */\n readonly service: readonly number[];\n /** These times are the send less the planned time, for each arrival that the driver sent. */\n readonly lag: readonly number[];\n /** These times are the age at the end of each arrival with no reply. Each one is a lower bound of a latency. */\n readonly censored: readonly number[];\n /** These samples are the seconds that the driver read, in order of time. A run with no list of samples has no such field. */\n readonly seconds?: readonly SecondSample[];\n}\n\n/** This function throws a `RangeError` for a sample that a measure of this run cannot hold. */\nfunction checkSeconds(seconds: readonly SecondSample[], end: number): void {\n let last = Number.NEGATIVE_INFINITY;\n for (const { at, cpu, loopP99, loopMax } of seconds) {\n if (!(at >= 0 && at <= end && at > last)) throw new RangeError(`measureLoad: the sample at ${at} is not in order, in a run that stops at ${end}.`);\n last = at;\n for (const value of [cpu, loopP99, loopMax])\n if (value !== undefined && !(value >= 0 && Number.isFinite(value))) throw new RangeError(`measureLoad: the sample at ${at} has the reading ${value}.`);\n if (loopP99 !== undefined && loopMax !== undefined && loopMax < loopP99)\n throw new RangeError(`measureLoad: the sample at ${at} has a largest delay of ${loopMax} under its p99 of ${loopP99}.`);\n }\n}\n\n/**\n * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the\n * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of\n * one index. It throws the same error for a reply before its send and for a time past `end`.\n *\n * `seconds` is optional, and the measure holds it as given. A sample must be in order and inside the\n * run, and each reading must be finite and not negative. Otherwise the function throws the same error.\n */\nexport function measureLoad(plan: Schedule, completions: readonly Completion[], end: number, seconds?: readonly SecondSample[]): LoadMeasure {\n if (seconds !== undefined) checkSeconds(seconds, end);\n const byIndex = new Map<number, Completion>();\n for (const completion of completions) {\n if (!Number.isInteger(completion.index) || completion.index < 0 || completion.index >= plan.times.length)\n throw new RangeError(`measureLoad: the index ${completion.index} is not in the schedule of ${plan.times.length} arrivals.`);\n if (byIndex.has(completion.index)) throw new RangeError(`measureLoad: the index ${completion.index} has two completions.`);\n const last = completion.done ?? completion.sent;\n if (!(completion.sent <= last && last <= end))\n throw new RangeError(`measureLoad: the index ${completion.index} has a send at ${completion.sent} and a reply at ${completion.done}, in a run that stops at ${end}.`);\n byIndex.set(completion.index, completion);\n }\n const latency: number[] = [];\n const service: number[] = [];\n const lag: number[] = [];\n const censored: number[] = [];\n let unfinished = 0;\n plan.times.forEach((planned, index) => {\n const completion = byIndex.get(index);\n if (completion !== undefined) lag.push(completion.sent - planned);\n if (completion === undefined || completion.done === null) {\n if (completion !== undefined) unfinished += 1;\n censored.push(Math.max(0, end - planned));\n return;\n }\n latency.push(completion.done - planned);\n service.push(completion.done - completion.sent);\n });\n return { planned: plan.times.length, unsent: plan.times.length - byIndex.size, unfinished, latency, service, lag, censored, ...(seconds === undefined ? {} : { seconds }) };\n}\n\n/**\n * This function gives a quantile of the latency, with each arrival with no reply at its lower bound.\n * With a censored value, the result is a lower bound of the true quantile. It uses the nearest rank,\n * thus the result is a measured value. It throws a `RangeError` for a run with no arrivals.\n */\nexport function latencyQuantile(measure: LoadMeasure, p: number): number {\n return quantile([...measure.latency, ...measure.censored], p, \"nearest-rank\");\n}\n\n/** This is the check of a driver: the schedule held, or the reason that it did not. */\nexport interface ScheduleCheck {\n readonly held: boolean;\n /** This time is the largest size of a lag, or null with no send. */\n readonly worst: number | null;\n /** This count is the number of sends past the limit. */\n readonly late: number;\n readonly reason: string;\n}\n\n/**\n * This function tells if a driver kept the schedule: it sent each arrival, each within `limit` ms of\n * its planned time. A run that fails this check measures its driver, not the target, thus an A/A run\n * with no fault reads it first.\n */\nexport function scheduleHeld(measure: LoadMeasure, limit: number): ScheduleCheck {\n const worst = measure.lag.reduce<number | null>((most, lag) => Math.max(most ?? 0, Math.abs(lag)), null);\n const late = measure.lag.filter((lag) => Math.abs(lag) > limit).length;\n const held = measure.unsent === 0 && late === 0;\n const facts = `${measure.planned} planned, ${measure.unsent} not sent, ${late} past ${limit} ms, the worst ${worst ?? \"none\"} ms`;\n return { held, worst, late, reason: `The driver ${held ? \"kept\" : \"did not keep\"} the schedule (${facts}).` };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/rigor-chaos",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "Open-loop load schedules with planned arrival times from a rate profile, and a measure of latency from the planned time with no coordinated omission.",
6
6
  "keywords": [
7
7
  "load",
@@ -38,8 +38,8 @@
38
38
  "node": ">=22"
39
39
  },
40
40
  "dependencies": {
41
- "@mark1russell7/rigor-generators": "^0.1.5",
42
- "@mark1russell7/rigor-statistics": "^0.1.11"
41
+ "@mark1russell7/rigor-generators": "^0.1.6",
42
+ "@mark1russell7/rigor-statistics": "^0.1.15"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@mark1russell7/cue": "^2.0.0",