@mark1russell7/rigor-bench 0.1.2 → 0.2.1
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 +80 -5
- package/build/fresh.d.ts +134 -0
- package/build/fresh.d.ts.map +1 -0
- package/build/fresh.js +321 -0
- package/build/fresh.js.map +1 -0
- package/build/index.d.ts +81 -40
- package/build/index.d.ts.map +1 -1
- package/build/index.js +147 -65
- package/build/index.js.map +1 -1
- package/build/paired.d.ts +36 -0
- package/build/paired.d.ts.map +1 -0
- package/build/paired.js +54 -0
- package/build/paired.js.map +1 -0
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -43,6 +43,13 @@ v.comparison; // the cases that gain and dro
|
|
|
43
43
|
| `limitVerdict(candidate, aa, options)` | The verdict of the claim "the ratio of each case is at most `limit`": `within`, `beyond` or `cannot decide`. |
|
|
44
44
|
| `timeRounds(arms, options)` | The timed rounds of one case in one process: a warm-up, a batch of at least the target time, and the order of `firstArm`. |
|
|
45
45
|
| `verdictPower(options)` | The share of each outcome over seeded benches with a known ratio: the power test of a rule. |
|
|
46
|
+
| `balanceOrders(records)` | The records of each case with a turned order. It removes the extra records of the larger order, with the highest rounds first. |
|
|
47
|
+
| `pooledVerdict(jobs, options)` | The verdict of the merged records of all jobs, with the verdict of each job alone as a guard. |
|
|
48
|
+
| `armVerdicts(run, aaPair, options)` | The verdict of each candidate pair of a run against its A/A pair. |
|
|
49
|
+
| `benchEnv()` | From `/fresh`: the computer of this process, with the CPU model and the runner. |
|
|
50
|
+
| `childLine(stdout)` | From `/fresh`: the last JSON line of a child, with its time `ms`, its `hash` and the reason of a skip. |
|
|
51
|
+
| `pairOrder(pairs, round, caseIndex)` | From `/fresh`: the order of the pairs of one case in one round, and the first arm of each pair. |
|
|
52
|
+
| `freshRun(options)` | From `/fresh`: the records of each pair, with each arm in its own fresh child process. |
|
|
46
53
|
|
|
47
54
|
## A claim with a limit
|
|
48
55
|
|
|
@@ -71,6 +78,68 @@ const { records } = timeRounds({ a: () => oldCode(input), b: () => newCode(input
|
|
|
71
78
|
|
|
72
79
|
Each arm gives a number. The engine cannot remove the work of a call, because the sum of these numbers uses it.
|
|
73
80
|
|
|
81
|
+
## The fresh runner
|
|
82
|
+
|
|
83
|
+
The entry `@mark1russell7/rigor-bench/fresh` starts child processes, so it is for Node only. The root entry stays free of `node:child_process`.
|
|
84
|
+
|
|
85
|
+
`freshRun` starts each arm of each pair, case and round in a fresh child. The child prints one JSON line, for example `{"ms": 12.5, "hash": "9f3a"}`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { freshRun } from "@mark1russell7/rigor-bench/fresh";
|
|
89
|
+
|
|
90
|
+
const run = await freshRun({
|
|
91
|
+
base: { id: "old", command: ["node", "bench/arm.js", "--entry", "old"] },
|
|
92
|
+
candidates: [{ id: "new", command: ["node", "bench/arm.js", "--entry", "new"] }],
|
|
93
|
+
cases: ["board-1", "board-2"],
|
|
94
|
+
rounds: 5,
|
|
95
|
+
roundOffset: job * 5, // global rounds over the jobs
|
|
96
|
+
job: String(job),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
run.pairs["new"]; // the records of the pair (old, new)
|
|
100
|
+
run.pairs["old~aa"]; // the records of the A/A pair (old, old)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).
|
|
104
|
+
- `pairOrder` gives the order. Pair `p` starts with `firstArm(round, caseIndex + p)`, and the pair that starts first turns each second round.
|
|
105
|
+
- The arguments of a case come after the command. The default is `--case <id>`.
|
|
106
|
+
- A child that exits with an error, takes too long or prints no JSON line stops the run with an error. The error names the arm, the case, the round and the tail of `stderr`.
|
|
107
|
+
- Only a line with `skipped` skips a case. A skip line does not need a time.
|
|
108
|
+
- A skip in the first round of the job removes the case from each pair, and the run starts no more children for it. A skip after the first round stops the run with an error, because the child is not deterministic.
|
|
109
|
+
- A child that does not stop at the time limit gets SIGTERM, and SIGKILL after 1 second. The signals go to its process group. On Windows, `taskkill /T /F` stops its tree.
|
|
110
|
+
- Two different hashes of one arm on one case stop the run with an error.
|
|
111
|
+
- Each record keeps its job and the computer of its job (`benchEnv`).
|
|
112
|
+
|
|
113
|
+
A run of many jobs sets `roundOffset` to `job * rounds`. Then the merged records have one record of each case and round. Without it, each job repeats the first orders, and the merge removes more records.
|
|
114
|
+
|
|
115
|
+
### Why pairs, and not a rotation of all arms
|
|
116
|
+
|
|
117
|
+
A rotation of N arms turns the order of two arms only when they are N/2 apart in the rotation. For 4 arms, two arms 1 apart come in the order 3:1 over 4 rounds. Thus each pair of `freshRun` has its own two children, and the order of each pair turns each round.
|
|
118
|
+
|
|
119
|
+
## The pooled verdict
|
|
120
|
+
|
|
121
|
+
`pooledVerdict` gives the verdict of the records of all jobs together:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const v = pooledVerdict(
|
|
125
|
+
jobs.map((j) => ({ job: j.job, candidate: j.pairs["new"], aa: j.pairs["old~aa"] })),
|
|
126
|
+
{ margin: 0.05 },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
v.outcome; // the pooled outcome, or "cannot decide" when a job decides the opposite direction
|
|
130
|
+
v.jobs; // the verdict of each job alone
|
|
131
|
+
v.opposed; // the jobs against the pooled direction
|
|
132
|
+
v.trimmed; // the records that balanceOrders removed
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- The function merges the records, and balances the merged records with `balanceOrders`. A caller cannot pool records without a turned order.
|
|
136
|
+
- The verdict of each job uses the same options as the pooled verdict.
|
|
137
|
+
- A job with `cannot decide` or `same` does not change the outcome. Only a job with `faster` against a pooled `slower`, or the reverse, makes the outcome `cannot decide`.
|
|
138
|
+
- A pooled `same` stays `same`, also with a job that gives `slower` or `faster`. Only the opposite direction opposes.
|
|
139
|
+
- Each record with a field `job` has the label of its job, or `pooledVerdict` stops with an error.
|
|
140
|
+
- Two jobs with a record of one case and one round are an error. Set `roundOffset` of each job.
|
|
141
|
+
- `armVerdicts` gives the verdict of each candidate pair of one run against its A/A pair.
|
|
142
|
+
|
|
74
143
|
## The noise cap and the pooled band
|
|
75
144
|
|
|
76
145
|
Two options of `timeVerdict` keep a noisy run from a decision. Both are off by default, so the output stays the same as in 0.1.0.
|
|
@@ -80,10 +149,12 @@ The option `noiseCap` caps the A/A spread of the run. The spread is the quantile
|
|
|
80
149
|
```ts
|
|
81
150
|
const v = timeVerdict(records, aaRecords, { noiseCap: 0.06 });
|
|
82
151
|
|
|
83
|
-
v.noise; // { spread, cap, noisy }
|
|
152
|
+
v.noise; // { spread, cap, noisy, aaMedian, biased }
|
|
84
153
|
v.reason; // the spread, the cap and the outcome without the cap
|
|
85
154
|
```
|
|
86
155
|
|
|
156
|
+
Since 0.2.0, the noise cap also examines the bias of the A/A arm. An A/A median ratio out of `[exp(-noiseCap), exp(noiseCap)]` gives `cannot decide`. A bias of this size comes from the order or from the arm slots, not from the candidate. With a `spreadLevel` of 0.5 or more, a biased run is also noisy. More than half of its A/A cases are out of the cap. Thus the bias check decides alone only below the level 0.5. The fields `aaMedian` and `biased` keep the facts of the check.
|
|
157
|
+
|
|
87
158
|
The option `pool` gives a band from the A/A records of earlier quiet runs, with a floor. The caller keeps the pool, for example in a file of the repository:
|
|
88
159
|
|
|
89
160
|
```ts
|
|
@@ -107,12 +178,14 @@ For a fixed limit, for example 1.10 against a fixed base, use `limitVerdict`.
|
|
|
107
178
|
|
|
108
179
|
| Outcome | The rule `difference` (the default) | The rule `band` |
|
|
109
180
|
| --- | --- | --- |
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
181
|
+
| `same` | The interval is in `[exp(-margin), exp(margin)]`. The default margin is 0.05. The rule examines this outcome first. | The interval is in the band. |
|
|
182
|
+
| `faster` | The whole interval is below 1, and it is not in the margin. | The interval of the median ratio is below the band. |
|
|
183
|
+
| `slower` | The whole interval is above 1, and it is not in the margin. | The interval is above the band. |
|
|
113
184
|
| `cannot decide` | The interval holds 1 and crosses the margin. | The interval crosses an end of the band. |
|
|
114
185
|
|
|
115
|
-
Both rules give `cannot decide` when the A/A arm has fewer than 5 cases, or when the candidate has no cases. With `noiseCap`,
|
|
186
|
+
Both rules give `cannot decide` when the A/A arm has fewer than 5 cases, or when the candidate has no cases. With `noiseCap`, an A/A spread of the run above the cap also gives `cannot decide`. So does an A/A median ratio of the run out of the cap.
|
|
187
|
+
|
|
188
|
+
Since 0.2.0, the rule `difference` examines the margin before 1. An interval in the margin is `same`, also when it is wholly below 1. An example is the interval `[0.96, 0.98]` with the margin 0.05: it is `same`, and 0.1.2 gave `faster`. An interval below 1 that crosses `exp(-margin)` is still `faster`. With the margin 0, each outcome stays the same.
|
|
116
189
|
|
|
117
190
|
## The power test
|
|
118
191
|
|
|
@@ -131,3 +204,5 @@ Both rules give `cannot decide` when the A/A arm has fewer than 5 cases, or when
|
|
|
131
204
|
| `difference` | 20 | 0.71 | 0 | 0.04 |
|
|
132
205
|
|
|
133
206
|
The rule `band` compares an interval with a band of the same noise, so it needs a change of approximately twice that noise. With the kind `case`, it also calls a real speedup `same`. Thus `difference` is the default. Its false rate is near the 5% of a two-sided interval of level 0.95. A change of 7% needs approximately 20 cases for a power of 0.7.
|
|
207
|
+
|
|
208
|
+
Since 0.2.0, `verdictPower` takes `same` as the true outcome of a ratio in the margin of the rule `difference`. The cells of the table stay, because the ratio 0.93 is out of the margin 0.05.
|
package/build/fresh.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { r as PairedTime, t as Arm } from "./paired.js";
|
|
2
|
+
/** The computer of a run. The `cpu` is the model of the first CPU. */
|
|
3
|
+
export interface BenchEnv {
|
|
4
|
+
readonly cpu: string;
|
|
5
|
+
readonly cores: number;
|
|
6
|
+
readonly os: string;
|
|
7
|
+
readonly arch: string;
|
|
8
|
+
readonly node: string;
|
|
9
|
+
/** The name of the runner, from `BENCH_RUNNER` or else `RUNNER_NAME`. An empty value is absent. */
|
|
10
|
+
readonly runner?: string;
|
|
11
|
+
}
|
|
12
|
+
/** This function gives the computer of this process. The runner comes from `BENCH_RUNNER`, or else from `RUNNER_NAME`. */
|
|
13
|
+
export declare function benchEnv(): BenchEnv;
|
|
14
|
+
/** The JSON line of a child: its time in milliseconds, the hash of its output, and the reason of a skip. */
|
|
15
|
+
export type ChildLine = ChildTime | ChildSkip;
|
|
16
|
+
/** The result line of a child that timed the arm. */
|
|
17
|
+
export interface ChildTime {
|
|
18
|
+
readonly ms: number;
|
|
19
|
+
/** The hash of the output of the arm. Two different hashes of one arm on one case are an error. */
|
|
20
|
+
readonly hash?: string;
|
|
21
|
+
readonly skipped?: undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The result line of a child that skipped the case. A skip line does not need a time. */
|
|
24
|
+
export interface ChildSkip {
|
|
25
|
+
/** The reason of the skip. The runner removes the case from each pair. */
|
|
26
|
+
readonly skipped: string;
|
|
27
|
+
readonly ms?: number;
|
|
28
|
+
readonly hash?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* This function reads the last line of `stdout` that starts with `{`. The time `ms` is a finite number above 0.
|
|
32
|
+
* A skip line has the reason `skipped` and does not need a time. No such line, bad JSON or a bad field is an error.
|
|
33
|
+
*/
|
|
34
|
+
export declare function childLine(stdout: string): ChildLine;
|
|
35
|
+
/** One slot of the order of a round: the index of the pair, and the arm of that pair that starts first. */
|
|
36
|
+
export interface PairSlot {
|
|
37
|
+
readonly pair: number;
|
|
38
|
+
readonly first: Arm;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* This function gives the order of the pairs of one case in one round. `round` is the global round.
|
|
42
|
+
* - Pair `p` starts with `firstArm(round, caseIndex + p)`.
|
|
43
|
+
* - The pairs rotate by `(Math.floor(round / 2) + caseIndex) % pairs`. Thus the pair that starts first turns each second round.
|
|
44
|
+
* - For 2 pairs, the order is the order of `timeArmOrder` of the joint bench.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pairOrder(pairs: number, round: number, caseIndex: number): PairSlot[];
|
|
47
|
+
/** One arm of a run: its id and the argv of its child. The first item of `command` is the program. */
|
|
48
|
+
export interface FreshArm {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly command: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/** The limits of one child. */
|
|
53
|
+
export interface StartOptions {
|
|
54
|
+
/** The longest time of the child in milliseconds. */
|
|
55
|
+
readonly timeoutMs: number;
|
|
56
|
+
/** The most bytes of `stdout` or `stderr`. */
|
|
57
|
+
readonly maxBuffer: number;
|
|
58
|
+
}
|
|
59
|
+
/** The end of one child. */
|
|
60
|
+
export interface ChildExit {
|
|
61
|
+
readonly stdout: string;
|
|
62
|
+
readonly stderr: string;
|
|
63
|
+
/** The exit code, or `null` when a signal stopped the child. */
|
|
64
|
+
readonly code: number | null;
|
|
65
|
+
/** The value is `true` when the child took more time than the limit. */
|
|
66
|
+
readonly timedOut?: boolean;
|
|
67
|
+
/** The reason of a fault outside the child, for example a program that does not exist or too much output. */
|
|
68
|
+
readonly error?: string;
|
|
69
|
+
}
|
|
70
|
+
/** The start of one child: it gives the end of the child. The default starts a process with `spawn`, without a shell. */
|
|
71
|
+
export type StartChild = (argv: readonly string[], options: StartOptions) => Promise<ChildExit>;
|
|
72
|
+
/** The options of a fresh run. */
|
|
73
|
+
export interface FreshOptions {
|
|
74
|
+
/** The base arm. It is arm `a` of each pair. */
|
|
75
|
+
readonly base: FreshArm;
|
|
76
|
+
/** The candidate arms. Each candidate is arm `b` of its own pair, and the id of the pair is its id. */
|
|
77
|
+
readonly candidates: readonly FreshArm[];
|
|
78
|
+
/** The ids of the cases. */
|
|
79
|
+
readonly cases: readonly string[];
|
|
80
|
+
/** The count of the rounds of this job. */
|
|
81
|
+
readonly rounds: number;
|
|
82
|
+
/**
|
|
83
|
+
* The global round of the first round of this job. The default is 0.
|
|
84
|
+
* A run of many jobs sets it to `job * rounds`, so each round is global over the jobs.
|
|
85
|
+
* Without it, each job repeats the first orders, and the merge removes more records.
|
|
86
|
+
*/
|
|
87
|
+
readonly roundOffset?: number;
|
|
88
|
+
/** The value `true` adds the A/A pair (base, base) with the id `${base.id}~aa`. The default is `true`. */
|
|
89
|
+
readonly aa?: boolean;
|
|
90
|
+
/** The arguments of a case after the command of an arm. The default is `["--case", id]`. */
|
|
91
|
+
readonly args?: (caseId: string) => readonly string[];
|
|
92
|
+
/** The longest time of one child in milliseconds. The default is 600000 (10 minutes). */
|
|
93
|
+
readonly timeoutMs?: number;
|
|
94
|
+
/** The most bytes of the output of one child. The default is 16 MiB. */
|
|
95
|
+
readonly maxBuffer?: number;
|
|
96
|
+
/** The label of the job. The default is `"0"`. */
|
|
97
|
+
readonly job?: string;
|
|
98
|
+
/** The computer of the run. The default is `benchEnv()`. */
|
|
99
|
+
readonly env?: BenchEnv;
|
|
100
|
+
/** The start of a child. The default starts a process. A test gives its own. */
|
|
101
|
+
readonly start?: StartChild;
|
|
102
|
+
}
|
|
103
|
+
/** One record of a fresh run: a `PairedTime` record with the job and the computer of its job. */
|
|
104
|
+
export interface FreshRecord extends PairedTime {
|
|
105
|
+
readonly job: string;
|
|
106
|
+
readonly env: BenchEnv;
|
|
107
|
+
}
|
|
108
|
+
/** A case that an arm skipped. The case is in no pair. */
|
|
109
|
+
export interface FreshSkip {
|
|
110
|
+
readonly arm: string;
|
|
111
|
+
readonly case: string;
|
|
112
|
+
/** The global round of the skip. A skip is in the first round of its job. */
|
|
113
|
+
readonly round: number;
|
|
114
|
+
readonly reason: string;
|
|
115
|
+
}
|
|
116
|
+
/** The result of a fresh run. */
|
|
117
|
+
export interface FreshRun {
|
|
118
|
+
readonly env: BenchEnv;
|
|
119
|
+
readonly job: string;
|
|
120
|
+
/** The records of each pair, by the id of the pair. */
|
|
121
|
+
readonly pairs: Record<string, FreshRecord[]>;
|
|
122
|
+
readonly skipped: FreshSkip[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* This function starts each arm of each pair, case and round in a fresh child, and gives the records of each pair.
|
|
126
|
+
* - Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).
|
|
127
|
+
* - In each round and case, the pairs come in the order of `pairOrder`. The first arm of a pair starts first.
|
|
128
|
+
* - A child that exits with an error, takes too long or prints no JSON line is an error. Only a skip line skips a case.
|
|
129
|
+
* - A case that an arm skips in the first round of the job leaves each pair, and the runner starts no more children for it.
|
|
130
|
+
* - A skip after the first round is an error. It shows a child that is not deterministic, and it drops paid data.
|
|
131
|
+
* - Two different hashes of one arm on one case are an error.
|
|
132
|
+
*/
|
|
133
|
+
export declare function freshRun(options: FreshOptions): Promise<FreshRun>;
|
|
134
|
+
//# sourceMappingURL=fresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh.d.ts","names":[],"sources":["../src/fresh.ts"],"mappings":";;iBASiB;WACN;WACA;WACA;WACA;WACA;;WAEA;;;wBAIK,YAAY;;YAQhB,YAAY,YAAY;;iBAGnB;WACN;;WAEA;WACA;;;iBAIM;;WAEN;WACA;WACA;;;;;;wBAOK,UAAU,iBAAiB;;iBAwB1B;WACN;WACA,OAAO;;;;;;;;wBASF,UAAU,eAAe,eAAe,oBAAoB;;iBAY3D;WACN;WACA;;;iBAIM;;WAEN;;WAEA;;;iBAIM;WACN;WACA;;WAEA;;WAEA;;WAEA;;;YAIC,cAAc,yBAAyB,SAAS,iBAAiB,QAAQ;;iBAGpE;;WAEN,MAAM;;WAEN,qBAAqB;;WAErB;;WAEA;;;;;;WAMA;;WAEA;;WAEA,QAAQ;;WAER;;WAEA;;WAEA;;WAEA,MAAM;;WAEN,QAAQ;;;iBAIF,oBAAoB;WAC1B;WACA,KAAK;;;iBAIC;WACN;WACA;;WAEA;WACA;;;iBAIM;WACN,KAAK;WACL;;WAEA,OAAO,eAAe;WACtB,SAAS;;;;;;;;;;;wBAkBE,SAAS,SAAS,eAAe,QAAQ"}
|
package/build/fresh.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { r as firstArm } from "./paired.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { arch, cpus, platform } from "node:os";
|
|
4
|
+
// src/fresh.ts
|
|
5
|
+
/**
|
|
6
|
+
* The fresh-process runner of a bench, for Node only. Each arm of each pair, case and round is one fresh child process.
|
|
7
|
+
* The child prints one JSON line with its time. The runner keeps the order of `pairOrder`, and gives `PairedTime` records.
|
|
8
|
+
*/
|
|
9
|
+
/** This function gives the computer of this process. The runner comes from `BENCH_RUNNER`, or else from `RUNNER_NAME`. */
|
|
10
|
+
function benchEnv() {
|
|
11
|
+
const all = cpus();
|
|
12
|
+
const env = {
|
|
13
|
+
cpu: all[0]?.model.trim() || "unknown",
|
|
14
|
+
cores: all.length,
|
|
15
|
+
os: platform(),
|
|
16
|
+
arch: arch(),
|
|
17
|
+
node: process.version
|
|
18
|
+
};
|
|
19
|
+
const runner = process.env["BENCH_RUNNER"] || process.env["RUNNER_NAME"];
|
|
20
|
+
return runner === void 0 || runner === "" ? env : {
|
|
21
|
+
...env,
|
|
22
|
+
runner
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* This function reads the last line of `stdout` that starts with `{`. The time `ms` is a finite number above 0.
|
|
27
|
+
* A skip line has the reason `skipped` and does not need a time. No such line, bad JSON or a bad field is an error.
|
|
28
|
+
*/
|
|
29
|
+
function childLine(stdout) {
|
|
30
|
+
const line = stdout.split("\n").map((l) => l.trim()).findLast((l) => l.startsWith("{"));
|
|
31
|
+
if (line === void 0) throw new Error("childLine: the output has no line that starts with {.");
|
|
32
|
+
let value;
|
|
33
|
+
try {
|
|
34
|
+
value = JSON.parse(line);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`childLine: the last line that starts with { is not JSON: ${line.slice(0, 200)}`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`childLine: the last JSON line is not an object: ${line.slice(0, 200)}`);
|
|
39
|
+
const { ms, hash, skipped } = value;
|
|
40
|
+
if (hash !== void 0 && typeof hash !== "string") throw new Error(`childLine: the hash is a string, not ${JSON.stringify(hash)}.`);
|
|
41
|
+
if (skipped !== void 0) {
|
|
42
|
+
if (!(typeof skipped === "string" && skipped !== "")) throw new Error(`childLine: the reason of a skip is a string that is not empty, not ${JSON.stringify(skipped)}.`);
|
|
43
|
+
return {
|
|
44
|
+
skipped,
|
|
45
|
+
...typeof ms === "number" ? { ms } : {},
|
|
46
|
+
...hash === void 0 ? {} : { hash }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!(typeof ms === "number" && Number.isFinite(ms) && ms > 0)) throw new Error(`childLine: the time ms is a finite number above 0, not ${JSON.stringify(ms)}.`);
|
|
50
|
+
return {
|
|
51
|
+
ms,
|
|
52
|
+
...hash === void 0 ? {} : { hash }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* This function gives the order of the pairs of one case in one round. `round` is the global round.
|
|
57
|
+
* - Pair `p` starts with `firstArm(round, caseIndex + p)`.
|
|
58
|
+
* - The pairs rotate by `(Math.floor(round / 2) + caseIndex) % pairs`. Thus the pair that starts first turns each second round.
|
|
59
|
+
* - For 2 pairs, the order is the order of `timeArmOrder` of the joint bench.
|
|
60
|
+
*/
|
|
61
|
+
function pairOrder(pairs, round, caseIndex) {
|
|
62
|
+
if (!Number.isSafeInteger(pairs) || pairs < 1) throw new RangeError(`pairOrder: the count of the pairs is a whole number of 1 or more, not ${pairs}.`);
|
|
63
|
+
if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`pairOrder: the round is a whole number of 0 or more, not ${round}.`);
|
|
64
|
+
if (!Number.isSafeInteger(caseIndex) || caseIndex < 0) throw new RangeError(`pairOrder: the index of the case is a whole number of 0 or more, not ${caseIndex}.`);
|
|
65
|
+
const shift = (Math.floor(round / 2) + caseIndex) % pairs;
|
|
66
|
+
return Array.from({ length: pairs }, (_, i) => {
|
|
67
|
+
const pair = (shift + i) % pairs;
|
|
68
|
+
return {
|
|
69
|
+
pair,
|
|
70
|
+
first: firstArm(round, caseIndex + pair)
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** The suffix of the id of the A/A pair. */
|
|
75
|
+
const AA_SUFFIX = "~aa";
|
|
76
|
+
/** The longest tail of `stderr` in an error. */
|
|
77
|
+
const TAIL = 2e3;
|
|
78
|
+
/**
|
|
79
|
+
* This function starts each arm of each pair, case and round in a fresh child, and gives the records of each pair.
|
|
80
|
+
* - Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).
|
|
81
|
+
* - In each round and case, the pairs come in the order of `pairOrder`. The first arm of a pair starts first.
|
|
82
|
+
* - A child that exits with an error, takes too long or prints no JSON line is an error. Only a skip line skips a case.
|
|
83
|
+
* - A case that an arm skips in the first round of the job leaves each pair, and the runner starts no more children for it.
|
|
84
|
+
* - A skip after the first round is an error. It shows a child that is not deterministic, and it drops paid data.
|
|
85
|
+
* - Two different hashes of one arm on one case are an error.
|
|
86
|
+
*/
|
|
87
|
+
async function freshRun(options) {
|
|
88
|
+
const { base, candidates, cases, rounds } = options;
|
|
89
|
+
const roundOffset = options.roundOffset ?? 0;
|
|
90
|
+
const job = options.job ?? "0";
|
|
91
|
+
const timeoutMs = options.timeoutMs ?? 6e5;
|
|
92
|
+
const maxBuffer = options.maxBuffer ?? 1 << 24;
|
|
93
|
+
const args = options.args ?? ((id) => ["--case", id]);
|
|
94
|
+
const start = options.start ?? startProcess;
|
|
95
|
+
if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`freshRun: the rounds are a whole number of 1 or more, not ${rounds}.`);
|
|
96
|
+
if (!Number.isSafeInteger(roundOffset) || roundOffset < 0) throw new RangeError(`freshRun: the round offset is a whole number of 0 or more, not ${roundOffset}.`);
|
|
97
|
+
if (!(timeoutMs > 0) || !(maxBuffer > 0)) throw new RangeError("freshRun: the time limit and the output limit are above 0.");
|
|
98
|
+
if (cases.length === 0 || new Set(cases).size !== cases.length || cases.some((c) => typeof c !== "string" || c === "")) throw new RangeError("freshRun: the cases are 1 or more different strings that are not empty.");
|
|
99
|
+
const pairs = candidates.map((c) => ({
|
|
100
|
+
id: c.id,
|
|
101
|
+
a: base,
|
|
102
|
+
b: c
|
|
103
|
+
}));
|
|
104
|
+
if (options.aa ?? true) pairs.push({
|
|
105
|
+
id: `${base.id}${AA_SUFFIX}`,
|
|
106
|
+
a: base,
|
|
107
|
+
b: base
|
|
108
|
+
});
|
|
109
|
+
if (pairs.length === 0) throw new RangeError("freshRun: the run has no pairs. Give a candidate, or keep the A/A pair.");
|
|
110
|
+
for (const arm of [base, ...candidates]) {
|
|
111
|
+
if (typeof arm.id !== "string" || arm.id === "") throw new RangeError("freshRun: the id of an arm is a string that is not empty.");
|
|
112
|
+
if (arm.command.length === 0 || arm.command.some((s) => typeof s !== "string")) throw new RangeError(`freshRun: the command of the arm ${JSON.stringify(arm.id)} is 1 or more strings.`);
|
|
113
|
+
}
|
|
114
|
+
const ids = /* @__PURE__ */ new Set([base.id]);
|
|
115
|
+
for (const { id } of pairs) {
|
|
116
|
+
if (ids.has(id)) throw new RangeError(`freshRun: two arms or pairs have the id ${JSON.stringify(id)}.`);
|
|
117
|
+
ids.add(id);
|
|
118
|
+
}
|
|
119
|
+
const env = options.env ?? benchEnv();
|
|
120
|
+
const out = Object.fromEntries(pairs.map((p) => [p.id, []]));
|
|
121
|
+
const skipped = [];
|
|
122
|
+
const gone = /* @__PURE__ */ new Set();
|
|
123
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
124
|
+
for (let r = 0; r < rounds; r++) {
|
|
125
|
+
const round = roundOffset + r;
|
|
126
|
+
for (let i = 0; i < cases.length; i++) {
|
|
127
|
+
const id = cases[i];
|
|
128
|
+
if (gone.has(id)) continue;
|
|
129
|
+
const records = [];
|
|
130
|
+
let skip;
|
|
131
|
+
for (const slot of pairOrder(pairs.length, round, i)) {
|
|
132
|
+
const pair = pairs[slot.pair];
|
|
133
|
+
const times = {};
|
|
134
|
+
for (const arm of [slot.first, slot.first === "a" ? "b" : "a"]) {
|
|
135
|
+
const who = pair[arm];
|
|
136
|
+
const where = `the arm ${JSON.stringify(who.id)} (pair ${JSON.stringify(pair.id)}, arm ${arm}) on the case ${JSON.stringify(id)}, round ${round}`;
|
|
137
|
+
const line = await child(start, [...who.command, ...args(id)], {
|
|
138
|
+
timeoutMs,
|
|
139
|
+
maxBuffer
|
|
140
|
+
}, where);
|
|
141
|
+
if (line.skipped !== void 0) {
|
|
142
|
+
if (r > 0) throw new Error(`freshRun: ${where} skipped the case after the first round of the job (${JSON.stringify(line.skipped)}). A child that skips only in a later round is not deterministic.`);
|
|
143
|
+
skip = {
|
|
144
|
+
arm: who.id,
|
|
145
|
+
case: id,
|
|
146
|
+
round,
|
|
147
|
+
reason: line.skipped
|
|
148
|
+
};
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (line.hash !== void 0) {
|
|
152
|
+
const key = `${who.id}\u0000${id}`;
|
|
153
|
+
const known = hashes.get(key);
|
|
154
|
+
if (known !== void 0 && known !== line.hash) throw new Error(`freshRun: ${where} gave the hash ${line.hash}, and an earlier child of the arm gave ${known}.`);
|
|
155
|
+
hashes.set(key, line.hash);
|
|
156
|
+
}
|
|
157
|
+
times[arm] = line.ms;
|
|
158
|
+
}
|
|
159
|
+
if (skip !== void 0) break;
|
|
160
|
+
records.push({
|
|
161
|
+
pair: pair.id,
|
|
162
|
+
record: {
|
|
163
|
+
case: id,
|
|
164
|
+
round,
|
|
165
|
+
first: slot.first,
|
|
166
|
+
a: times.a,
|
|
167
|
+
b: times.b,
|
|
168
|
+
job,
|
|
169
|
+
env
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (skip !== void 0) {
|
|
174
|
+
skipped.push(skip);
|
|
175
|
+
gone.add(id);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const { pair, record } of records) out[pair].push(record);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
env,
|
|
183
|
+
job,
|
|
184
|
+
pairs: out,
|
|
185
|
+
skipped
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** This function starts one child and reads its JSON line. A fault gives an error that names the arm, the case, the round and the tail of `stderr`. */
|
|
189
|
+
async function child(start, argv, limits, where) {
|
|
190
|
+
let exit;
|
|
191
|
+
try {
|
|
192
|
+
exit = await start(argv, limits);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(`freshRun: ${where} did not start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
195
|
+
}
|
|
196
|
+
const fail = (why) => {
|
|
197
|
+
const tail = exit.stderr.length > TAIL ? `...${exit.stderr.slice(-2e3)}` : exit.stderr;
|
|
198
|
+
return /* @__PURE__ */ new Error(`freshRun: ${where} ${why}. The tail of stderr: ${tail.trim() === "" ? "(empty)" : tail.trimEnd()}`);
|
|
199
|
+
};
|
|
200
|
+
if (exit.error !== void 0) throw fail(`failed: ${exit.error}`);
|
|
201
|
+
if (exit.timedOut === true) throw fail(`took more than ${limits.timeoutMs} ms`);
|
|
202
|
+
if (exit.code !== 0) throw fail(exit.code === null ? "stopped on a signal" : `exited with the code ${exit.code}`);
|
|
203
|
+
try {
|
|
204
|
+
return childLine(exit.stdout);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw fail(`printed no good JSON line (${error instanceof Error ? error.message : String(error)})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** The wait, in milliseconds, for the pipes of a child after its exit, and for the exit of a child after a stop. */
|
|
210
|
+
const GRACE_MS = 1e3;
|
|
211
|
+
/**
|
|
212
|
+
* This function starts a process with `spawn` and no shell. It stops the process and its tree at the time limit or at the output limit.
|
|
213
|
+
*
|
|
214
|
+
* - The promise settles at the close of the pipes, or a grace time after the exit of the child. A grandchild can keep a pipe open.
|
|
215
|
+
* - A stop sends SIGTERM to the process group, and SIGKILL after the grace time. On Windows, `taskkill /T /F` stops the tree.
|
|
216
|
+
*/
|
|
217
|
+
function startProcess(argv, options) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const [program, ...rest] = argv;
|
|
220
|
+
const windows = process.platform === "win32";
|
|
221
|
+
const proc = spawn(program, rest, {
|
|
222
|
+
stdio: [
|
|
223
|
+
"ignore",
|
|
224
|
+
"pipe",
|
|
225
|
+
"pipe"
|
|
226
|
+
],
|
|
227
|
+
windowsHide: true,
|
|
228
|
+
detached: !windows
|
|
229
|
+
});
|
|
230
|
+
const out = [];
|
|
231
|
+
const err = [];
|
|
232
|
+
let timedOut = false;
|
|
233
|
+
let settled = false;
|
|
234
|
+
let stopping = false;
|
|
235
|
+
let error;
|
|
236
|
+
let grace;
|
|
237
|
+
const signal = (name) => {
|
|
238
|
+
const pid = proc.pid;
|
|
239
|
+
if (pid === void 0) return;
|
|
240
|
+
if (windows) {
|
|
241
|
+
spawn("taskkill", [
|
|
242
|
+
"/pid",
|
|
243
|
+
String(pid),
|
|
244
|
+
"/T",
|
|
245
|
+
"/F"
|
|
246
|
+
], {
|
|
247
|
+
stdio: "ignore",
|
|
248
|
+
windowsHide: true
|
|
249
|
+
}).on("error", () => proc.kill());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
process.kill(-pid, name);
|
|
254
|
+
} catch {
|
|
255
|
+
proc.kill(name);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const finish = (code) => {
|
|
259
|
+
if (settled) return;
|
|
260
|
+
settled = true;
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
clearTimeout(grace);
|
|
263
|
+
proc.stdout.destroy();
|
|
264
|
+
proc.stderr.destroy();
|
|
265
|
+
const exit = {
|
|
266
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
267
|
+
stderr: Buffer.concat(err).toString("utf8"),
|
|
268
|
+
code,
|
|
269
|
+
timedOut
|
|
270
|
+
};
|
|
271
|
+
resolve(error === void 0 ? exit : {
|
|
272
|
+
...exit,
|
|
273
|
+
error
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const stop = () => {
|
|
277
|
+
if (stopping) return;
|
|
278
|
+
stopping = true;
|
|
279
|
+
signal("SIGTERM");
|
|
280
|
+
clearTimeout(grace);
|
|
281
|
+
grace = setTimeout(() => {
|
|
282
|
+
if (!windows) signal("SIGKILL");
|
|
283
|
+
finish(null);
|
|
284
|
+
}, GRACE_MS);
|
|
285
|
+
};
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
timedOut = true;
|
|
288
|
+
stop();
|
|
289
|
+
}, options.timeoutMs);
|
|
290
|
+
const take = (list) => {
|
|
291
|
+
let bytes = 0;
|
|
292
|
+
return (chunk) => {
|
|
293
|
+
bytes += chunk.length;
|
|
294
|
+
if (bytes > options.maxBuffer) {
|
|
295
|
+
error ??= `an output is more than ${options.maxBuffer} bytes`;
|
|
296
|
+
stop();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
list.push(chunk);
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
proc.stdout.on("data", take(out));
|
|
303
|
+
proc.stderr.on("data", take(err));
|
|
304
|
+
proc.on("error", (e) => {
|
|
305
|
+
error ??= e.message;
|
|
306
|
+
if (proc.pid === void 0) finish(null);
|
|
307
|
+
});
|
|
308
|
+
proc.on("exit", (code) => {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
if (stopping) return;
|
|
311
|
+
grace = setTimeout(() => {
|
|
312
|
+
if (!windows) signal("SIGKILL");
|
|
313
|
+
finish(code);
|
|
314
|
+
}, GRACE_MS);
|
|
315
|
+
});
|
|
316
|
+
proc.on("close", (code) => finish(code));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
export { benchEnv, childLine, freshRun, pairOrder };
|
|
320
|
+
|
|
321
|
+
//# sourceMappingURL=fresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh.js","names":[],"sources":["../src/fresh.ts"],"sourcesContent":["/**\n * The fresh-process runner of a bench, for Node only. Each arm of each pair, case and round is one fresh child process.\n * The child prints one JSON line with its time. The runner keeps the order of `pairOrder`, and gives `PairedTime` records.\n */\nimport { spawn } from \"node:child_process\";\nimport { arch, cpus, platform } from \"node:os\";\nimport { type Arm, type PairedTime, firstArm } from \"./paired.ts\";\n\n/** The computer of a run. The `cpu` is the model of the first CPU. */\nexport interface BenchEnv {\n readonly cpu: string;\n readonly cores: number;\n readonly os: string;\n readonly arch: string;\n readonly node: string;\n /** The name of the runner, from `BENCH_RUNNER` or else `RUNNER_NAME`. An empty value is absent. */\n readonly runner?: string;\n}\n\n/** This function gives the computer of this process. The runner comes from `BENCH_RUNNER`, or else from `RUNNER_NAME`. */\nexport function benchEnv(): BenchEnv {\n const all = cpus();\n const env = { cpu: all[0]?.model.trim() || \"unknown\", cores: all.length, os: platform(), arch: arch(), node: process.version };\n const runner = process.env[\"BENCH_RUNNER\"] || process.env[\"RUNNER_NAME\"];\n return runner === undefined || runner === \"\" ? env : { ...env, runner };\n}\n\n/** The JSON line of a child: its time in milliseconds, the hash of its output, and the reason of a skip. */\nexport type ChildLine = ChildTime | ChildSkip;\n\n/** The result line of a child that timed the arm. */\nexport interface ChildTime {\n readonly ms: number;\n /** The hash of the output of the arm. Two different hashes of one arm on one case are an error. */\n readonly hash?: string;\n readonly skipped?: undefined;\n}\n\n/** The result line of a child that skipped the case. A skip line does not need a time. */\nexport interface ChildSkip {\n /** The reason of the skip. The runner removes the case from each pair. */\n readonly skipped: string;\n readonly ms?: number;\n readonly hash?: string;\n}\n\n/**\n * This function reads the last line of `stdout` that starts with `{`. The time `ms` is a finite number above 0.\n * A skip line has the reason `skipped` and does not need a time. No such line, bad JSON or a bad field is an error.\n */\nexport function childLine(stdout: string): ChildLine {\n const line = stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .findLast((l) => l.startsWith(\"{\"));\n if (line === undefined) throw new Error(\"childLine: the output has no line that starts with {.\");\n let value: unknown;\n try {\n value = JSON.parse(line);\n } catch {\n throw new Error(`childLine: the last line that starts with { is not JSON: ${line.slice(0, 200)}`);\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new Error(`childLine: the last JSON line is not an object: ${line.slice(0, 200)}`);\n const { ms, hash, skipped } = value as Record<string, unknown>;\n if (hash !== undefined && typeof hash !== \"string\") throw new Error(`childLine: the hash is a string, not ${JSON.stringify(hash)}.`);\n if (skipped !== undefined) {\n if (!(typeof skipped === \"string\" && skipped !== \"\")) throw new Error(`childLine: the reason of a skip is a string that is not empty, not ${JSON.stringify(skipped)}.`);\n return { skipped, ...(typeof ms === \"number\" ? { ms } : {}), ...(hash === undefined ? {} : { hash }) };\n }\n if (!(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0)) throw new Error(`childLine: the time ms is a finite number above 0, not ${JSON.stringify(ms)}.`);\n return { ms, ...(hash === undefined ? {} : { hash }) };\n}\n\n/** One slot of the order of a round: the index of the pair, and the arm of that pair that starts first. */\nexport interface PairSlot {\n readonly pair: number;\n readonly first: Arm;\n}\n\n/**\n * This function gives the order of the pairs of one case in one round. `round` is the global round.\n * - Pair `p` starts with `firstArm(round, caseIndex + p)`.\n * - The pairs rotate by `(Math.floor(round / 2) + caseIndex) % pairs`. Thus the pair that starts first turns each second round.\n * - For 2 pairs, the order is the order of `timeArmOrder` of the joint bench.\n */\nexport function pairOrder(pairs: number, round: number, caseIndex: number): PairSlot[] {\n if (!Number.isSafeInteger(pairs) || pairs < 1) throw new RangeError(`pairOrder: the count of the pairs is a whole number of 1 or more, not ${pairs}.`);\n if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`pairOrder: the round is a whole number of 0 or more, not ${round}.`);\n if (!Number.isSafeInteger(caseIndex) || caseIndex < 0) throw new RangeError(`pairOrder: the index of the case is a whole number of 0 or more, not ${caseIndex}.`);\n const shift = (Math.floor(round / 2) + caseIndex) % pairs;\n return Array.from({ length: pairs }, (_, i) => {\n const pair = (shift + i) % pairs;\n return { pair, first: firstArm(round, caseIndex + pair) };\n });\n}\n\n/** One arm of a run: its id and the argv of its child. The first item of `command` is the program. */\nexport interface FreshArm {\n readonly id: string;\n readonly command: readonly string[];\n}\n\n/** The limits of one child. */\nexport interface StartOptions {\n /** The longest time of the child in milliseconds. */\n readonly timeoutMs: number;\n /** The most bytes of `stdout` or `stderr`. */\n readonly maxBuffer: number;\n}\n\n/** The end of one child. */\nexport interface ChildExit {\n readonly stdout: string;\n readonly stderr: string;\n /** The exit code, or `null` when a signal stopped the child. */\n readonly code: number | null;\n /** The value is `true` when the child took more time than the limit. */\n readonly timedOut?: boolean;\n /** The reason of a fault outside the child, for example a program that does not exist or too much output. */\n readonly error?: string;\n}\n\n/** The start of one child: it gives the end of the child. The default starts a process with `spawn`, without a shell. */\nexport type StartChild = (argv: readonly string[], options: StartOptions) => Promise<ChildExit>;\n\n/** The options of a fresh run. */\nexport interface FreshOptions {\n /** The base arm. It is arm `a` of each pair. */\n readonly base: FreshArm;\n /** The candidate arms. Each candidate is arm `b` of its own pair, and the id of the pair is its id. */\n readonly candidates: readonly FreshArm[];\n /** The ids of the cases. */\n readonly cases: readonly string[];\n /** The count of the rounds of this job. */\n readonly rounds: number;\n /**\n * The global round of the first round of this job. The default is 0.\n * A run of many jobs sets it to `job * rounds`, so each round is global over the jobs.\n * Without it, each job repeats the first orders, and the merge removes more records.\n */\n readonly roundOffset?: number;\n /** The value `true` adds the A/A pair (base, base) with the id `${base.id}~aa`. The default is `true`. */\n readonly aa?: boolean;\n /** The arguments of a case after the command of an arm. The default is `[\"--case\", id]`. */\n readonly args?: (caseId: string) => readonly string[];\n /** The longest time of one child in milliseconds. The default is 600000 (10 minutes). */\n readonly timeoutMs?: number;\n /** The most bytes of the output of one child. The default is 16 MiB. */\n readonly maxBuffer?: number;\n /** The label of the job. The default is `\"0\"`. */\n readonly job?: string;\n /** The computer of the run. The default is `benchEnv()`. */\n readonly env?: BenchEnv;\n /** The start of a child. The default starts a process. A test gives its own. */\n readonly start?: StartChild;\n}\n\n/** One record of a fresh run: a `PairedTime` record with the job and the computer of its job. */\nexport interface FreshRecord extends PairedTime {\n readonly job: string;\n readonly env: BenchEnv;\n}\n\n/** A case that an arm skipped. The case is in no pair. */\nexport interface FreshSkip {\n readonly arm: string;\n readonly case: string;\n /** The global round of the skip. A skip is in the first round of its job. */\n readonly round: number;\n readonly reason: string;\n}\n\n/** The result of a fresh run. */\nexport interface FreshRun {\n readonly env: BenchEnv;\n readonly job: string;\n /** The records of each pair, by the id of the pair. */\n readonly pairs: Record<string, FreshRecord[]>;\n readonly skipped: FreshSkip[];\n}\n\n/** The suffix of the id of the A/A pair. */\nconst AA_SUFFIX = \"~aa\";\n\n/** The longest tail of `stderr` in an error. */\nconst TAIL = 2000;\n\n/**\n * This function starts each arm of each pair, case and round in a fresh child, and gives the records of each pair.\n * - Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).\n * - In each round and case, the pairs come in the order of `pairOrder`. The first arm of a pair starts first.\n * - A child that exits with an error, takes too long or prints no JSON line is an error. Only a skip line skips a case.\n * - A case that an arm skips in the first round of the job leaves each pair, and the runner starts no more children for it.\n * - A skip after the first round is an error. It shows a child that is not deterministic, and it drops paid data.\n * - Two different hashes of one arm on one case are an error.\n */\nexport async function freshRun(options: FreshOptions): Promise<FreshRun> {\n const { base, candidates, cases, rounds } = options;\n const roundOffset = options.roundOffset ?? 0;\n const job = options.job ?? \"0\";\n const timeoutMs = options.timeoutMs ?? 600_000;\n const maxBuffer = options.maxBuffer ?? 1 << 24;\n const args = options.args ?? ((id: string) => [\"--case\", id]);\n const start = options.start ?? startProcess;\n if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`freshRun: the rounds are a whole number of 1 or more, not ${rounds}.`);\n if (!Number.isSafeInteger(roundOffset) || roundOffset < 0) throw new RangeError(`freshRun: the round offset is a whole number of 0 or more, not ${roundOffset}.`);\n if (!(timeoutMs > 0) || !(maxBuffer > 0)) throw new RangeError(\"freshRun: the time limit and the output limit are above 0.\");\n if (cases.length === 0 || new Set(cases).size !== cases.length || cases.some((c) => typeof c !== \"string\" || c === \"\")) {\n throw new RangeError(\"freshRun: the cases are 1 or more different strings that are not empty.\");\n }\n const pairs: { id: string; a: FreshArm; b: FreshArm }[] = candidates.map((c) => ({ id: c.id, a: base, b: c }));\n if (options.aa ?? true) pairs.push({ id: `${base.id}${AA_SUFFIX}`, a: base, b: base });\n if (pairs.length === 0) throw new RangeError(\"freshRun: the run has no pairs. Give a candidate, or keep the A/A pair.\");\n for (const arm of [base, ...candidates]) {\n if (typeof arm.id !== \"string\" || arm.id === \"\") throw new RangeError(\"freshRun: the id of an arm is a string that is not empty.\");\n if (arm.command.length === 0 || arm.command.some((s) => typeof s !== \"string\")) throw new RangeError(`freshRun: the command of the arm ${JSON.stringify(arm.id)} is 1 or more strings.`);\n }\n const ids = new Set([base.id]);\n for (const { id } of pairs) {\n if (ids.has(id)) throw new RangeError(`freshRun: two arms or pairs have the id ${JSON.stringify(id)}.`);\n ids.add(id);\n }\n\n const env = options.env ?? benchEnv();\n const out: Record<string, FreshRecord[]> = Object.fromEntries(pairs.map((p) => [p.id, []]));\n const skipped: FreshSkip[] = [];\n const gone = new Set<string>();\n const hashes = new Map<string, string>();\n\n for (let r = 0; r < rounds; r++) {\n const round = roundOffset + r;\n for (let i = 0; i < cases.length; i++) {\n const id = cases[i]!;\n if (gone.has(id)) continue;\n const records: { pair: string; record: FreshRecord }[] = [];\n let skip: FreshSkip | undefined;\n for (const slot of pairOrder(pairs.length, round, i)) {\n const pair = pairs[slot.pair]!;\n const times: Partial<Record<Arm, number>> = {};\n for (const arm of [slot.first, slot.first === \"a\" ? \"b\" : \"a\"] as const) {\n const who = pair[arm];\n const where = `the arm ${JSON.stringify(who.id)} (pair ${JSON.stringify(pair.id)}, arm ${arm}) on the case ${JSON.stringify(id)}, round ${round}`;\n const line = await child(start, [...who.command, ...args(id)], { timeoutMs, maxBuffer }, where);\n if (line.skipped !== undefined) {\n if (r > 0) throw new Error(`freshRun: ${where} skipped the case after the first round of the job (${JSON.stringify(line.skipped)}). A child that skips only in a later round is not deterministic.`);\n skip = { arm: who.id, case: id, round, reason: line.skipped };\n break;\n }\n if (line.hash !== undefined) {\n const key = `${who.id}\\u0000${id}`;\n const known = hashes.get(key);\n if (known !== undefined && known !== line.hash) throw new Error(`freshRun: ${where} gave the hash ${line.hash}, and an earlier child of the arm gave ${known}.`);\n hashes.set(key, line.hash);\n }\n times[arm] = line.ms;\n }\n if (skip !== undefined) break;\n records.push({ pair: pair.id, record: { case: id, round, first: slot.first, a: times.a!, b: times.b!, job, env } });\n }\n if (skip !== undefined) {\n skipped.push(skip);\n gone.add(id);\n continue;\n }\n for (const { pair, record } of records) out[pair]!.push(record);\n }\n }\n return { env, job, pairs: out, skipped };\n}\n\n/** This function starts one child and reads its JSON line. A fault gives an error that names the arm, the case, the round and the tail of `stderr`. */\nasync function child(start: StartChild, argv: readonly string[], limits: StartOptions, where: string): Promise<ChildLine> {\n let exit: ChildExit;\n try {\n exit = await start(argv, limits);\n } catch (error) {\n throw new Error(`freshRun: ${where} did not start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });\n }\n const fail = (why: string): Error => {\n const tail = exit.stderr.length > TAIL ? `...${exit.stderr.slice(-TAIL)}` : exit.stderr;\n return new Error(`freshRun: ${where} ${why}. The tail of stderr: ${tail.trim() === \"\" ? \"(empty)\" : tail.trimEnd()}`);\n };\n if (exit.error !== undefined) throw fail(`failed: ${exit.error}`);\n if (exit.timedOut === true) throw fail(`took more than ${limits.timeoutMs} ms`);\n if (exit.code !== 0) throw fail(exit.code === null ? \"stopped on a signal\" : `exited with the code ${exit.code}`);\n try {\n return childLine(exit.stdout);\n } catch (error) {\n throw fail(`printed no good JSON line (${error instanceof Error ? error.message : String(error)})`);\n }\n}\n\n/** The wait, in milliseconds, for the pipes of a child after its exit, and for the exit of a child after a stop. */\nconst GRACE_MS = 1000;\n\n/**\n * This function starts a process with `spawn` and no shell. It stops the process and its tree at the time limit or at the output limit.\n *\n * - The promise settles at the close of the pipes, or a grace time after the exit of the child. A grandchild can keep a pipe open.\n * - A stop sends SIGTERM to the process group, and SIGKILL after the grace time. On Windows, `taskkill /T /F` stops the tree.\n */\nfunction startProcess(argv: readonly string[], options: StartOptions): Promise<ChildExit> {\n return new Promise((resolve) => {\n const [program, ...rest] = argv;\n const windows = process.platform === \"win32\";\n // On POSIX, the child leads its own process group, so a signal to the group reaches the grandchildren.\n const proc = spawn(program!, rest, { stdio: [\"ignore\", \"pipe\", \"pipe\"], windowsHide: true, detached: !windows });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n let timedOut = false;\n let settled = false;\n let stopping = false;\n let error: string | undefined;\n let grace: ReturnType<typeof setTimeout> | undefined;\n const signal = (name: NodeJS.Signals): void => {\n const pid = proc.pid;\n if (pid === undefined) return;\n if (windows) {\n // TerminateProcess stops only the direct child. The flag /T of taskkill also stops its tree.\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { stdio: \"ignore\", windowsHide: true }).on(\"error\", () => proc.kill());\n return;\n }\n try {\n process.kill(-pid, name);\n } catch {\n proc.kill(name);\n }\n };\n const finish = (code: number | null): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n clearTimeout(grace);\n proc.stdout.destroy();\n proc.stderr.destroy();\n const exit: ChildExit = { stdout: Buffer.concat(out).toString(\"utf8\"), stderr: Buffer.concat(err).toString(\"utf8\"), code, timedOut };\n resolve(error === undefined ? exit : { ...exit, error });\n };\n const stop = (): void => {\n if (stopping) return;\n stopping = true;\n signal(\"SIGTERM\");\n clearTimeout(grace);\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(null);\n }, GRACE_MS);\n };\n const timer = setTimeout(() => {\n timedOut = true;\n stop();\n }, options.timeoutMs);\n const take = (list: Buffer[]) => {\n let bytes = 0;\n return (chunk: Buffer): void => {\n bytes += chunk.length;\n if (bytes > options.maxBuffer) {\n error ??= `an output is more than ${options.maxBuffer} bytes`;\n stop();\n return;\n }\n list.push(chunk);\n };\n };\n proc.stdout.on(\"data\", take(out));\n proc.stderr.on(\"data\", take(err));\n proc.on(\"error\", (e) => {\n error ??= e.message;\n // A program that does not start gives no event close.\n if (proc.pid === undefined) finish(null);\n });\n proc.on(\"exit\", (code) => {\n clearTimeout(timer);\n if (stopping) return;\n // A grandchild that holds a pipe keeps the event close away. After the grace time, the runner stops the group.\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(code);\n }, GRACE_MS);\n });\n proc.on(\"close\", (code) => finish(code));\n });\n}\n"],"mappings":";;;;;;;;;AAoBA,SAAgB,WAAqB;CACnC,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM;EAAE,KAAK,IAAI,EAAE,EAAE,MAAM,KAAK,KAAK;EAAW,OAAO,IAAI;EAAQ,IAAI,SAAS;EAAG,MAAM,KAAK;EAAG,MAAM,QAAQ;CAAQ;CAC7H,MAAM,SAAS,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;CAC1D,OAAO,WAAW,KAAA,KAAa,WAAW,KAAK,MAAM;EAAE,GAAG;EAAK;CAAO;AACxE;;;;;AAyBA,SAAgB,UAAU,QAA2B;CACnD,MAAM,OAAO,OACV,MAAM,IAAI,CAAC,CACX,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,UAAU,MAAM,EAAE,WAAW,GAAG,CAAC;CACpC,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,MAAM,uDAAuD;CAC/F,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,IAAI;CACzB,QAAQ;EACN,MAAM,IAAI,MAAM,4DAA4D,KAAK,MAAM,GAAG,GAAG,GAAG;CAClG;CACA,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,mDAAmD,KAAK,MAAM,GAAG,GAAG,GAAG;CAChK,MAAM,EAAE,IAAI,MAAM,YAAY;CAC9B,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UAAU,MAAM,IAAI,MAAM,wCAAwC,KAAK,UAAU,IAAI,EAAE,EAAE;CACnI,IAAI,YAAY,KAAA,GAAW;EACzB,IAAI,EAAE,OAAO,YAAY,YAAY,YAAY,KAAK,MAAM,IAAI,MAAM,sEAAsE,KAAK,UAAU,OAAO,EAAE,EAAE;EACtK,OAAO;GAAE;GAAS,GAAI,OAAO,OAAO,WAAW,EAAE,GAAG,IAAI,CAAC;GAAI,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;EAAG;CACvG;CACA,IAAI,EAAE,OAAO,OAAO,YAAY,OAAO,SAAS,EAAE,KAAK,KAAK,IAAI,MAAM,IAAI,MAAM,0DAA0D,KAAK,UAAU,EAAE,EAAE,EAAE;CAC/J,OAAO;EAAE;EAAI,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;CAAG;AACvD;;;;;;;AAcA,SAAgB,UAAU,OAAe,OAAe,WAA+B;CACrF,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAAG,MAAM,IAAI,WAAW,yEAAyE,MAAM,EAAE;CACrJ,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAAG,MAAM,IAAI,WAAW,4DAA4D,MAAM,EAAE;CACxI,IAAI,CAAC,OAAO,cAAc,SAAS,KAAK,YAAY,GAAG,MAAM,IAAI,WAAW,wEAAwE,UAAU,EAAE;CAChK,MAAM,SAAS,KAAK,MAAM,QAAQ,CAAC,IAAI,aAAa;CACpD,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM;EAC7C,MAAM,QAAQ,QAAQ,KAAK;EAC3B,OAAO;GAAE;GAAM,OAAO,SAAS,OAAO,YAAY,IAAI;EAAE;CAC1D,CAAC;AACH;;AAwFA,MAAM,YAAY;;AAGlB,MAAM,OAAO;;;;;;;;;;AAWb,eAAsB,SAAS,SAA0C;CACvE,MAAM,EAAE,MAAM,YAAY,OAAO,WAAW;CAC5C,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,YAAY,QAAQ,aAAa,KAAK;CAC5C,MAAM,OAAO,QAAQ,UAAU,OAAe,CAAC,UAAU,EAAE;CAC3D,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,OAAO,cAAc,MAAM,KAAK,SAAS,GAAG,MAAM,IAAI,WAAW,6DAA6D,OAAO,EAAE;CAC5I,IAAI,CAAC,OAAO,cAAc,WAAW,KAAK,cAAc,GAAG,MAAM,IAAI,WAAW,kEAAkE,YAAY,EAAE;CAChK,IAAI,EAAE,YAAY,MAAM,EAAE,YAAY,IAAI,MAAM,IAAI,WAAW,4DAA4D;CAC3H,IAAI,MAAM,WAAW,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,SAAS,MAAM,UAAU,MAAM,MAAM,MAAM,OAAO,MAAM,YAAY,MAAM,EAAE,GACnH,MAAM,IAAI,WAAW,yEAAyE;CAEhG,MAAM,QAAoD,WAAW,KAAK,OAAO;EAAE,IAAI,EAAE;EAAI,GAAG;EAAM,GAAG;CAAE,EAAE;CAC7G,IAAI,QAAQ,MAAM,MAAM,MAAM,KAAK;EAAE,IAAI,GAAG,KAAK,KAAK;EAAa,GAAG;EAAM,GAAG;CAAK,CAAC;CACrF,IAAI,MAAM,WAAW,GAAG,MAAM,IAAI,WAAW,yEAAyE;CACtH,KAAK,MAAM,OAAO,CAAC,MAAM,GAAG,UAAU,GAAG;EACvC,IAAI,OAAO,IAAI,OAAO,YAAY,IAAI,OAAO,IAAI,MAAM,IAAI,WAAW,2DAA2D;EACjI,IAAI,IAAI,QAAQ,WAAW,KAAK,IAAI,QAAQ,MAAM,MAAM,OAAO,MAAM,QAAQ,GAAG,MAAM,IAAI,WAAW,oCAAoC,KAAK,UAAU,IAAI,EAAE,EAAE,uBAAuB;CACzL;CACA,MAAM,sBAAM,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;CAC7B,KAAK,MAAM,EAAE,QAAQ,OAAO;EAC1B,IAAI,IAAI,IAAI,EAAE,GAAG,MAAM,IAAI,WAAW,2CAA2C,KAAK,UAAU,EAAE,EAAE,EAAE;EACtG,IAAI,IAAI,EAAE;CACZ;CAEA,MAAM,MAAM,QAAQ,OAAO,SAAS;CACpC,MAAM,MAAqC,OAAO,YAAY,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;CAC1F,MAAM,UAAuB,CAAC;CAC9B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,yBAAS,IAAI,IAAoB;CAEvC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,MAAM,QAAQ,cAAc;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,KAAK,MAAM;GACjB,IAAI,KAAK,IAAI,EAAE,GAAG;GAClB,MAAM,UAAmD,CAAC;GAC1D,IAAI;GACJ,KAAK,MAAM,QAAQ,UAAU,MAAM,QAAQ,OAAO,CAAC,GAAG;IACpD,MAAM,OAAO,MAAM,KAAK;IACxB,MAAM,QAAsC,CAAC;IAC7C,KAAK,MAAM,OAAO,CAAC,KAAK,OAAO,KAAK,UAAU,MAAM,MAAM,GAAG,GAAY;KACvE,MAAM,MAAM,KAAK;KACjB,MAAM,QAAQ,WAAW,KAAK,UAAU,IAAI,EAAE,EAAE,SAAS,KAAK,UAAU,KAAK,EAAE,EAAE,QAAQ,IAAI,gBAAgB,KAAK,UAAU,EAAE,EAAE,UAAU;KAC1I,MAAM,OAAO,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,SAAS,GAAG,KAAK,EAAE,CAAC,GAAG;MAAE;MAAW;KAAU,GAAG,KAAK;KAC9F,IAAI,KAAK,YAAY,KAAA,GAAW;MAC9B,IAAI,IAAI,GAAG,MAAM,IAAI,MAAM,aAAa,MAAM,sDAAsD,KAAK,UAAU,KAAK,OAAO,EAAE,kEAAkE;MACnM,OAAO;OAAE,KAAK,IAAI;OAAI,MAAM;OAAI;OAAO,QAAQ,KAAK;MAAQ;MAC5D;KACF;KACA,IAAI,KAAK,SAAS,KAAA,GAAW;MAC3B,MAAM,MAAM,GAAG,IAAI,GAAG,QAAQ;MAC9B,MAAM,QAAQ,OAAO,IAAI,GAAG;MAC5B,IAAI,UAAU,KAAA,KAAa,UAAU,KAAK,MAAM,MAAM,IAAI,MAAM,aAAa,MAAM,iBAAiB,KAAK,KAAK,yCAAyC,MAAM,EAAE;MAC/J,OAAO,IAAI,KAAK,KAAK,IAAI;KAC3B;KACA,MAAM,OAAO,KAAK;IACpB;IACA,IAAI,SAAS,KAAA,GAAW;IACxB,QAAQ,KAAK;KAAE,MAAM,KAAK;KAAI,QAAQ;MAAE,MAAM;MAAI;MAAO,OAAO,KAAK;MAAO,GAAG,MAAM;MAAI,GAAG,MAAM;MAAI;MAAK;KAAI;IAAE,CAAC;GACpH;GACA,IAAI,SAAS,KAAA,GAAW;IACtB,QAAQ,KAAK,IAAI;IACjB,KAAK,IAAI,EAAE;IACX;GACF;GACA,KAAK,MAAM,EAAE,MAAM,YAAY,SAAS,IAAI,KAAK,CAAE,KAAK,MAAM;EAChE;CACF;CACA,OAAO;EAAE;EAAK;EAAK,OAAO;EAAK;CAAQ;AACzC;;AAGA,eAAe,MAAM,OAAmB,MAAyB,QAAsB,OAAmC;CACxH,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,MAAM,MAAM,MAAM;CACjC,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,aAAa,MAAM,kBAAkB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAAK,EAAE,OAAO,MAAM,CAAC;CACjI;CACA,MAAM,QAAQ,QAAuB;EACnC,MAAM,OAAO,KAAK,OAAO,SAAS,OAAO,MAAM,KAAK,OAAO,MAAM,IAAK,MAAM,KAAK;EACjF,uBAAO,IAAI,MAAM,aAAa,MAAM,GAAG,IAAI,wBAAwB,KAAK,KAAK,MAAM,KAAK,YAAY,KAAK,QAAQ,GAAG;CACtH;CACA,IAAI,KAAK,UAAU,KAAA,GAAW,MAAM,KAAK,WAAW,KAAK,OAAO;CAChE,IAAI,KAAK,aAAa,MAAM,MAAM,KAAK,kBAAkB,OAAO,UAAU,IAAI;CAC9E,IAAI,KAAK,SAAS,GAAG,MAAM,KAAK,KAAK,SAAS,OAAO,wBAAwB,wBAAwB,KAAK,MAAM;CAChH,IAAI;EACF,OAAO,UAAU,KAAK,MAAM;CAC9B,SAAS,OAAO;EACd,MAAM,KAAK,8BAA8B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,EAAE;CACpG;AACF;;AAGA,MAAM,WAAW;;;;;;;AAQjB,SAAS,aAAa,MAAyB,SAA2C;CACxF,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,CAAC,SAAS,GAAG,QAAQ;EAC3B,MAAM,UAAU,QAAQ,aAAa;EAErC,MAAM,OAAO,MAAM,SAAU,MAAM;GAAE,OAAO;IAAC;IAAU;IAAQ;GAAM;GAAG,aAAa;GAAM,UAAU,CAAC;EAAQ,CAAC;EAC/G,MAAM,MAAgB,CAAC;EACvB,MAAM,MAAgB,CAAC;EACvB,IAAI,WAAW;EACf,IAAI,UAAU;EACd,IAAI,WAAW;EACf,IAAI;EACJ,IAAI;EACJ,MAAM,UAAU,SAA+B;GAC7C,MAAM,MAAM,KAAK;GACjB,IAAI,QAAQ,KAAA,GAAW;GACvB,IAAI,SAAS;IAEX,MAAM,YAAY;KAAC;KAAQ,OAAO,GAAG;KAAG;KAAM;IAAI,GAAG;KAAE,OAAO;KAAU,aAAa;IAAK,CAAC,CAAC,CAAC,GAAG,eAAe,KAAK,KAAK,CAAC;IAC1H;GACF;GACA,IAAI;IACF,QAAQ,KAAK,CAAC,KAAK,IAAI;GACzB,QAAQ;IACN,KAAK,KAAK,IAAI;GAChB;EACF;EACA,MAAM,UAAU,SAA8B;GAC5C,IAAI,SAAS;GACb,UAAU;GACV,aAAa,KAAK;GAClB,aAAa,KAAK;GAClB,KAAK,OAAO,QAAQ;GACpB,KAAK,OAAO,QAAQ;GACpB,MAAM,OAAkB;IAAE,QAAQ,OAAO,OAAO,GAAG,CAAC,CAAC,SAAS,MAAM;IAAG,QAAQ,OAAO,OAAO,GAAG,CAAC,CAAC,SAAS,MAAM;IAAG;IAAM;GAAS;GACnI,QAAQ,UAAU,KAAA,IAAY,OAAO;IAAE,GAAG;IAAM;GAAM,CAAC;EACzD;EACA,MAAM,aAAmB;GACvB,IAAI,UAAU;GACd,WAAW;GACX,OAAO,SAAS;GAChB,aAAa,KAAK;GAClB,QAAQ,iBAAiB;IACvB,IAAI,CAAC,SAAS,OAAO,SAAS;IAC9B,OAAO,IAAI;GACb,GAAG,QAAQ;EACb;EACA,MAAM,QAAQ,iBAAiB;GAC7B,WAAW;GACX,KAAK;EACP,GAAG,QAAQ,SAAS;EACpB,MAAM,QAAQ,SAAmB;GAC/B,IAAI,QAAQ;GACZ,QAAQ,UAAwB;IAC9B,SAAS,MAAM;IACf,IAAI,QAAQ,QAAQ,WAAW;KAC7B,UAAU,0BAA0B,QAAQ,UAAU;KACtD,KAAK;KACL;IACF;IACA,KAAK,KAAK,KAAK;GACjB;EACF;EACA,KAAK,OAAO,GAAG,QAAQ,KAAK,GAAG,CAAC;EAChC,KAAK,OAAO,GAAG,QAAQ,KAAK,GAAG,CAAC;EAChC,KAAK,GAAG,UAAU,MAAM;GACtB,UAAU,EAAE;GAEZ,IAAI,KAAK,QAAQ,KAAA,GAAW,OAAO,IAAI;EACzC,CAAC;EACD,KAAK,GAAG,SAAS,SAAS;GACxB,aAAa,KAAK;GAClB,IAAI,UAAU;GAEd,QAAQ,iBAAiB;IACvB,IAAI,CAAC,SAAS,OAAO,SAAS;IAC9B,OAAO,IAAI;GACb,GAAG,QAAQ;EACb,CAAC;EACD,KAAK,GAAG,UAAU,SAAS,OAAO,IAAI,CAAC;CACzC,CAAC;AACH"}
|