@mark1russell7/rigor-chaos 0.0.0-bootstrap.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
1
  # @mark1russell7/rigor-chaos
2
2
 
3
- This version is a placeholder. The release workflow of https://github.com/mark1russell7/rigor publishes the real versions. Use a later version.
3
+ This package holds open-loop load schedules, and the measure of latency that goes with them.
4
+
5
+ A closed-loop driver waits for each reply before its next request. A stall of the target thus stops the load, and the measure shows one slow request. This is coordinated omission. An open-loop schedule fixes each arrival time before the run, and a measure counts each latency from its planned time.
6
+
7
+ ## A schedule
8
+
9
+ A profile is a rate over time, in straight pieces. `constant`, `ramp`, `steps` and `spike` make the common ones. `schedule` gives the planned times, in ms from the start.
10
+
11
+ ```ts
12
+ import { schedule, spike } from "@mark1russell7/rigor-chaos";
13
+
14
+ const plan = schedule(spike(100, 1000, { before: 10_000, width: 1000, after: 10_000 }), { arrivals: "poisson", seed: "drill" });
15
+ ```
16
+
17
+ - `uniform` arrivals have equal gaps at a fixed rate, and no random part.
18
+ - `poisson` arrivals have independent gaps. The seed gives the same times again on one platform.
19
+
20
+ A schedule is data. A driver reads `times`, and it sends each arrival at its time with no wait for a reply. The driver is not in this package, because it belongs to the process that makes the load. A record keeps the schedule, because `poisson` times use `Math.log`.
21
+
22
+ ## A measure
23
+
24
+ `measureLoad` takes the schedule, what the driver saw of each arrival, and the end of the run.
25
+
26
+ - `latency` is the reply less the planned time. This is what a user waited.
27
+ - `service` is the reply less the send. This is what a closed-loop driver measures.
28
+ - `lag` is the send less the planned time.
29
+ - `censored` holds the age at the end of each arrival with no reply. Each value is a lower bound of a latency.
30
+
31
+ `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
+ `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.
@@ -0,0 +1,3 @@
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 };
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { latencyQuantile, measureLoad, scheduleHeld } from "./measure.js";
2
+ 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 };
@@ -0,0 +1,61 @@
1
+ import { Schedule } from "./schedule.js";
2
+ /**
3
+ * A closed-loop driver waits for a reply before its next request, thus a stall hides the requests
4
+ * of its interval. This is coordinated omission. This module counts each latency from
5
+ * the planned time, thus a stall counts in each arrival that it delayed.
6
+ *
7
+ * An arrival with no reply at the end of a run does not leave the measure. Its age at the end is a
8
+ * lower bound of its latency, and a quantile reads it.
9
+ */
10
+ /** A completion is what a driver saw of one arrival. Its times are in ms from the start of the schedule. */
11
+ export interface Completion {
12
+ /** This number is the position of the arrival in the schedule. */
13
+ readonly index: number;
14
+ readonly sent: number;
15
+ /** This time is null for an arrival with no reply. */
16
+ readonly done: number | null;
17
+ }
18
+ /** This is the measure of one run of a schedule. Each list is in the order of the schedule. */
19
+ export interface LoadMeasure {
20
+ readonly planned: number;
21
+ /** This count is the number of arrivals that the driver did not send. */
22
+ readonly unsent: number;
23
+ /** This count is the number of arrivals that had no reply at the end of the run. */
24
+ readonly unfinished: number;
25
+ /** These times are the reply less the planned time: what a user waited. */
26
+ readonly latency: readonly number[];
27
+ /** These times are the reply less the send: the measure of a closed-loop driver. */
28
+ readonly service: readonly number[];
29
+ /** These times are the send less the planned time, for each arrival that the driver sent. */
30
+ readonly lag: readonly number[];
31
+ /** These times are the age at the end of each arrival with no reply. Each one is a lower bound of a latency. */
32
+ readonly censored: readonly number[];
33
+ }
34
+ /**
35
+ * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the
36
+ * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of
37
+ * one index. It throws the same error for a reply before its send and for a time past `end`.
38
+ */
39
+ export declare function measureLoad(plan: Schedule, completions: readonly Completion[], end: number): LoadMeasure;
40
+ /**
41
+ * This function gives a quantile of the latency, with each arrival with no reply at its lower bound.
42
+ * With a censored value, the result is a lower bound of the true quantile. It uses the nearest rank,
43
+ * thus the result is a measured value. It throws a `RangeError` for a run with no arrivals.
44
+ */
45
+ export declare function latencyQuantile(measure: LoadMeasure, p: number): number;
46
+ /** This is the check of a driver: the schedule held, or the reason that it did not. */
47
+ export interface ScheduleCheck {
48
+ readonly held: boolean;
49
+ /** This time is the largest size of a lag, or null with no send. */
50
+ readonly worst: number | null;
51
+ /** This count is the number of sends past the limit. */
52
+ readonly late: number;
53
+ readonly reason: string;
54
+ }
55
+ /**
56
+ * This function tells if a driver kept the schedule: it sent each arrival, each within `limit` ms of
57
+ * its planned time. A run that fails this check measures its driver, not the target, thus an A/A run
58
+ * with no fault reads it first.
59
+ */
60
+ export declare function scheduleHeld(measure: LoadMeasure, limit: number): ScheduleCheck;
61
+ //# sourceMappingURL=measure.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,70 @@
1
+ import { quantile } from "@mark1russell7/rigor-statistics";
2
+ // src/measure.ts
3
+ /**
4
+ * This function gives the measure of a run. `end` is the end of the run, in ms from the start of the
5
+ * schedule. It throws a `RangeError` for an index outside the schedule and for two completions of
6
+ * one index. It throws the same error for a reply before its send and for a time past `end`.
7
+ */
8
+ function measureLoad(plan, completions, end) {
9
+ const byIndex = /* @__PURE__ */ new Map();
10
+ for (const completion of completions) {
11
+ 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.`);
12
+ if (byIndex.has(completion.index)) throw new RangeError(`measureLoad: the index ${completion.index} has two completions.`);
13
+ const last = completion.done ?? completion.sent;
14
+ if (!(completion.sent <= last && last <= end)) 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}.`);
15
+ byIndex.set(completion.index, completion);
16
+ }
17
+ const latency = [];
18
+ const service = [];
19
+ const lag = [];
20
+ const censored = [];
21
+ let unfinished = 0;
22
+ plan.times.forEach((planned, index) => {
23
+ const completion = byIndex.get(index);
24
+ if (completion !== void 0) lag.push(completion.sent - planned);
25
+ if (completion === void 0 || completion.done === null) {
26
+ if (completion !== void 0) unfinished += 1;
27
+ censored.push(Math.max(0, end - planned));
28
+ return;
29
+ }
30
+ latency.push(completion.done - planned);
31
+ service.push(completion.done - completion.sent);
32
+ });
33
+ return {
34
+ planned: plan.times.length,
35
+ unsent: plan.times.length - byIndex.size,
36
+ unfinished,
37
+ latency,
38
+ service,
39
+ lag,
40
+ censored
41
+ };
42
+ }
43
+ /**
44
+ * This function gives a quantile of the latency, with each arrival with no reply at its lower bound.
45
+ * With a censored value, the result is a lower bound of the true quantile. It uses the nearest rank,
46
+ * thus the result is a measured value. It throws a `RangeError` for a run with no arrivals.
47
+ */
48
+ function latencyQuantile(measure, p) {
49
+ return quantile([...measure.latency, ...measure.censored], p, "nearest-rank");
50
+ }
51
+ /**
52
+ * This function tells if a driver kept the schedule: it sent each arrival, each within `limit` ms of
53
+ * its planned time. A run that fails this check measures its driver, not the target, thus an A/A run
54
+ * with no fault reads it first.
55
+ */
56
+ function scheduleHeld(measure, limit) {
57
+ const worst = measure.lag.reduce((most, lag) => Math.max(most ?? 0, Math.abs(lag)), null);
58
+ const late = measure.lag.filter((lag) => Math.abs(lag) > limit).length;
59
+ const held = measure.unsent === 0 && late === 0;
60
+ const facts = `${measure.planned} planned, ${measure.unsent} not sent, ${late} past ${limit} ms, the worst ${worst ?? "none"} ms`;
61
+ return {
62
+ held,
63
+ worst,
64
+ late,
65
+ reason: `The driver ${held ? "kept" : "did not keep"} the schedule (${facts}).`
66
+ };
67
+ }
68
+ export { latencyQuantile, measureLoad, scheduleHeld };
69
+
70
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * An open-loop schedule fixes the time of each arrival before the first request starts. No time
3
+ * depends on a reply. Thus a slow system cannot slow its own load, and a measure can count from the
4
+ * planned time.
5
+ *
6
+ * A profile is a rate over time, in straight pieces. One algorithm gives each arrival process. The
7
+ * cumulative rate turns a process of rate 1 into a process that follows the profile, by the
8
+ * time-rescaling theorem. `uniform` and `poisson` differ only in that process of rate 1.
9
+ */
10
+ /** This value is the format of a schedule. */
11
+ export declare const SCHEDULE_FORMAT = "rigor-schedule/1";
12
+ /** A phase is one straight piece of a profile. Its rate moves from `from` to `to`, in arrivals each second, over `ms`. */
13
+ export interface Phase {
14
+ readonly ms: number;
15
+ readonly from: number;
16
+ readonly to: number;
17
+ }
18
+ /** A profile is a list of phases, one after the other. */
19
+ export type Profile = readonly Phase[];
20
+ /** This word names the arrival process. `uniform` has no random part, and `poisson` has independent gaps. */
21
+ export type Arrivals = "uniform" | "poisson";
22
+ /** These options select the arrival process. Only `poisson` has a seed. */
23
+ export type ScheduleOptions = {
24
+ readonly arrivals: "uniform";
25
+ } | {
26
+ readonly arrivals: "poisson";
27
+ readonly seed: number | string;
28
+ };
29
+ /** A schedule is data, thus a record can keep the plan that a driver followed. */
30
+ export interface Schedule {
31
+ readonly format: typeof SCHEDULE_FORMAT;
32
+ readonly arrivals: Arrivals;
33
+ readonly seed: number | string | null;
34
+ readonly profile: Profile;
35
+ /** These are the planned times, in ms from the start, in order. */
36
+ readonly times: readonly number[];
37
+ }
38
+ /** This function gives one phase with a fixed rate. */
39
+ export declare function constant(rate: number, ms: number): Profile;
40
+ /** This function gives one phase with a rate that moves in a line. */
41
+ export declare function ramp(from: number, to: number, ms: number): Profile;
42
+ /** This function gives one fixed phase for each rate, each with the length `ms`. */
43
+ export declare function steps(rates: readonly number[], ms: number): Profile;
44
+ /** This function gives a fixed rate with one interval at a higher rate. */
45
+ export declare function spike(base: number, peak: number, shape: {
46
+ readonly before: number;
47
+ readonly width: number;
48
+ readonly after: number;
49
+ }): Profile;
50
+ /** This function gives each problem of a profile. An empty list means that the profile is valid. */
51
+ export declare function checkProfile(profile: Profile): readonly string[];
52
+ /** This function gives the number of arrivals of a profile on average: the area under its rate. */
53
+ export declare function expectedArrivals(profile: Profile): number;
54
+ /**
55
+ * This function gives the schedule of a profile. The same profile and the same options give the same
56
+ * times on one platform. A `poisson` schedule uses `Math.log`, thus a record keeps its times, and a
57
+ * later run reads them from the record. This function throws a `RangeError` for a profile with problems.
58
+ */
59
+ export declare function schedule(profile: Profile, options: ScheduleOptions): Schedule;
60
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","names":[],"sources":["../src/schedule.ts"],"mappings":";;;;;;;;;;qBAaa;;iBAGI;WACN;WACA;WACA;;;YAIC,mBAAmB;;YAGnB;;YAGA;WAA6B;;WAAmC;WAA8B;;;iBAGzF;WACN,eAAe;WACf,UAAU;WACV;WACA,SAAS;;WAET;;;wBAIK,SAAS,cAAc,aAAa;;wBAKpC,KAAK,cAAc,YAAY,aAAa;;wBAK5C,MAAM,0BAA0B,aAAa;;wBAK7C,MAAM,cAAc,cAAc;WAAkB;WAAyB;WAAwB;IAAkB;;wBAKvH,aAAa,SAAS;;wBActB,iBAAiB,SAAS;;;;;;wBAkC1B,SAAS,SAAS,SAAS,SAAS,kBAAkB"}
@@ -0,0 +1,110 @@
1
+ import { random } from "@mark1russell7/rigor-generators";
2
+ // src/schedule.ts
3
+ /**
4
+ * An open-loop schedule fixes the time of each arrival before the first request starts. No time
5
+ * depends on a reply. Thus a slow system cannot slow its own load, and a measure can count from the
6
+ * planned time.
7
+ *
8
+ * A profile is a rate over time, in straight pieces. One algorithm gives each arrival process. The
9
+ * cumulative rate turns a process of rate 1 into a process that follows the profile, by the
10
+ * time-rescaling theorem. `uniform` and `poisson` differ only in that process of rate 1.
11
+ */
12
+ /** This value is the format of a schedule. */
13
+ const SCHEDULE_FORMAT = "rigor-schedule/1";
14
+ /** This function gives one phase with a fixed rate. */
15
+ function constant(rate, ms) {
16
+ return [{
17
+ ms,
18
+ from: rate,
19
+ to: rate
20
+ }];
21
+ }
22
+ /** This function gives one phase with a rate that moves in a line. */
23
+ function ramp(from, to, ms) {
24
+ return [{
25
+ ms,
26
+ from,
27
+ to
28
+ }];
29
+ }
30
+ /** This function gives one fixed phase for each rate, each with the length `ms`. */
31
+ function steps(rates, ms) {
32
+ return rates.flatMap((rate) => constant(rate, ms));
33
+ }
34
+ /** This function gives a fixed rate with one interval at a higher rate. */
35
+ function spike(base, peak, shape) {
36
+ return [
37
+ ...constant(base, shape.before),
38
+ ...constant(peak, shape.width),
39
+ ...constant(base, shape.after)
40
+ ];
41
+ }
42
+ /** This function gives each problem of a profile. An empty list means that the profile is valid. */
43
+ function checkProfile(profile) {
44
+ const out = [];
45
+ if (profile.length === 0) out.push("The profile has no phases.");
46
+ profile.forEach((phase, index) => {
47
+ if (!(phase.ms > 0) || !Number.isFinite(phase.ms)) out.push(`The phase ${index}: the length is more than 0, not ${phase.ms}.`);
48
+ for (const rate of [phase.from, phase.to]) if (!(rate >= 0) || !Number.isFinite(rate)) out.push(`The phase ${index}: a rate is 0 or more, not ${rate}.`);
49
+ });
50
+ return out;
51
+ }
52
+ /** This function gives the number of arrivals of a phase on average. */
53
+ const areaOf = (phase) => (phase.from + phase.to) / 2 * phase.ms / 1e3;
54
+ /** This function gives the number of arrivals of a profile on average: the area under its rate. */
55
+ function expectedArrivals(profile) {
56
+ return profile.reduce((sum, phase) => sum + areaOf(phase), 0);
57
+ }
58
+ /**
59
+ * This function gives the time in a phase, in ms, at which its cumulative rate reaches `area`. The
60
+ * form has no division by the slope, thus it holds for a flat phase and for a phase that starts at 0.
61
+ * Rounding can move the last arrival of a falling ramp past the end of the root, thus the root stops at 0.
62
+ */
63
+ const inverse = (phase, area) => {
64
+ if (area === 0) return 0;
65
+ const seconds = phase.ms / 1e3;
66
+ const root = Math.sqrt(Math.max(0, phase.from * phase.from + 2 * (phase.to - phase.from) * area / seconds));
67
+ return Math.min(phase.ms, 2 * area * 1e3 / (phase.from + root));
68
+ };
69
+ /** This function gives the positions of the process of rate 1, up to `total`. */
70
+ const unitProcess = (total, options) => {
71
+ const out = [];
72
+ if (options.arrivals === "uniform") {
73
+ for (let at = .5; at < total; at += 1) out.push(at);
74
+ return out;
75
+ }
76
+ const rng = random(options.seed);
77
+ for (let at = -Math.log(1 - rng.next()); at < total; at += -Math.log(1 - rng.next())) out.push(at);
78
+ return out;
79
+ };
80
+ /**
81
+ * This function gives the schedule of a profile. The same profile and the same options give the same
82
+ * times on one platform. A `poisson` schedule uses `Math.log`, thus a record keeps its times, and a
83
+ * later run reads them from the record. This function throws a `RangeError` for a profile with problems.
84
+ */
85
+ function schedule(profile, options) {
86
+ const problems = checkProfile(profile);
87
+ if (problems.length > 0) throw new RangeError(`schedule: ${problems.join(" ")}`);
88
+ const times = [];
89
+ let phase = 0;
90
+ let before = 0;
91
+ let start = 0;
92
+ for (const at of unitProcess(expectedArrivals(profile), options)) {
93
+ while (phase < profile.length - 1 && at >= before + areaOf(profile[phase])) {
94
+ before += areaOf(profile[phase]);
95
+ start += profile[phase].ms;
96
+ phase += 1;
97
+ }
98
+ times.push(start + inverse(profile[phase], at - before));
99
+ }
100
+ return {
101
+ format: SCHEDULE_FORMAT,
102
+ arrivals: options.arrivals,
103
+ seed: options.arrivals === "poisson" ? options.seed : null,
104
+ profile,
105
+ times
106
+ };
107
+ }
108
+ export { SCHEDULE_FORMAT, checkProfile, constant, expectedArrivals, ramp, schedule, spike, steps };
109
+
110
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","names":[],"sources":["../src/schedule.ts"],"sourcesContent":["import { random } from \"@mark1russell7/rigor-generators\";\n\n/**\n * An open-loop schedule fixes the time of each arrival before the first request starts. No time\n * depends on a reply. Thus a slow system cannot slow its own load, and a measure can count from the\n * planned time.\n *\n * A profile is a rate over time, in straight pieces. One algorithm gives each arrival process. The\n * cumulative rate turns a process of rate 1 into a process that follows the profile, by the\n * time-rescaling theorem. `uniform` and `poisson` differ only in that process of rate 1.\n */\n\n/** This value is the format of a schedule. */\nexport const SCHEDULE_FORMAT = \"rigor-schedule/1\";\n\n/** A phase is one straight piece of a profile. Its rate moves from `from` to `to`, in arrivals each second, over `ms`. */\nexport interface Phase {\n readonly ms: number;\n readonly from: number;\n readonly to: number;\n}\n\n/** A profile is a list of phases, one after the other. */\nexport type Profile = readonly Phase[];\n\n/** This word names the arrival process. `uniform` has no random part, and `poisson` has independent gaps. */\nexport type Arrivals = \"uniform\" | \"poisson\";\n\n/** These options select the arrival process. Only `poisson` has a seed. */\nexport type ScheduleOptions = { readonly arrivals: \"uniform\" } | { readonly arrivals: \"poisson\"; readonly seed: number | string };\n\n/** A schedule is data, thus a record can keep the plan that a driver followed. */\nexport interface Schedule {\n readonly format: typeof SCHEDULE_FORMAT;\n readonly arrivals: Arrivals;\n readonly seed: number | string | null;\n readonly profile: Profile;\n /** These are the planned times, in ms from the start, in order. */\n readonly times: readonly number[];\n}\n\n/** This function gives one phase with a fixed rate. */\nexport function constant(rate: number, ms: number): Profile {\n return [{ ms, from: rate, to: rate }];\n}\n\n/** This function gives one phase with a rate that moves in a line. */\nexport function ramp(from: number, to: number, ms: number): Profile {\n return [{ ms, from, to }];\n}\n\n/** This function gives one fixed phase for each rate, each with the length `ms`. */\nexport function steps(rates: readonly number[], ms: number): Profile {\n return rates.flatMap((rate) => constant(rate, ms));\n}\n\n/** This function gives a fixed rate with one interval at a higher rate. */\nexport function spike(base: number, peak: number, shape: { readonly before: number; readonly width: number; readonly after: number }): Profile {\n return [...constant(base, shape.before), ...constant(peak, shape.width), ...constant(base, shape.after)];\n}\n\n/** This function gives each problem of a profile. An empty list means that the profile is valid. */\nexport function checkProfile(profile: Profile): readonly string[] {\n const out: string[] = [];\n if (profile.length === 0) out.push(\"The profile has no phases.\");\n profile.forEach((phase, index) => {\n if (!(phase.ms > 0) || !Number.isFinite(phase.ms)) out.push(`The phase ${index}: the length is more than 0, not ${phase.ms}.`);\n for (const rate of [phase.from, phase.to]) if (!(rate >= 0) || !Number.isFinite(rate)) out.push(`The phase ${index}: a rate is 0 or more, not ${rate}.`);\n });\n return out;\n}\n\n/** This function gives the number of arrivals of a phase on average. */\nconst areaOf = (phase: Phase): number => (((phase.from + phase.to) / 2) * phase.ms) / 1000;\n\n/** This function gives the number of arrivals of a profile on average: the area under its rate. */\nexport function expectedArrivals(profile: Profile): number {\n return profile.reduce((sum, phase) => sum + areaOf(phase), 0);\n}\n\n/**\n * This function gives the time in a phase, in ms, at which its cumulative rate reaches `area`. The\n * form has no division by the slope, thus it holds for a flat phase and for a phase that starts at 0.\n * Rounding can move the last arrival of a falling ramp past the end of the root, thus the root stops at 0.\n */\nconst inverse = (phase: Phase, area: number): number => {\n if (area === 0) return 0;\n const seconds = phase.ms / 1000;\n const root = Math.sqrt(Math.max(0, phase.from * phase.from + (2 * (phase.to - phase.from) * area) / seconds));\n return Math.min(phase.ms, (2 * area * 1000) / (phase.from + root));\n};\n\n/** This function gives the positions of the process of rate 1, up to `total`. */\nconst unitProcess = (total: number, options: ScheduleOptions): number[] => {\n const out: number[] = [];\n if (options.arrivals === \"uniform\") {\n // the middle of each unit gives an exact count, and a first arrival half a gap after the start\n for (let at = 0.5; at < total; at += 1) out.push(at);\n return out;\n }\n const rng = random(options.seed);\n for (let at = -Math.log(1 - rng.next()); at < total; at += -Math.log(1 - rng.next())) out.push(at);\n return out;\n};\n\n/**\n * This function gives the schedule of a profile. The same profile and the same options give the same\n * times on one platform. A `poisson` schedule uses `Math.log`, thus a record keeps its times, and a\n * later run reads them from the record. This function throws a `RangeError` for a profile with problems.\n */\nexport function schedule(profile: Profile, options: ScheduleOptions): Schedule {\n const problems = checkProfile(profile);\n if (problems.length > 0) throw new RangeError(`schedule: ${problems.join(\" \")}`);\n const times: number[] = [];\n let phase = 0;\n let before = 0;\n let start = 0;\n for (const at of unitProcess(expectedArrivals(profile), options)) {\n while (phase < profile.length - 1 && at >= before + areaOf(profile[phase] as Phase)) {\n before += areaOf(profile[phase] as Phase);\n start += (profile[phase] as Phase).ms;\n phase += 1;\n }\n times.push(start + inverse(profile[phase] as Phase, at - before));\n }\n return { format: SCHEDULE_FORMAT, arrivals: options.arrivals, seed: options.arrivals === \"poisson\" ? options.seed : null, profile, times };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,66 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@mark1russell7/rigor-chaos",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "A placeholder that lets npm trust the release workflow of mark1russell7/rigor. Use a later version.",
4
+ "version": "0.1.0",
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
+ "keywords": [
7
+ "load",
8
+ "open-loop",
9
+ "schedule",
10
+ "poisson",
11
+ "chaos",
12
+ "coordinated-omission",
13
+ "latency",
14
+ "testing",
15
+ "benchmark"
16
+ ],
17
+ "homepage": "https://github.com/mark1russell7/rigor/tree/main/packages/chaos#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/mark1russell7/rigor/issues"
20
+ },
5
21
  "repository": {
6
22
  "type": "git",
7
- "url": "git+https://github.com/mark1russell7/rigor.git"
23
+ "url": "git+https://github.com/mark1russell7/rigor.git",
24
+ "directory": "packages/chaos"
8
25
  },
9
26
  "license": "MIT",
27
+ "author": "Mark Russell (https://github.com/mark1russell7)",
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "main": "./build/index.js",
31
+ "files": [
32
+ "build"
33
+ ],
10
34
  "publishConfig": {
11
35
  "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "dependencies": {
41
+ "@mark1russell7/rigor-generators": "^0.1.5",
42
+ "@mark1russell7/rigor-statistics": "^0.1.11"
43
+ },
44
+ "devDependencies": {
45
+ "@mark1russell7/cue": "^2.0.0",
46
+ "@types/node": "^26.6.5",
47
+ "esbuild": "^0.28.2",
48
+ "tsdown": "^0.23.1",
49
+ "typescript": "^7.0.2",
50
+ "vitest": "^5.0.3"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
57
+ },
58
+ "types": "./build/index.d.ts",
59
+ "exports": {
60
+ ".": {
61
+ "types": "./build/index.d.ts",
62
+ "import": "./build/index.js"
63
+ },
64
+ "./package.json": "./package.json"
12
65
  }
13
- }
66
+ }