tsc-ts 0.1.0 → 0.1.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 +1 -1
- package/dist/api/compiler/ast.d.ts +9 -3
- package/dist/api/compiler/ast.js +17 -12
- package/dist/api/compiler/checker-classes.js +2 -1
- package/dist/api/compiler/checker-contextual.d.ts +20 -0
- package/dist/api/compiler/checker-contextual.js +111 -1
- package/dist/api/compiler/checker-expressions.js +14 -1
- package/dist/api/compiler/checker-jsx.js +2 -1
- package/dist/api/compiler/checker-type-factory.js +53 -12
- package/dist/api/compiler/checker.d.ts +5 -1
- package/dist/api/compiler/checker.js +10 -4
- package/dist/api/compiler/compile-program.d.ts +5 -0
- package/dist/api/compiler/compile-program.js +8 -0
- package/dist/api/compiler/expr-record.d.ts +11 -0
- package/dist/api/compiler/expr-record.js +53 -0
- package/dist/api/compiler/identifier-name.d.ts +9 -0
- package/dist/api/compiler/identifier-name.js +10 -0
- package/dist/api/compiler/jsdoc.js +4 -2
- package/dist/api/compiler/parser.d.ts +2 -1
- package/dist/api/compiler/parser.js +10 -8
- package/dist/api/compiler/printer/factory.js +3 -2
- package/dist/api/compiler/printer/printer.js +3 -2
- package/dist/api/compiler/reparser.js +4 -3
- package/dist/api/compiler/scanner.d.ts +8 -0
- package/dist/api/compiler/scanner.js +58 -42
- package/dist/api/compiler/shared-results.d.ts +376 -143
- package/dist/api/compiler/shared-results.js +1550 -773
- package/dist/api/compiler/transformers/declarations.js +2 -1
- package/package.json +9 -9
- package/src/compiler/ast.ts +19 -12
- package/src/compiler/cgroup.ts +157 -0
- package/src/compiler/checker-classes.ts +2 -1
- package/src/compiler/checker-contextual.ts +104 -0
- package/src/compiler/checker-expressions.ts +16 -1
- package/src/compiler/checker-jsx.ts +2 -1
- package/src/compiler/checker-type-factory.ts +53 -14
- package/src/compiler/checker.ts +11 -4
- package/src/compiler/compile-program.ts +8 -0
- package/src/compiler/decoder.ts +2 -1
- package/src/compiler/doctor-report.ts +133 -0
- package/src/compiler/expr-record.ts +59 -0
- package/src/compiler/identifier-name.ts +13 -0
- package/src/compiler/jsdoc.ts +4 -2
- package/src/compiler/parallel-check.ts +353 -146
- package/src/compiler/parallel-emit.ts +3 -3
- package/src/compiler/parser.ts +10 -8
- package/src/compiler/printer/factory.ts +3 -2
- package/src/compiler/printer/printer.ts +4 -2
- package/src/compiler/reparser.ts +7 -3
- package/src/compiler/run-config.ts +1383 -0
- package/src/compiler/run-record-read.ts +87 -0
- package/src/compiler/run-records.ts +575 -0
- package/src/compiler/scanner.ts +57 -41
- package/src/compiler/shared-results.ts +1626 -896
- package/src/compiler/thread-entry.ts +3 -1
- package/src/compiler/transformers/declarations.ts +2 -1
- package/src/compiler/tsc-build.ts +64 -6
- package/src/compiler/tsc-cli.ts +33 -4
- package/src/compiler/tsc-compile.ts +213 -8
- package/src/compiler/tsc-watch.ts +25 -4
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run configuration: how many checker threads check a program, whether they share results with each
|
|
3
|
+
* other, and how many threads parse ahead of the program, with where each value came from. The values are
|
|
4
|
+
* pinned by `--checkers` / `"checkers"` and by the `TSC_TS_CHECKERS`, `TSC_TS_SHARE`, `TSC_TS_PARSE_THREADS`
|
|
5
|
+
* and `TSC_TS_AUTO` environment variables; a value that no pin gives is chosen by the program's signals
|
|
6
|
+
* (the static rules below) and, where the project has run before, by what those runs measured: a
|
|
7
|
+
* configuration the record shows to be faster is adopted, an adopted one is re-measured by control runs
|
|
8
|
+
* and dropped when it stops paying, and a configuration the signals leave uncertain is tried in a real run
|
|
9
|
+
* (`decideCheck`, `preParseConfig`).
|
|
10
|
+
*
|
|
11
|
+
* The parse threads must be known before any file is parsed (`preParseConfig`); the checker count and
|
|
12
|
+
* sharing are decided once the program is bound, from what it shows about the cost of checking it
|
|
13
|
+
* (`decideCheck`). `--extendedDiagnostics` prints the values, their sources and how to pin them after the
|
|
14
|
+
* statistics table (`reportRunConfig`).
|
|
15
|
+
*/
|
|
16
|
+
import { availableCpus } from './cgroup.js';
|
|
17
|
+
import {
|
|
18
|
+
shapeChanged,
|
|
19
|
+
type RecordChoice,
|
|
20
|
+
type RecordRejection,
|
|
21
|
+
type RecordSample,
|
|
22
|
+
type RecordShape,
|
|
23
|
+
type RunRecord,
|
|
24
|
+
} from './run-records.js';
|
|
25
|
+
|
|
26
|
+
/** Where a setting's value came from. */
|
|
27
|
+
export enum Source {
|
|
28
|
+
/** An explicit pin: `--checkers`, `"checkers"`, or a `TSC_TS_*` environment variable. */
|
|
29
|
+
Pinned,
|
|
30
|
+
/** What the compiler used before anything chose, and what `TSC_TS_AUTO=0` keeps. */
|
|
31
|
+
Default,
|
|
32
|
+
/** A rule over the program's signals. */
|
|
33
|
+
Static,
|
|
34
|
+
/** What earlier runs of the project measured (run records). */
|
|
35
|
+
Learned,
|
|
36
|
+
/** A measurement taken during this run. */
|
|
37
|
+
Measured,
|
|
38
|
+
/** A run trying an alternative configuration, to learn from it. */
|
|
39
|
+
Explore,
|
|
40
|
+
/** A run re-measuring the default, to keep the learned choice honest. */
|
|
41
|
+
Control,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How much of the selection may run. */
|
|
45
|
+
export enum Automaticity {
|
|
46
|
+
/** Every source may choose: rules, records and in-run switches. */
|
|
47
|
+
On,
|
|
48
|
+
/** Rules and in-run guards only (`TSC_TS_AUTO=static`): no records are read or written. */
|
|
49
|
+
Static,
|
|
50
|
+
/** Nothing but the defaults the pins leave (`TSC_TS_AUTO=0`). */
|
|
51
|
+
Off,
|
|
52
|
+
/**
|
|
53
|
+
* As `On`, and a run may try an alternative the signals leave uncertain (`TSC_TS_AUTO=explore`).
|
|
54
|
+
* Experimental: a run that explores is slower than the configuration it would have used.
|
|
55
|
+
*/
|
|
56
|
+
Explore,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the record may decide any setting: `TSC_TS_AUTO` unset or `1`, and `explore`, which adds the
|
|
61
|
+
* exploration of the settings the signals leave uncertain.
|
|
62
|
+
*/
|
|
63
|
+
export function learns(automaticity: Automaticity): boolean {
|
|
64
|
+
return automaticity === Automaticity.On || automaticity === Automaticity.Explore;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The values a run uses and where each came from. */
|
|
68
|
+
export class RunConfig {
|
|
69
|
+
/** The checker threads that check the program; 1 checks on the main thread. */
|
|
70
|
+
checkers = 1;
|
|
71
|
+
checkersSource: Source = Source.Default;
|
|
72
|
+
/** Whether checker threads share the results they compute (`shared-results.ts`). */
|
|
73
|
+
share = false;
|
|
74
|
+
shareSource: Source = Source.Default;
|
|
75
|
+
/**
|
|
76
|
+
* Whether a sharing run's posts are cost-bounded (`shared-results.ts`): it posts only results another checker
|
|
77
|
+
* demanded and budgets the records a post adds and a take builds. Off by default: a run that gains nothing by
|
|
78
|
+
* it pays for the demand bits and the budget, and no signal separates the projects that gain from those that
|
|
79
|
+
* do not, so a project turns it on by measuring it (`learn`).
|
|
80
|
+
*/
|
|
81
|
+
bounded = false;
|
|
82
|
+
boundedSource: Source = Source.Default;
|
|
83
|
+
/** The threads that parse and bind the program's files ahead of it (`parallel-parse.ts`). */
|
|
84
|
+
parseThreads = 1;
|
|
85
|
+
parseSource: Source = Source.Default;
|
|
86
|
+
/** The pin a value came from, for the section (`--checkers`, `TSC_TS_SHARE`, `TSC_TS_PARSE_THREADS`). */
|
|
87
|
+
checkersPin: string | undefined = undefined;
|
|
88
|
+
sharePin: string | undefined = undefined;
|
|
89
|
+
boundedPin: string | undefined = undefined;
|
|
90
|
+
parsePin: string | undefined = undefined;
|
|
91
|
+
/** How much of the selection may run (`TSC_TS_AUTO`); `Off` leaves every value to the pins and defaults. */
|
|
92
|
+
automaticity = Automaticity.On;
|
|
93
|
+
/** Whether this run reports its signals to a doctor child's report (`TSC_TS_DOCTOR_REPORT`). */
|
|
94
|
+
measure = false;
|
|
95
|
+
/** Why each value was chosen, for the section's `automatic:` reason on that setting's line. */
|
|
96
|
+
readonly checkersReasons: string[] = [];
|
|
97
|
+
readonly shareReasons: string[] = [];
|
|
98
|
+
readonly boundedReasons: string[] = [];
|
|
99
|
+
readonly parseReasons: string[] = [];
|
|
100
|
+
/** Whether this run may write a run record (`run-records.ts`), and why not where it may not. */
|
|
101
|
+
records = true;
|
|
102
|
+
recordsReason: string | undefined = undefined;
|
|
103
|
+
/** What the bound program showed about the cost of checking it (`parallel-check.ts`), where computed. */
|
|
104
|
+
signals: CheckSignals | undefined = undefined;
|
|
105
|
+
/** The project's record of earlier runs, where it was read before this run's files were parsed. */
|
|
106
|
+
record: RunRecord | undefined = undefined;
|
|
107
|
+
/** Where that record lives: its directory and key, and the file they name (`run-records.ts`). */
|
|
108
|
+
recordDirectory: string | undefined = undefined;
|
|
109
|
+
recordKey: string | undefined = undefined;
|
|
110
|
+
recordFile: string | undefined = undefined;
|
|
111
|
+
/** The program's size, where this run computed it (`parallel-check.ts`): its record and the shape check. */
|
|
112
|
+
shape: RecordShape | undefined = undefined;
|
|
113
|
+
/** The role this run's sample gets in the record. */
|
|
114
|
+
role: 'chosen' | 'control' | 'explore' = 'chosen';
|
|
115
|
+
/**
|
|
116
|
+
* The configuration the record keeps as chosen after this run. Learning adopts a measured configuration
|
|
117
|
+
* and reverts to the static one; a run reading no record leaves it for the writer.
|
|
118
|
+
*/
|
|
119
|
+
chosen: RecordChoice | undefined = undefined;
|
|
120
|
+
/**
|
|
121
|
+
* The configuration `fastRevert` rejected in this run, which the record keeps so a later run reads only
|
|
122
|
+
* the samples taken after the rejection as evidence for it (`run-records.ts`).
|
|
123
|
+
*/
|
|
124
|
+
rejected: RecordRejection | undefined = undefined;
|
|
125
|
+
/** The window a sharing run reads its checkers' counters in: a tenth of the recorded check, or 300 ms. */
|
|
126
|
+
shareWindowMs = shareWindow;
|
|
127
|
+
/**
|
|
128
|
+
* Whether this run may try an alternative configuration: `TSC_TS_AUTO=explore` only, and never in CI,
|
|
129
|
+
* watch mode or a concurrent build. Off by default: a run adopts what the record's samples show faster,
|
|
130
|
+
* and tries nothing that would make the run itself slower.
|
|
131
|
+
*/
|
|
132
|
+
explore = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The environment's pins, parsed once per run: `TSC_TS_AUTO`, `TSC_TS_SHARE` and `TSC_TS_PARSE_THREADS`. */
|
|
136
|
+
export interface Pins {
|
|
137
|
+
readonly automaticity: Automaticity;
|
|
138
|
+
/** `TSC_TS_SHARE`: sharing pinned on or off; `undefined` when the variable is not set. */
|
|
139
|
+
readonly share: boolean | undefined;
|
|
140
|
+
/** `TSC_TS_SHARE_BOUNDED`: bounded posting pinned on or off; `undefined` when the variable is not set. */
|
|
141
|
+
readonly bounded: boolean | undefined;
|
|
142
|
+
/** `TSC_TS_PARSE_THREADS`: the parse threads pinned; `undefined` when not set or not a whole number above 0. */
|
|
143
|
+
readonly parseThreads: number | undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `TSC_TS_SHARE` parsed: `1` pins sharing on, `0` pins it off, and anything else pins nothing. */
|
|
147
|
+
export function sharePin(env: Record<string, string | undefined>): boolean | undefined {
|
|
148
|
+
return binaryPin(env.TSC_TS_SHARE);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** `TSC_TS_SHARE_BOUNDED` parsed: `1` pins bounded posting on, `0` pins it off, anything else pins nothing. */
|
|
152
|
+
export function boundedPin(env: Record<string, string | undefined>): boolean | undefined {
|
|
153
|
+
return binaryPin(env.TSC_TS_SHARE_BOUNDED);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `1` as true, `0` as false, and anything else (including unset) as no pin. */
|
|
157
|
+
function binaryPin(value: string | undefined): boolean | undefined {
|
|
158
|
+
return value === '1' ? true : value === '0' ? false : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The pins of `env`: `TSC_TS_AUTO` (`0`, `static`, `explore` or unset), `TSC_TS_SHARE` and `TSC_TS_PARSE_THREADS`. */
|
|
162
|
+
export function pinsOf(env: Record<string, string | undefined>): Pins {
|
|
163
|
+
const automatic = env.TSC_TS_AUTO;
|
|
164
|
+
const threads = env.TSC_TS_PARSE_THREADS === undefined ? NaN : Number(env.TSC_TS_PARSE_THREADS);
|
|
165
|
+
return {
|
|
166
|
+
automaticity:
|
|
167
|
+
automatic === '0'
|
|
168
|
+
? Automaticity.Off
|
|
169
|
+
: automatic === 'static'
|
|
170
|
+
? Automaticity.Static
|
|
171
|
+
: automatic === 'explore'
|
|
172
|
+
? Automaticity.Explore
|
|
173
|
+
: Automaticity.On,
|
|
174
|
+
share: sharePin(env),
|
|
175
|
+
bounded: boundedPin(env),
|
|
176
|
+
parseThreads: Number.isInteger(threads) && threads >= 1 ? threads : undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* How the number of checkers is chosen without `--checkers`: `auto` (the default) uses every CPU this
|
|
182
|
+
* process may run on (`availableCpus()`, at most `maxAutoCheckers`), and at least tsgo's four;
|
|
183
|
+
* fewer than the CPUs, but never fewer than four, when the estimated peak memory of more checkers would
|
|
184
|
+
* not fit. `fixed` (`TSC_TS_CHECKERS=fixed`) uses tsgo's four.
|
|
185
|
+
*/
|
|
186
|
+
export function checkersPolicy(env: Record<string, string | undefined>): 'fixed' | 'auto' {
|
|
187
|
+
return env.TSC_TS_CHECKERS === 'fixed' ? 'fixed' : 'auto';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** tsgo's number of checkers, and the fewest `auto` starts. */
|
|
191
|
+
export const tsgoCheckers = 4;
|
|
192
|
+
|
|
193
|
+
/** The most checkers `auto` starts (measured up to 16 CPUs). */
|
|
194
|
+
export const maxAutoCheckers = 16;
|
|
195
|
+
|
|
196
|
+
/** `auto` with `cpus` CPUs when the memory estimate fits `fit` checkers: tsgo's four at least. */
|
|
197
|
+
export function autoCheckers(cpus: number, fit: number): number {
|
|
198
|
+
return Math.max(tsgoCheckers, Math.min(cpus, fit));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* When a checker's own files are a small part of what it resolves (`libraryBoundCount`): the least
|
|
203
|
+
* text, in characters, of the libraries the files of a checker's range reach, on average over the
|
|
204
|
+
* ranges, and the least multiple of the text of the range's own files it is.
|
|
205
|
+
*/
|
|
206
|
+
export const libraryBoundText = 6_000_000;
|
|
207
|
+
export const libraryBoundShare = 5;
|
|
208
|
+
|
|
209
|
+
/** Whether ranges that reach `library` characters of libraries and check `own` are library-bound. */
|
|
210
|
+
export function isLibraryBound(library: number, own: number): boolean {
|
|
211
|
+
return library >= libraryBoundText && library >= libraryBoundShare * own;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The number of checkers for `count` checkers whose ranges reach `library` characters of libraries
|
|
216
|
+
* and check `own` characters each, on average: three quarters of them (tsgo's four at least) where
|
|
217
|
+
* every checker spends most of its time resolving the same large libraries (`libraryBoundText`,
|
|
218
|
+
* `libraryBoundShare`), else `count`.
|
|
219
|
+
*
|
|
220
|
+
* Each checker resolves the declarations its files reach, whatever else it checks. Where they are
|
|
221
|
+
* millions of characters, such as an Effect-based project's, and much more than the checker's own
|
|
222
|
+
* files, a checker costs seconds of CPU and hundreds of MB before its files get cheap, and the last
|
|
223
|
+
* quarter of the checkers (on 16 CPUs of 8 cores, the ones sharing a core with another) repeat that
|
|
224
|
+
* work rather than save time: T3 Code's web and server projects ran 3% and 1% faster with 12
|
|
225
|
+
* checkers than with 16, and used 16% and 8% less memory. Where checkers resolve less, or mostly
|
|
226
|
+
* check their own files, 12 of 16 checkers were slower: Excalidraw (1.4M library and 0.15M own
|
|
227
|
+
* characters per range) by 6%, Sentry (5.7M and 3.0M) and the tsc-ts sources by about 11%.
|
|
228
|
+
*/
|
|
229
|
+
export function libraryBoundCount(count: number, library: number, own: number): number {
|
|
230
|
+
return isLibraryBound(library, own) ? Math.max(tsgoCheckers, Math.ceil((count * 3) / 4)) : count;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The least checkers sharing can pay with: 12, from T3 Code's two projects at 16 and 4 neutral to
|
|
235
|
+
* slightly worse (1.028). c8 and c12 are unmeasured, so 12 is where the measurements begin.
|
|
236
|
+
*/
|
|
237
|
+
export const minSharingCheckers = 12;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The share of a program's files that must be declaration files or come from `node_modules` (F) for
|
|
241
|
+
* sharing to pay: the files every checker resolves again. T3 Code's projects are at 0.48 and 0.66;
|
|
242
|
+
* TypeORM, VS Code and Sentry at 0.08, 0.08 and 0.26, where sharing was neutral or slower.
|
|
243
|
+
*/
|
|
244
|
+
export const sharingFileShare = 0.4;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* How much sharing raises the estimated peak memory, for the fit a count must pass before sharing turns
|
|
248
|
+
* on: peak RSS grew by up to 11% at 16 checkers (T3 web), and each extra checker's declarations are
|
|
249
|
+
* resolved by one more checker.
|
|
250
|
+
*/
|
|
251
|
+
export const sharingPeakShare = 1.12;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Whether `signals` say that sharing pays: at least `minSharingCheckers` checkers, `sharingFileShare`
|
|
255
|
+
* of the program declarations or from node_modules, and ranges that mostly resolve the same large
|
|
256
|
+
* libraries — the work sharing removes. The memory fit (`CheckFit`) is checked by `decideCheck`.
|
|
257
|
+
*/
|
|
258
|
+
export function sharingCandidate(signals: CheckSignals): boolean {
|
|
259
|
+
return (
|
|
260
|
+
signals.count >= minSharingCheckers &&
|
|
261
|
+
signals.fileShare >= sharingFileShare &&
|
|
262
|
+
isLibraryBound(signals.library, signals.own)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The window a sharing run reads its counters in where the record has no check time: 300 ms. */
|
|
267
|
+
export const shareWindow = 300;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Learning (DESIGN 3, 4, 5). A run with a record of the project's earlier runs compares the configurations
|
|
271
|
+
* those samples measured with the ones the static rules choose. It needs three usable samples of each arm
|
|
272
|
+
* before it trusts a comparison (`comparisonSamples`), and a configuration beats another only when it is
|
|
273
|
+
* faster by the margin the runs around each sample leave (`beatsPaired`, DESIGN 4.1: 3% at least, twice the
|
|
274
|
+
* standard error of the paired gains), or, where the record has too few pairs, by `marginOf` (3% at least, twice the
|
|
275
|
+
* larger arm's relative MAD). Adopted configurations are re-measured by control runs and dropped by the
|
|
276
|
+
* fast-revert rule; a configuration the signals leave uncertain is tried in a real run, at most one in
|
|
277
|
+
* `exploreRuns` and never when the record shows it loses.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
/** The fewest usable samples of each arm a comparison needs. */
|
|
281
|
+
export const comparisonSamples = 3;
|
|
282
|
+
/** The smallest margin one configuration must beat another by (DESIGN 4.1: T1's c1 noise band). */
|
|
283
|
+
export const marginFloor = 0.03;
|
|
284
|
+
/** The load above which a sample is kept in the record but not used for a decision (DESIGN 3.3). */
|
|
285
|
+
export const usableLoad = 0.5;
|
|
286
|
+
/** The runs between two control runs of an adopted configuration (DESIGN 4.2). */
|
|
287
|
+
export const controlRuns = 16;
|
|
288
|
+
/** The seconds after which an adopted configuration is re-measured, even without `controlRuns` runs. */
|
|
289
|
+
export const controlAfterSeconds = 14 * 86400;
|
|
290
|
+
/** The fewest runs between two exploring runs (DESIGN 4.4). */
|
|
291
|
+
export const exploreRuns = 8;
|
|
292
|
+
/** The least recorded check time exploration is worth its noise at, milliseconds (DESIGN 4.4). */
|
|
293
|
+
export const exploreCheckMs = 2000;
|
|
294
|
+
/** The fewest checkers a sharing exploration runs with (DESIGN 4.4). */
|
|
295
|
+
export const exploreSharingCheckers = 8;
|
|
296
|
+
/** The least file share at which sharing is uncertain (DESIGN 4.4). */
|
|
297
|
+
export const exploreSharingShare = 0.3;
|
|
298
|
+
/** The band of L / O within which the checker count is uncertain (DESIGN 4.4). */
|
|
299
|
+
export const exploreLibraryRatio = 2.5;
|
|
300
|
+
export const exploreLibraryRatioHigh = 10;
|
|
301
|
+
/** The least library text at which the checker count is uncertain (DESIGN 4.4). */
|
|
302
|
+
export const exploreLibraryText = 3_000_000;
|
|
303
|
+
|
|
304
|
+
/** The median of `values`, `undefined` where there are none. */
|
|
305
|
+
export function medianOf(values: readonly number[]): number | undefined {
|
|
306
|
+
if (values.length === 0) return undefined;
|
|
307
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
308
|
+
const middle = sorted.length >> 1;
|
|
309
|
+
return sorted.length % 2 === 1 ? sorted[middle]! : (sorted[middle - 1]! + sorted[middle]!) / 2;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** An arm's relative noise: 1.4826 × its median absolute deviation / its median (DESIGN 4.1). */
|
|
313
|
+
export function noiseOf(values: readonly number[]): number {
|
|
314
|
+
const median = medianOf(values);
|
|
315
|
+
if (median === undefined || median <= 0) return 0;
|
|
316
|
+
const deviations = values.map((value) => Math.abs(value - median));
|
|
317
|
+
return (1.4826 * (medianOf(deviations) ?? 0)) / median;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The margin `a` and `b` must differ by: 3%, or twice the larger arm's noise (DESIGN 4.1). */
|
|
321
|
+
export function marginOf(a: readonly number[], b: readonly number[]): number {
|
|
322
|
+
return Math.max(marginFloor, 2 * Math.max(noiseOf(a), noiseOf(b)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* How much faster `better` is than `worse`, where it beats it by `marginOf`: `0.21` for 21% faster,
|
|
327
|
+
* `undefined` where there are fewer than `comparisonSamples` usable samples of either arm or no margin
|
|
328
|
+
* separates them.
|
|
329
|
+
*/
|
|
330
|
+
export function beatsBy(better: readonly number[], worse: readonly number[]): number | undefined {
|
|
331
|
+
if (better.length < comparisonSamples || worse.length < comparisonSamples) return undefined;
|
|
332
|
+
const b = medianOf(better)!;
|
|
333
|
+
const w = medianOf(worse)!;
|
|
334
|
+
if (b <= 0 || w <= 0) return undefined;
|
|
335
|
+
return b <= w * (1 - marginOf(better, worse)) ? 1 - b / w : undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** The samples of `record` a decision may use: those taken while the machine was not busy. */
|
|
339
|
+
export function usableSamples(record: RunRecord): RecordSample[] {
|
|
340
|
+
return record.samples.filter((sample) => sample.load <= usableLoad);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A configuration a paired comparison names: the three settings a sample's check time depends on. */
|
|
344
|
+
export interface CheckArm {
|
|
345
|
+
readonly checkers: number;
|
|
346
|
+
readonly share: boolean;
|
|
347
|
+
readonly bounded: boolean;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Whether `sample` checked with `arm`. */
|
|
351
|
+
function checkedWithArm(sample: RecordSample, arm: CheckArm): boolean {
|
|
352
|
+
return sample.checkers === arm.checkers && sample.share === arm.share && (sample.bounded ?? false) === arm.bounded;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The relative gains of each sample of `better` against the sample of `worse` nearest to it in record order: `1 -
|
|
357
|
+
* better/worse` for every sample of the first arm that has one of the second to compare with. Runs minutes apart
|
|
358
|
+
* meet the same memory pressure — the page cache, the allocator, the garbage collector's footprint — and that
|
|
359
|
+
* pressure moves a whole session's runs together, so the spread of either arm's samples over-counts it
|
|
360
|
+
* (`noiseOf` reads that spread). A difference against the nearest run of the other arm cancels it, which is what
|
|
361
|
+
* this reads. A tie goes to the earlier sample, the run the alternative was tried after.
|
|
362
|
+
*
|
|
363
|
+
* The record may alternate the two arms or space them `exploreRuns` apart; either way a sample is compared with
|
|
364
|
+
* the record's nearest evidence of the other arm, not with a global median. Each sample of `worse` pairs once:
|
|
365
|
+
* three alternative samples must read three runs of the baseline, so a record with one baseline sample gives one
|
|
366
|
+
* gain however many alternative samples it has.
|
|
367
|
+
*/
|
|
368
|
+
export function pairedGains(samples: readonly RecordSample[], better: CheckArm, worse: CheckArm): number[] {
|
|
369
|
+
const gains: number[] = [];
|
|
370
|
+
const paired: boolean[] = [];
|
|
371
|
+
for (let i = 0; i < samples.length; i++) {
|
|
372
|
+
const sample = samples[i]!;
|
|
373
|
+
if (!checkedWithArm(sample, better)) continue;
|
|
374
|
+
let near = -1;
|
|
375
|
+
for (let j = 0; j < samples.length; j++) {
|
|
376
|
+
if (j === i || paired[j] || !checkedWithArm(samples[j]!, worse)) continue;
|
|
377
|
+
const distance = Math.abs(j - i);
|
|
378
|
+
if (near < 0 || distance < Math.abs(near - i) || (distance === Math.abs(near - i) && j < near)) near = j;
|
|
379
|
+
}
|
|
380
|
+
if (near < 0) continue;
|
|
381
|
+
paired[near] = true;
|
|
382
|
+
const worseMs = samples[near]!.checkMs;
|
|
383
|
+
if (worseMs > 0) gains.push(1 - sample.checkMs / worseMs);
|
|
384
|
+
}
|
|
385
|
+
return gains;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* How much faster `better` is than `worse`, from the gains of `pairedGains`: `0.11` for 11% faster, `undefined`
|
|
390
|
+
* where the record cannot show it. Two views must agree. The median paired gain must clear `marginFloor` and
|
|
391
|
+
* twice the standard error of the median of the gains, 2 × 1.2533 × 1.4826 × their median absolute deviation /
|
|
392
|
+
* √(the pairs): the spread two neighbouring runs do not share is what one pair is worth, and a median of several
|
|
393
|
+
* pairs is closer than that. The arms' medians must also differ by the floor, so a gain made up of a few lucky
|
|
394
|
+
* pairs cannot carry a comparison the record as a whole does not show. The comparison needs `comparisonSamples`
|
|
395
|
+
* pairs, and `pairedGains` pairs each baseline sample once, so that is three usable samples of each arm.
|
|
396
|
+
*/
|
|
397
|
+
export function beatsPaired(samples: readonly RecordSample[], better: CheckArm, worse: CheckArm): number | undefined {
|
|
398
|
+
const gains = pairedGains(samples, better, worse);
|
|
399
|
+
if (gains.length < comparisonSamples) return undefined;
|
|
400
|
+
return pairedBeat(samples, better, worse, gains);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** `beatsPaired` over `gains`, which the caller has already found to be `comparisonSamples` or more. */
|
|
404
|
+
function pairedBeat(
|
|
405
|
+
samples: readonly RecordSample[],
|
|
406
|
+
better: CheckArm,
|
|
407
|
+
worse: CheckArm,
|
|
408
|
+
gains: readonly number[],
|
|
409
|
+
): number | undefined {
|
|
410
|
+
const median = medianOf(gains)!;
|
|
411
|
+
const spread = 1.4826 * (medianOf(gains.map((gain) => Math.abs(gain - median))) ?? 0);
|
|
412
|
+
if (median < Math.max(marginFloor, (2 * 1.2533 * spread) / Math.sqrt(gains.length))) return undefined;
|
|
413
|
+
const b = medianOf(checkTimes(samples, better.checkers, better.share, better.bounded));
|
|
414
|
+
const w = medianOf(checkTimes(samples, worse.checkers, worse.share, worse.bounded));
|
|
415
|
+
if (b === undefined || w === undefined || b <= 0 || w <= 0 || b > w * (1 - marginFloor)) return undefined;
|
|
416
|
+
return median;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* How much faster `better` is than `worse` on the record's evidence: the median paired gain where the record has
|
|
421
|
+
* enough pairs of neighbouring samples (`beatsPaired`), and the arm medians by `marginOf` only where it has too
|
|
422
|
+
* few pairs to read one (`beatsBy`). A paired comparison that found the pairs and still could not show the gain
|
|
423
|
+
* is the record's verdict on the alternative, and the fallback does not overrule it.
|
|
424
|
+
*
|
|
425
|
+
* `considered` is the alternative's samples with the rejected configuration's left out; the static arm is
|
|
426
|
+
* compared over every sample the record kept.
|
|
427
|
+
*/
|
|
428
|
+
function beats(
|
|
429
|
+
samples: readonly RecordSample[],
|
|
430
|
+
considered: readonly RecordSample[],
|
|
431
|
+
better: CheckArm,
|
|
432
|
+
worse: CheckArm,
|
|
433
|
+
): number | undefined {
|
|
434
|
+
const gains = pairedGains(considered, better, worse);
|
|
435
|
+
if (gains.length >= comparisonSamples) return pairedBeat(considered, better, worse, gains);
|
|
436
|
+
return beatsBy(
|
|
437
|
+
checkTimes(considered, better.checkers, better.share, better.bounded),
|
|
438
|
+
checkTimes(samples, worse.checkers, worse.share, worse.bounded),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The check times of the samples that checked with `checkers` checkers, sharing `share`, and cost-bounded posting
|
|
444
|
+
* `bounded`. Every comparison names all three arms: a sample without the field is an unbounded one.
|
|
445
|
+
*/
|
|
446
|
+
export function checkTimes(
|
|
447
|
+
samples: readonly RecordSample[],
|
|
448
|
+
checkers: number,
|
|
449
|
+
share: boolean,
|
|
450
|
+
bounded: boolean,
|
|
451
|
+
): number[] {
|
|
452
|
+
const times: number[] = [];
|
|
453
|
+
for (const sample of samples)
|
|
454
|
+
if (sample.checkers === checkers && sample.share === share && (sample.bounded === true) === bounded)
|
|
455
|
+
times.push(sample.checkMs);
|
|
456
|
+
return times;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** The parse times of the samples that parsed with `parseThreads` threads. */
|
|
460
|
+
export function parseTimes(samples: readonly RecordSample[], parseThreads: number): number[] {
|
|
461
|
+
const times: number[] = [];
|
|
462
|
+
for (const sample of samples) if (sample.parseThreads === parseThreads) times.push(sample.parseMs);
|
|
463
|
+
return times;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The least margin between masses of the two arms a fast revert needs: the three adopted samples against
|
|
468
|
+
* control samples that may be fewer than `comparisonSamples`.
|
|
469
|
+
*/
|
|
470
|
+
function revertMargin(adopted: readonly number[], control: readonly number[]): number {
|
|
471
|
+
return Math.max(marginFloor, 2 * Math.max(noiseOf(adopted), noiseOf(control)));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Whether the last three usable samples of an adopted configuration all measured slower than the latest
|
|
476
|
+
* control samples by more than the margin: the adopted choice has gone bad and reverts now, without
|
|
477
|
+
* waiting for three new control samples (DESIGN 4.2).
|
|
478
|
+
*/
|
|
479
|
+
export function fastRevert(
|
|
480
|
+
samples: readonly RecordSample[],
|
|
481
|
+
checkers: number,
|
|
482
|
+
share: boolean,
|
|
483
|
+
bounded: boolean,
|
|
484
|
+
): boolean {
|
|
485
|
+
const adopted: number[] = [];
|
|
486
|
+
for (const sample of samples)
|
|
487
|
+
if (sample.checkers === checkers && sample.share === share && (sample.bounded === true) === bounded)
|
|
488
|
+
adopted.push(sample.checkMs);
|
|
489
|
+
if (adopted.length < comparisonSamples) return false;
|
|
490
|
+
const last = adopted.slice(-comparisonSamples);
|
|
491
|
+
const control: number[] = [];
|
|
492
|
+
for (const sample of samples)
|
|
493
|
+
if (sample.role === 'control' && sample.load <= usableLoad) control.push(sample.checkMs);
|
|
494
|
+
if (control.length === 0) return false;
|
|
495
|
+
const baseline = medianOf(control.slice(-comparisonSamples))!;
|
|
496
|
+
const margin = revertMargin(last, control.slice(-comparisonSamples));
|
|
497
|
+
return last.every((ms) => ms > baseline * (1 + margin));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** The window a sharing run reads its counters in: a tenth of the recorded check, at least 200 ms. */
|
|
501
|
+
export function shareWindowOf(
|
|
502
|
+
record: RunRecord | undefined,
|
|
503
|
+
checkers: number,
|
|
504
|
+
share: boolean,
|
|
505
|
+
bounded: boolean,
|
|
506
|
+
): number {
|
|
507
|
+
if (record === undefined) return shareWindow;
|
|
508
|
+
const samples = usableSamples(record);
|
|
509
|
+
const times =
|
|
510
|
+
checkTimes(samples, checkers, share, bounded).length > 0
|
|
511
|
+
? checkTimes(samples, checkers, share, bounded)
|
|
512
|
+
: checkTimes(samples, checkers, share, !bounded);
|
|
513
|
+
const median = medianOf(times);
|
|
514
|
+
if (median === undefined) return shareWindow;
|
|
515
|
+
return Math.max(200, Math.round(median / 10));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** `fraction` as a whole percentage, `21` for 0.213. */
|
|
519
|
+
function percent(fraction: number): number {
|
|
520
|
+
return Math.round(fraction * 100);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The number of threads that parse ahead of a program checked by `checkers` checkers: every CPU this
|
|
525
|
+
* process may run on, as tsgo parses on every core whatever its checker count (parse threads hand
|
|
526
|
+
* their files to the shared program; what else they allocate is reused by the checker threads), but
|
|
527
|
+
* one. The main thread hands out the chunks they load: with a thread on every CPU, it waits for a CPU
|
|
528
|
+
* each time a thread returns a chunk, and so does that thread for its next chunk (VS Code's parse took
|
|
529
|
+
* 18-57 ms longer with 16 threads on 16 CPUs than with 15).
|
|
530
|
+
*/
|
|
531
|
+
export function defaultParseThreads(checkers: number): number {
|
|
532
|
+
const cpus = availableCpus();
|
|
533
|
+
return Math.max(1, Math.min(cpus - 1, Math.max(checkers, Math.min(cpus, maxAutoCheckers))));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** The options the configuration reads: `"checkers"` or `--checkers` pins the count (`CompilerOptions`). */
|
|
537
|
+
export interface RunOptions {
|
|
538
|
+
readonly checkers?: number;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The run's configuration for a program with `options`, before its files are parsed: the parse threads,
|
|
543
|
+
* which are fixed before any file is known, and the pins of the checker count and sharing. `checkers` is
|
|
544
|
+
* the count for the options without the program (`checkerThreads`); the count and sharing are decided
|
|
545
|
+
* after bind, in `decideCheck`.
|
|
546
|
+
*
|
|
547
|
+
* `record` is the project's record of earlier runs where this run may read one; its chosen parse threads
|
|
548
|
+
* apply here, and where the record leaves them uncertain and the static rules have nothing to try, this
|
|
549
|
+
* run parses with half the default threads to measure them. `explore` is false where a run may not try an
|
|
550
|
+
* alternative (watch mode, a concurrent build); CI is read from `env`.
|
|
551
|
+
*/
|
|
552
|
+
export function preParseConfig(
|
|
553
|
+
options: RunOptions,
|
|
554
|
+
checkers: number,
|
|
555
|
+
env: Record<string, string | undefined> = process.env,
|
|
556
|
+
record: RunRecord | undefined = undefined,
|
|
557
|
+
explore: boolean = true,
|
|
558
|
+
): RunConfig {
|
|
559
|
+
const config = new RunConfig();
|
|
560
|
+
const pins = pinsOf(env);
|
|
561
|
+
config.automaticity = pins.automaticity;
|
|
562
|
+
config.checkers = checkers;
|
|
563
|
+
config.record = record;
|
|
564
|
+
const ci = env.CI;
|
|
565
|
+
config.explore =
|
|
566
|
+
explore && pins.automaticity === Automaticity.Explore && (ci === undefined || ci === '' || ci === 'false');
|
|
567
|
+
if (options.checkers !== undefined) {
|
|
568
|
+
config.checkersSource = Source.Pinned;
|
|
569
|
+
config.checkersPin = '--checkers';
|
|
570
|
+
} else if (checkersPolicy(env) === 'fixed') {
|
|
571
|
+
config.checkersSource = Source.Pinned;
|
|
572
|
+
config.checkersPin = 'TSC_TS_CHECKERS';
|
|
573
|
+
}
|
|
574
|
+
const parseDefault = defaultParseThreads(checkers);
|
|
575
|
+
if (pins.parseThreads !== undefined) {
|
|
576
|
+
config.parseThreads = pins.parseThreads;
|
|
577
|
+
config.parseSource = Source.Pinned;
|
|
578
|
+
config.parsePin = 'TSC_TS_PARSE_THREADS';
|
|
579
|
+
} else if (controlDue(record, pins.automaticity, Math.floor(Date.now() / 1000))) {
|
|
580
|
+
// A control run parses with the default threads too: it re-measures the static choice whole.
|
|
581
|
+
config.parseThreads = parseDefault;
|
|
582
|
+
config.parseSource = Source.Control;
|
|
583
|
+
config.role = 'control';
|
|
584
|
+
} else {
|
|
585
|
+
config.parseThreads = parseDefault;
|
|
586
|
+
const learned = learnedParseThreads(record, parseDefault, pins.automaticity);
|
|
587
|
+
const explored = learned === undefined ? exploreParseThreads(record, parseDefault, config.explore) : undefined;
|
|
588
|
+
if (learned !== undefined) {
|
|
589
|
+
config.parseThreads = learned.threads;
|
|
590
|
+
config.parseSource = Source.Learned;
|
|
591
|
+
config.parseReasons.push(learned.reason);
|
|
592
|
+
} else if (explored !== undefined) {
|
|
593
|
+
config.parseThreads = explored;
|
|
594
|
+
config.parseSource = Source.Explore;
|
|
595
|
+
config.role = 'explore';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (pins.share !== undefined) {
|
|
599
|
+
config.share = pins.share;
|
|
600
|
+
config.shareSource = Source.Pinned;
|
|
601
|
+
config.sharePin = 'TSC_TS_SHARE';
|
|
602
|
+
}
|
|
603
|
+
if (pins.bounded !== undefined) {
|
|
604
|
+
config.bounded = pins.bounded;
|
|
605
|
+
config.boundedSource = Source.Pinned;
|
|
606
|
+
config.boundedPin = 'TSC_TS_SHARE_BOUNDED';
|
|
607
|
+
}
|
|
608
|
+
if (env.TSC_TS_CACHE === '0') {
|
|
609
|
+
config.records = false;
|
|
610
|
+
config.recordsReason = 'TSC_TS_CACHE=0';
|
|
611
|
+
} else if (!learns(pins.automaticity)) {
|
|
612
|
+
config.records = false;
|
|
613
|
+
config.recordsReason = 'TSC_TS_AUTO=' + (pins.automaticity === Automaticity.Off ? '0' : 'static');
|
|
614
|
+
}
|
|
615
|
+
// A doctor child reports the signals even though it keeps no record (`doctor-report.ts`).
|
|
616
|
+
const report = env.TSC_TS_DOCTOR_REPORT;
|
|
617
|
+
config.measure = report !== undefined && report !== '';
|
|
618
|
+
return config;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* What a bound program shows about the cost of checking it, which the rules that choose the checker
|
|
623
|
+
* count and sharing read. The ranges are the checker threads' ranges of files (`planSchedule`).
|
|
624
|
+
*/
|
|
625
|
+
export class CheckSignals {
|
|
626
|
+
/** The checkers the plan was made for, before any rule cut the count. */
|
|
627
|
+
count: number;
|
|
628
|
+
/** The statement ranges of the plan; 0 where every file is checked whole. */
|
|
629
|
+
ranges: number;
|
|
630
|
+
/** Characters of the libraries a checker's range reaches, on average over the ranges (L). */
|
|
631
|
+
library = 0;
|
|
632
|
+
/** Characters of the files a checker's range checks, on average over the ranges (O). */
|
|
633
|
+
own = 0;
|
|
634
|
+
/** The share of `library` + `own` that a second range reaches too (R). */
|
|
635
|
+
redundancy = 0;
|
|
636
|
+
/** The share of the program's files that are declaration files (F_d). */
|
|
637
|
+
declarationShare = 0;
|
|
638
|
+
/** The share of the program's files that come from `node_modules` (F_n). */
|
|
639
|
+
nodeModulesShare = 0;
|
|
640
|
+
|
|
641
|
+
constructor(count: number, ranges: number) {
|
|
642
|
+
this.count = count;
|
|
643
|
+
this.ranges = ranges;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** F = max(F_d, F_n): the share of the program that is declarations every checker resolves again. */
|
|
647
|
+
get fileShare(): number {
|
|
648
|
+
return Math.max(this.declarationShare, this.nodeModulesShare);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* The checkers whose estimated peak memory fits a program: without sharing, and with the peak sharing
|
|
654
|
+
* adds (`sharingPeakShare`). Computed by `parallel-check.ts`, which holds the estimate.
|
|
655
|
+
*/
|
|
656
|
+
export interface CheckFit {
|
|
657
|
+
/** The checkers whose estimated peak memory fits with the peak sharing adds (`sharingPeakShare`). */
|
|
658
|
+
readonly sharing: number;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** What `decideCheck` needs of the run besides the signals: whether it can share, and how many checkers fit. */
|
|
662
|
+
export interface CheckDecision {
|
|
663
|
+
/** Whether checking happens on checker threads that share the program (scriptc builds), more than one. */
|
|
664
|
+
readonly possible: boolean;
|
|
665
|
+
/** The memory fit, where the sharing rule needs it; `undefined` elsewhere. */
|
|
666
|
+
readonly fit: CheckFit | undefined;
|
|
667
|
+
/** The program's shape, where this run read a record: its samples describe this program only if it matches. */
|
|
668
|
+
readonly shape?: RecordShape;
|
|
669
|
+
/** The CPUs automatic selection may use (`min(availableCpus(), maxAutoCheckers)`). */
|
|
670
|
+
readonly cpus?: number;
|
|
671
|
+
/** The memory limit, in bytes: a configuration whose recorded peak exceeds `memoryShare` of it is not chosen. */
|
|
672
|
+
readonly memoryBytes?: number;
|
|
673
|
+
/** The time, seconds since the epoch, for the age of the last control sample (DESIGN 4.2). */
|
|
674
|
+
readonly now?: number;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** The decision of a run that cannot share, made without the memory fit. */
|
|
678
|
+
export const noSharing: CheckDecision = { possible: false, fit: undefined };
|
|
679
|
+
|
|
680
|
+
/** The share of the memory limit a configuration may have reached at its peak (DESIGN 5.5). */
|
|
681
|
+
export const memoryShare = 0.85;
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* The checker count and sharing for a bound program, from `signals`; returns the count.
|
|
685
|
+
*
|
|
686
|
+
* The count is the one the plan was made for, cut to three quarters where every range mostly resolves
|
|
687
|
+
* the same large libraries and no pin gave it (`libraryBoundCount`, `tests/parallel-schedule` records
|
|
688
|
+
* the measurements).
|
|
689
|
+
*
|
|
690
|
+
* Sharing is turned on by the static rule where the run may choose it and can share, the signals say it
|
|
691
|
+
* pays (`sharingCandidate`), the count it leaves is one sharing was measured at (`minSharingCheckers`),
|
|
692
|
+
* and that count fits memory with the peak sharing adds. The cut stays on: 16 checkers with sharing are
|
|
693
|
+
* within noise of the cut count with it on T3 Code's two projects (Q2: 0.99 and 1.03 of the cut count,
|
|
694
|
+
* the intervals covering 1), and the cut uses 0.4 GB to 0.9 GB less at the peak, so the count is not
|
|
695
|
+
* raised for sharing.
|
|
696
|
+
*
|
|
697
|
+
* Where the run read the project's record, the samples it holds decide more: an adopted configuration
|
|
698
|
+
* stays until the static one beats it and reverts early when it stops paying (`learn`), a configuration
|
|
699
|
+
* the signals leave uncertain is tried, and a run in every `controlRuns` re-measures the static choice.
|
|
700
|
+
* That includes bounded posting (`RunConfig.bounded`), which no signal decides and which a project learns
|
|
701
|
+
* from its own runs: T3 web's checks were 7% faster with it at 16 checkers and T3 server's 8-9% slower.
|
|
702
|
+
*
|
|
703
|
+
* Sharing is what `TSC_TS_SHARE` pins, and bounded posting `TSC_TS_SHARE_BOUNDED`. The decisions are those
|
|
704
|
+
* of a run before this module existed, and what `TSC_TS_AUTO=0` keeps.
|
|
705
|
+
*/
|
|
706
|
+
export function decideCheck(config: RunConfig, signals: CheckSignals, decision: CheckDecision = noSharing): number {
|
|
707
|
+
const count = signals.count;
|
|
708
|
+
const pinned = config.checkersSource === Source.Pinned;
|
|
709
|
+
const cut = pinned || count <= tsgoCheckers ? count : libraryBoundCount(count, signals.library, signals.own);
|
|
710
|
+
config.checkers = cut;
|
|
711
|
+
const fit = decision.fit;
|
|
712
|
+
if (
|
|
713
|
+
decision.possible &&
|
|
714
|
+
fit !== undefined &&
|
|
715
|
+
fit.sharing >= cut &&
|
|
716
|
+
cut >= minSharingCheckers &&
|
|
717
|
+
config.shareSource !== Source.Pinned &&
|
|
718
|
+
config.automaticity !== Automaticity.Off &&
|
|
719
|
+
sharingCandidate(signals)
|
|
720
|
+
) {
|
|
721
|
+
config.share = true;
|
|
722
|
+
config.shareSource = Source.Static;
|
|
723
|
+
config.shareReasons.push(sharingReason(signals));
|
|
724
|
+
}
|
|
725
|
+
config.shape = decision.shape;
|
|
726
|
+
if (learns(config.automaticity) && config.record !== undefined) learn(config, signals, decision, cut);
|
|
727
|
+
config.shareWindowMs = shareWindowOf(config.record, config.checkers, config.share, config.bounded);
|
|
728
|
+
return config.checkers;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* The static rules' choice and the record's evidence decide this run's checker count and sharing, and the
|
|
733
|
+
* configuration the record keeps as chosen (DESIGN 4.2-4.4):
|
|
734
|
+
*
|
|
735
|
+
* - an adopted configuration stays while the static one does not beat it, and reverts at once when its
|
|
736
|
+
* last three samples are slower than the last control samples (`fastRevert`);
|
|
737
|
+
* - one run in `controlRuns`, or the first after `controlAfterSeconds`, uses the static choice and is
|
|
738
|
+
* recorded with role `control`, so the baseline the keep rule reads stays current;
|
|
739
|
+
* - a configuration with `comparisonSamples` usable samples that beats the static choice by the margin
|
|
740
|
+
* (`beats`) is adopted, one setting at a time: sharing, the checker count, then bounded posting at the
|
|
741
|
+
* count and sharing adopted before it;
|
|
742
|
+
* - where the signals leave a setting uncertain and the record has fewer than `comparisonSamples` of the
|
|
743
|
+
* alternative, one run in `exploreRuns` tries it, with role `explore`.
|
|
744
|
+
*
|
|
745
|
+
* A pinned setting is never chosen by any of this. A run that explored its parse threads (`role`
|
|
746
|
+
* `explore` before binding) or is a control run keeps its static checkers and sharing, so that one
|
|
747
|
+
* dimension is measured at a time.
|
|
748
|
+
*/
|
|
749
|
+
function learn(config: RunConfig, signals: CheckSignals, decision: CheckDecision, staticCheckers: number): void {
|
|
750
|
+
const record = config.record!;
|
|
751
|
+
const staticShare =
|
|
752
|
+
config.shareSource === Source.Pinned
|
|
753
|
+
? config.share
|
|
754
|
+
: sharingCandidate(signals) &&
|
|
755
|
+
decision.possible &&
|
|
756
|
+
decision.fit !== undefined &&
|
|
757
|
+
decision.fit.sharing >= staticCheckers &&
|
|
758
|
+
staticCheckers >= minSharingCheckers;
|
|
759
|
+
const changed = decision.shape !== undefined && shapeChanged(record.shape, decision.shape);
|
|
760
|
+
const samples = changed ? [] : usableSamples(record);
|
|
761
|
+
const cpus = Math.min(decision.cpus ?? availableCpus(), maxAutoCheckers);
|
|
762
|
+
const parseDefault = defaultParseThreads(signals.count);
|
|
763
|
+
const checkersPinned = config.checkersSource === Source.Pinned;
|
|
764
|
+
const sharePinned = config.shareSource === Source.Pinned;
|
|
765
|
+
const boundedPinned = config.boundedSource === Source.Pinned;
|
|
766
|
+
// The static choice is unbounded posting: a run with no record, or with `TSC_TS_AUTO=static`, never bounds it.
|
|
767
|
+
const staticBounded = boundedPinned ? config.bounded : false;
|
|
768
|
+
const parsePinned = config.parseSource === Source.Pinned;
|
|
769
|
+
const before = config.role;
|
|
770
|
+
const exploredBefore = before === 'explore';
|
|
771
|
+
const controlBefore = before === 'control';
|
|
772
|
+
|
|
773
|
+
// Written out field by field rather than spread (`{ ...record.chosen }`): a spread copies properties
|
|
774
|
+
// the compiler cannot name at its call sites, which scriptc's native build reads as a dynamic store
|
|
775
|
+
// and answers by storing every class's number fields as doubles (`scripts/check-int32-fields.mjs`).
|
|
776
|
+
let chosen: RecordChoice = changed
|
|
777
|
+
? {
|
|
778
|
+
checkers: staticCheckers,
|
|
779
|
+
share: staticShare,
|
|
780
|
+
bounded: staticBounded,
|
|
781
|
+
parseThreads: parseDefault,
|
|
782
|
+
source: 'default',
|
|
783
|
+
since: 0,
|
|
784
|
+
}
|
|
785
|
+
: {
|
|
786
|
+
checkers: record.chosen.checkers,
|
|
787
|
+
share: record.chosen.share,
|
|
788
|
+
bounded: record.chosen.bounded,
|
|
789
|
+
parseThreads: record.chosen.parseThreads,
|
|
790
|
+
source: record.chosen.source,
|
|
791
|
+
since: record.chosen.since,
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
let checkers = staticCheckers;
|
|
795
|
+
let share = staticShare;
|
|
796
|
+
let bounded = staticBounded;
|
|
797
|
+
let checkersLearned = false;
|
|
798
|
+
let shareLearned = false;
|
|
799
|
+
let boundedLearned = false;
|
|
800
|
+
let checkersGain: number | undefined = undefined;
|
|
801
|
+
let shareGain: number | undefined = undefined;
|
|
802
|
+
let boundedGain: number | undefined = undefined;
|
|
803
|
+
let parseChosen = parseDefault;
|
|
804
|
+
let adopted = false;
|
|
805
|
+
let role: 'chosen' | 'control' | 'explore' = before;
|
|
806
|
+
// The configuration `fastRevert` rejected, this run or an earlier one: its older samples can still
|
|
807
|
+
// dominate a median, and the last three runs are what the revert rule reads (`samplesFor`).
|
|
808
|
+
let rejection = record.rejected;
|
|
809
|
+
|
|
810
|
+
if (!exploredBefore && !controlBefore) {
|
|
811
|
+
// 1. An adopted configuration stays until the static choice beats it, and a bad one reverts at once.
|
|
812
|
+
const learned = chosen.source === 'learned';
|
|
813
|
+
const wantCheckers = checkersPinned ? staticCheckers : chosen.checkers;
|
|
814
|
+
const wantShare = sharePinned ? staticShare : chosen.share;
|
|
815
|
+
const wantBounded = boundedPinned ? staticBounded : chosen.bounded === true;
|
|
816
|
+
if (
|
|
817
|
+
learned &&
|
|
818
|
+
(wantCheckers !== staticCheckers || wantShare !== staticShare || wantBounded !== staticBounded) &&
|
|
819
|
+
fits(wantCheckers, wantShare, decision, cpus, samples, decision.memoryBytes)
|
|
820
|
+
) {
|
|
821
|
+
const own = checkTimes(samples, wantCheckers, wantShare, wantBounded);
|
|
822
|
+
const baseline = checkTimes(samples, staticCheckers, staticShare, staticBounded);
|
|
823
|
+
const staticBeats = beatsBy(baseline, own) !== undefined;
|
|
824
|
+
const fellBehind = !staticBeats && fastRevert(samples, wantCheckers, wantShare, wantBounded);
|
|
825
|
+
if (!staticBeats && !fellBehind) {
|
|
826
|
+
checkers = wantCheckers;
|
|
827
|
+
share = wantShare;
|
|
828
|
+
bounded = wantBounded;
|
|
829
|
+
checkersLearned = wantCheckers !== staticCheckers;
|
|
830
|
+
shareLearned = wantShare !== staticShare;
|
|
831
|
+
boundedLearned = wantBounded !== staticBounded;
|
|
832
|
+
const gain = beatsBy(own, baseline);
|
|
833
|
+
if (checkersLearned) checkersGain = gain;
|
|
834
|
+
if (shareLearned) shareGain = gain;
|
|
835
|
+
if (boundedLearned) boundedGain = gain;
|
|
836
|
+
}
|
|
837
|
+
if (fellBehind) rejection = { checkers: wantCheckers, share: wantShare, bounded: wantBounded, run: record.runs };
|
|
838
|
+
}
|
|
839
|
+
// 2. A control run re-measures the static choice, and changes no choice.
|
|
840
|
+
const learnedBefore =
|
|
841
|
+
learned &&
|
|
842
|
+
(chosen.checkers !== staticCheckers ||
|
|
843
|
+
chosen.share !== staticShare ||
|
|
844
|
+
(chosen.bounded === true) !== staticBounded ||
|
|
845
|
+
chosen.parseThreads !== parseDefault);
|
|
846
|
+
if (learnedBefore && (record.runs - record.lastControl >= controlRuns || controlIsStale(record, decision.now))) {
|
|
847
|
+
checkers = staticCheckers;
|
|
848
|
+
share = staticShare;
|
|
849
|
+
bounded = staticBounded;
|
|
850
|
+
checkersLearned = false;
|
|
851
|
+
shareLearned = false;
|
|
852
|
+
boundedLearned = false;
|
|
853
|
+
parseChosen = parseDefault;
|
|
854
|
+
role = 'control';
|
|
855
|
+
} else {
|
|
856
|
+
// 3. A configuration the record shows faster than the static one is adopted.
|
|
857
|
+
if (!sharePinned && !shareLearned) {
|
|
858
|
+
const alternative = !staticShare;
|
|
859
|
+
if (fits(checkers, alternative, decision, cpus, samples, decision.memoryBytes)) {
|
|
860
|
+
const own = samplesFor(samples, rejection, checkers, alternative, bounded);
|
|
861
|
+
const gain = beats(
|
|
862
|
+
samples,
|
|
863
|
+
own,
|
|
864
|
+
{ checkers, share: alternative, bounded },
|
|
865
|
+
{ checkers, share: staticShare, bounded },
|
|
866
|
+
);
|
|
867
|
+
if (gain !== undefined) {
|
|
868
|
+
share = alternative;
|
|
869
|
+
shareLearned = true;
|
|
870
|
+
shareGain = gain;
|
|
871
|
+
adopted = true;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (!checkersPinned && !checkersLearned) {
|
|
876
|
+
for (const candidate of countCandidates(staticCheckers, cpus)) {
|
|
877
|
+
if (candidate === checkers || !fits(candidate, share, decision, cpus, samples, decision.memoryBytes))
|
|
878
|
+
continue;
|
|
879
|
+
const own = samplesFor(samples, rejection, candidate, share, bounded);
|
|
880
|
+
const gain = beats(
|
|
881
|
+
samples,
|
|
882
|
+
own,
|
|
883
|
+
{ checkers: candidate, share, bounded },
|
|
884
|
+
{ checkers: staticCheckers, share: staticShare, bounded },
|
|
885
|
+
);
|
|
886
|
+
if (gain !== undefined) {
|
|
887
|
+
checkers = candidate;
|
|
888
|
+
checkersLearned = true;
|
|
889
|
+
checkersGain = gain;
|
|
890
|
+
adopted = true;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// 3b. Bounded posting is adopted where the project's runs measured it faster than Phase 1's posting at the
|
|
896
|
+
// count and sharing this run uses. It is the one setting no signal separates: T3 web gained 7% by it and T3
|
|
897
|
+
// server lost 8-9%, and both projects are library-bound at 16 checkers. A record with no bounded samples, or
|
|
898
|
+
// with samples that lose, leaves it off.
|
|
899
|
+
if (!boundedPinned && !boundedLearned && share && decision.possible) {
|
|
900
|
+
const own = samplesFor(samples, rejection, checkers, share, true);
|
|
901
|
+
const gain = beats(samples, own, { checkers, share, bounded: true }, { checkers, share, bounded: false });
|
|
902
|
+
if (gain !== undefined) {
|
|
903
|
+
bounded = true;
|
|
904
|
+
boundedLearned = true;
|
|
905
|
+
boundedGain = gain;
|
|
906
|
+
adopted = true;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// 4. Parse threads are chosen for the next run here; this run parsed with the choice its record had.
|
|
910
|
+
// The recorded count is kept only while the comparison that adopted it still shows it faster: a
|
|
911
|
+
// record can name a count without one (a first run pinned it before any record existed), and the
|
|
912
|
+
// samples of one run are no comparison, so that count is not learned here either.
|
|
913
|
+
if (!parsePinned) {
|
|
914
|
+
if (learned && chosen.parseThreads !== parseDefault && parseWins(samples, chosen.parseThreads, parseDefault)) {
|
|
915
|
+
parseChosen = chosen.parseThreads;
|
|
916
|
+
} else {
|
|
917
|
+
for (const candidate of parseCandidates(parseDefault, cpus)) {
|
|
918
|
+
if (candidate === parseChosen) continue;
|
|
919
|
+
const own = parseTimes(samples, candidate);
|
|
920
|
+
const gain = beatsBy(own, parseTimes(samples, parseDefault));
|
|
921
|
+
if (gain !== undefined) {
|
|
922
|
+
parseChosen = candidate;
|
|
923
|
+
adopted = true;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// 5. A setting the signals leave uncertain is tried in this run, one dimension at a time, unless
|
|
930
|
+
// a pin fixed it.
|
|
931
|
+
if (role === 'chosen' && !adopted && config.explore && !changed) {
|
|
932
|
+
const checkMs = medianOf(checkTimes(samples, checkers, share, bounded));
|
|
933
|
+
if (record.runs - record.lastExplore >= exploreRuns && checkMs !== undefined && checkMs >= exploreCheckMs) {
|
|
934
|
+
const shareAlt = !share;
|
|
935
|
+
// The dimensions are tried in order and the first with an alternative left takes the run: a band whose
|
|
936
|
+
// alternatives are all closed, such as a checker count whose candidates have measured, leaves the next
|
|
937
|
+
// one to be tried rather than ending exploration.
|
|
938
|
+
let exploring = false;
|
|
939
|
+
if (
|
|
940
|
+
!sharePinned &&
|
|
941
|
+
sharingBand(signals) &&
|
|
942
|
+
fits(checkers, shareAlt, decision, cpus, samples, decision.memoryBytes) &&
|
|
943
|
+
openCandidate(samples, checkers, shareAlt, bounded, checkers, share, bounded)
|
|
944
|
+
) {
|
|
945
|
+
share = shareAlt;
|
|
946
|
+
role = 'explore';
|
|
947
|
+
exploring = true;
|
|
948
|
+
}
|
|
949
|
+
if (!exploring && !checkersPinned && checkersBand(signals)) {
|
|
950
|
+
for (const candidate of countCandidates(staticCheckers, cpus)) {
|
|
951
|
+
if (candidate === checkers || !fits(candidate, share, decision, cpus, samples, decision.memoryBytes))
|
|
952
|
+
continue;
|
|
953
|
+
if (openCandidate(samples, candidate, share, bounded, checkers, share, bounded)) {
|
|
954
|
+
checkers = candidate;
|
|
955
|
+
role = 'explore';
|
|
956
|
+
exploring = true;
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (
|
|
962
|
+
!exploring &&
|
|
963
|
+
!boundedPinned &&
|
|
964
|
+
share &&
|
|
965
|
+
decision.possible &&
|
|
966
|
+
openCandidate(samples, checkers, share, true, checkers, share, false)
|
|
967
|
+
) {
|
|
968
|
+
// Bounded posting is the last dimension exploration tries: it changes no diagnostics and no other
|
|
969
|
+
// setting, and a run that tries it pays a few percent when it does not pay back.
|
|
970
|
+
bounded = true;
|
|
971
|
+
role = 'explore';
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Apply what the run uses, and what the record keeps as chosen.
|
|
979
|
+
config.checkers = checkers;
|
|
980
|
+
config.share = share;
|
|
981
|
+
config.bounded = bounded;
|
|
982
|
+
config.role = role;
|
|
983
|
+
config.checkersSource = checkersLearned ? Source.Learned : config.checkersSource;
|
|
984
|
+
if (checkersLearned)
|
|
985
|
+
config.checkersReasons.push(
|
|
986
|
+
learnedReason('check', checkersGain, checkTimes(samples, checkers, share, bounded).length),
|
|
987
|
+
);
|
|
988
|
+
if (shareLearned) {
|
|
989
|
+
config.shareSource = Source.Learned;
|
|
990
|
+
config.shareReasons.length = 0;
|
|
991
|
+
config.shareReasons.push(learnedReason('check', shareGain, checkTimes(samples, checkers, share, bounded).length));
|
|
992
|
+
} else if (!share && config.shareSource === Source.Static) {
|
|
993
|
+
config.shareSource = Source.Default;
|
|
994
|
+
config.shareReasons.length = 0;
|
|
995
|
+
}
|
|
996
|
+
if (boundedLearned) {
|
|
997
|
+
config.boundedSource = Source.Learned;
|
|
998
|
+
config.boundedReasons.length = 0;
|
|
999
|
+
config.boundedReasons.push(
|
|
1000
|
+
learnedReason('bounded', boundedGain, checkTimes(samples, checkers, share, bounded).length),
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (role === 'explore') {
|
|
1004
|
+
// The run explores the dimension it changed: the parse threads where the block above did not run.
|
|
1005
|
+
if (share !== staticShare) {
|
|
1006
|
+
config.shareSource = Source.Explore;
|
|
1007
|
+
config.shareReasons.length = 0;
|
|
1008
|
+
} else if (checkers !== staticCheckers) {
|
|
1009
|
+
config.checkersSource = Source.Explore;
|
|
1010
|
+
config.checkersReasons.length = 0;
|
|
1011
|
+
} else if (bounded !== staticBounded) {
|
|
1012
|
+
config.boundedSource = Source.Explore;
|
|
1013
|
+
config.boundedReasons.length = 0;
|
|
1014
|
+
}
|
|
1015
|
+
} else if (role === 'control') {
|
|
1016
|
+
// A control run uses the static checker count and sharing; the parse threads were fixed before the
|
|
1017
|
+
// program was bound, and a run that parsed with learned ones says so.
|
|
1018
|
+
config.checkersSource = Source.Control;
|
|
1019
|
+
config.shareSource = Source.Control;
|
|
1020
|
+
config.boundedSource = Source.Control;
|
|
1021
|
+
config.checkersReasons.length = 0;
|
|
1022
|
+
config.shareReasons.length = 0;
|
|
1023
|
+
config.boundedReasons.length = 0;
|
|
1024
|
+
}
|
|
1025
|
+
const chosenNow: RecordChoice = {
|
|
1026
|
+
checkers,
|
|
1027
|
+
share,
|
|
1028
|
+
bounded,
|
|
1029
|
+
parseThreads: parseChosen,
|
|
1030
|
+
source: checkersLearned || shareLearned || boundedLearned || parseChosen !== parseDefault ? 'learned' : 'default',
|
|
1031
|
+
since: 0,
|
|
1032
|
+
};
|
|
1033
|
+
config.chosen = role === 'chosen' ? chosenNow : chosen;
|
|
1034
|
+
config.rejected = rejection;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* The samples an adoption may read for a configuration: where the record rejected it, only the runs after
|
|
1039
|
+
* the rejection. The older samples are the ones `fastRevert` found it had fallen behind on, and a median
|
|
1040
|
+
* over them would adopt the configuration the record just gave up again (`RecordRejection`).
|
|
1041
|
+
*/
|
|
1042
|
+
function samplesFor(
|
|
1043
|
+
samples: readonly RecordSample[],
|
|
1044
|
+
rejection: RecordRejection | undefined,
|
|
1045
|
+
checkers: number,
|
|
1046
|
+
share: boolean,
|
|
1047
|
+
bounded: boolean,
|
|
1048
|
+
): readonly RecordSample[] {
|
|
1049
|
+
if (
|
|
1050
|
+
rejection === undefined ||
|
|
1051
|
+
rejection.checkers !== checkers ||
|
|
1052
|
+
rejection.share !== share ||
|
|
1053
|
+
(rejection.bounded === true) !== bounded
|
|
1054
|
+
)
|
|
1055
|
+
return samples;
|
|
1056
|
+
return samples.filter((sample) => sample.run > rejection.run);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Whether a configuration may be chosen or tried: the count automatic selection may use, and the memory it reaches. */
|
|
1060
|
+
function fits(
|
|
1061
|
+
checkers: number,
|
|
1062
|
+
share: boolean,
|
|
1063
|
+
decision: CheckDecision,
|
|
1064
|
+
cpus: number,
|
|
1065
|
+
samples: readonly RecordSample[],
|
|
1066
|
+
memoryBytes: number | undefined,
|
|
1067
|
+
): boolean {
|
|
1068
|
+
if (checkers < 1 || checkers > cpus) return false;
|
|
1069
|
+
if (share && !(decision.possible && decision.fit !== undefined && decision.fit.sharing >= checkers)) return false;
|
|
1070
|
+
return peakFits(samples, checkers, share, memoryBytes);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** Whether every sample of a configuration stayed under `memoryShare` of the memory limit (DESIGN 5.5). */
|
|
1074
|
+
function peakFits(
|
|
1075
|
+
samples: readonly RecordSample[],
|
|
1076
|
+
checkers: number,
|
|
1077
|
+
share: boolean,
|
|
1078
|
+
memoryBytes: number | undefined,
|
|
1079
|
+
): boolean {
|
|
1080
|
+
if (memoryBytes === undefined || memoryBytes <= 0) return true;
|
|
1081
|
+
const limit = (memoryBytes / 1048576) * memoryShare;
|
|
1082
|
+
for (const sample of samples)
|
|
1083
|
+
if (sample.checkers === checkers && sample.share === share && sample.peakMB > limit) return false;
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Whether this run is a control run: the record holds a learned choice, and one is due (DESIGN 4.2). */
|
|
1088
|
+
function controlDue(record: RunRecord | undefined, automaticity: Automaticity, now: number): boolean {
|
|
1089
|
+
if (!learns(automaticity) || record === undefined || record.chosen.source !== 'learned') return false;
|
|
1090
|
+
if (record.runs - record.lastControl >= controlRuns) return true;
|
|
1091
|
+
return controlIsStale(record, now);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Whether a control run is due: none yet, or the last one is older than `controlAfterSeconds` (DESIGN 4.2).
|
|
1096
|
+
* This reads every sample the record kept, not only the usable ones: when the run adopted a choice or last
|
|
1097
|
+
* measured it is a fact about the record, and a busy run is still a run. Filtering by load here would date the
|
|
1098
|
+
* adoption from nothing whenever that run's load was above the gate, and every run after it would be a control
|
|
1099
|
+
* run.
|
|
1100
|
+
*/
|
|
1101
|
+
function controlIsStale(record: RunRecord, now: number | undefined): boolean {
|
|
1102
|
+
if (now === undefined) return false;
|
|
1103
|
+
let last: number | undefined = undefined;
|
|
1104
|
+
for (const sample of record.samples)
|
|
1105
|
+
if (sample.role === 'control' && (last === undefined || sample.at > last)) last = sample.at;
|
|
1106
|
+
if (last === undefined) {
|
|
1107
|
+
for (const sample of record.samples)
|
|
1108
|
+
if (sample.run === record.chosen.since && (last === undefined || sample.at > last)) last = sample.at;
|
|
1109
|
+
}
|
|
1110
|
+
return last === undefined || now - last >= controlAfterSeconds;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Whether a configuration is still worth trying: it has fewer than `comparisonSamples` usable samples and
|
|
1115
|
+
* has not lost to `baseline` by the margin on two (DESIGN 4.4).
|
|
1116
|
+
*/
|
|
1117
|
+
function openCandidate(
|
|
1118
|
+
samples: readonly RecordSample[],
|
|
1119
|
+
checkers: number,
|
|
1120
|
+
share: boolean,
|
|
1121
|
+
bounded: boolean,
|
|
1122
|
+
baselineCheckers: number,
|
|
1123
|
+
baselineShare: boolean,
|
|
1124
|
+
baselineBounded: boolean,
|
|
1125
|
+
): boolean {
|
|
1126
|
+
const own = checkTimes(samples, checkers, share, bounded);
|
|
1127
|
+
if (own.length >= comparisonSamples) return false;
|
|
1128
|
+
return !loses(own, checkTimes(samples, baselineCheckers, baselineShare, baselineBounded));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** Whether an arm with at least two samples lost to `baseline` by the margin. */
|
|
1132
|
+
function loses(candidate: readonly number[], baseline: readonly number[]): boolean {
|
|
1133
|
+
if (candidate.length < 2 || baseline.length === 0) return false;
|
|
1134
|
+
const c = medianOf(candidate)!;
|
|
1135
|
+
const b = medianOf(baseline)!;
|
|
1136
|
+
if (c <= 0 || b <= 0) return false;
|
|
1137
|
+
return c > b * (1 + marginOf(candidate, baseline));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** The counts automatic selection may try instead of `count`: three quarters of it, and every CPU (DESIGN 4.4). */
|
|
1141
|
+
function countCandidates(count: number, cpus: number): number[] {
|
|
1142
|
+
const candidates = [Math.max(1, Math.ceil((count * 3) / 4)), cpus];
|
|
1143
|
+
const distinct: number[] = [];
|
|
1144
|
+
for (const candidate of candidates)
|
|
1145
|
+
if (candidate !== count && !distinct.includes(candidate)) distinct.push(candidate);
|
|
1146
|
+
return distinct;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** Whether `threads` parsed faster than `parseDefault` by the margin over `comparisonSamples` runs a side. */
|
|
1150
|
+
function parseWins(samples: readonly RecordSample[], threads: number, parseDefault: number): boolean {
|
|
1151
|
+
return beatsBy(parseTimes(samples, threads), parseTimes(samples, parseDefault)) !== undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** The parse-thread counts automatic selection may try: half the default (DESIGN 4.4). */
|
|
1155
|
+
function parseCandidates(parseDefault: number, cpus: number): number[] {
|
|
1156
|
+
const candidate = Math.max(1, Math.ceil(parseDefault / 2));
|
|
1157
|
+
if (candidate >= parseDefault || candidate > Math.max(1, cpus - 1)) return [];
|
|
1158
|
+
return [candidate];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Whether sharing is uncertain (DESIGN 4.4): at least `exploreSharingCheckers` checkers, `exploreSharingShare`
|
|
1163
|
+
* of the files, and exactly one of the static rule's two tests holds.
|
|
1164
|
+
*/
|
|
1165
|
+
export function sharingBand(signals: CheckSignals): boolean {
|
|
1166
|
+
return (
|
|
1167
|
+
signals.count >= exploreSharingCheckers &&
|
|
1168
|
+
signals.fileShare >= exploreSharingShare &&
|
|
1169
|
+
signals.fileShare >= sharingFileShare !== isLibraryBound(signals.library, signals.own)
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** Whether the checker count is uncertain (DESIGN 4.4): L / O between 2.5 and 10, and at least 3M library characters. */
|
|
1174
|
+
export function checkersBand(signals: CheckSignals): boolean {
|
|
1175
|
+
return bandOf(signals.library, signals.own);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/** `bandOf` on a record's shape, for the parse threads, which are chosen before the program is bound. */
|
|
1179
|
+
function bandOf(library: number, own: number): boolean {
|
|
1180
|
+
if (own <= 0 || library < exploreLibraryText) return false;
|
|
1181
|
+
const ratio = library / own;
|
|
1182
|
+
return ratio >= exploreLibraryRatio && ratio <= exploreLibraryRatioHigh;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** Whether the record's signals leave sharing uncertain, for the parse threads' choice before binding. */
|
|
1186
|
+
function recordSharingBand(record: RunRecord): boolean {
|
|
1187
|
+
const shape = record.shape;
|
|
1188
|
+
const fileShare = shape.fileShare ?? 0;
|
|
1189
|
+
return (
|
|
1190
|
+
record.chosen.checkers >= exploreSharingCheckers &&
|
|
1191
|
+
fileShare >= exploreSharingShare &&
|
|
1192
|
+
fileShare >= sharingFileShare !== isLibraryBound(shape.library, shape.own)
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** How a learned choice reads in the section: `check 21% faster over 5 runs`, `parse 4% faster over 3 runs`. */
|
|
1197
|
+
function learnedReason(dimension: 'check' | 'parse' | 'bounded', gain: number | undefined, runs: number): string {
|
|
1198
|
+
const count = runs === 1 ? '1 run' : `${runs} runs`;
|
|
1199
|
+
const label = dimension === 'parse' ? 'parse' : dimension === 'bounded' ? 'bounded posting' : 'check';
|
|
1200
|
+
return gain === undefined ? `${label} measured over ${count}` : `${label} ${percent(gain)}% faster over ${count}`;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** The parse threads the record adopted, and why, where this run may use them (`learnedParseThreads`). */
|
|
1204
|
+
function learnedParseThreads(
|
|
1205
|
+
record: RunRecord | undefined,
|
|
1206
|
+
parseDefault: number,
|
|
1207
|
+
automaticity: Automaticity,
|
|
1208
|
+
): { threads: number; reason: string } | undefined {
|
|
1209
|
+
if (!learns(automaticity) || record === undefined) return undefined;
|
|
1210
|
+
const threads = record.chosen.parseThreads;
|
|
1211
|
+
if (!Number.isInteger(threads) || threads < 1 || threads === parseDefault) return undefined;
|
|
1212
|
+
const samples = usableSamples(record);
|
|
1213
|
+
const gain = beatsBy(parseTimes(samples, threads), parseTimes(samples, parseDefault));
|
|
1214
|
+
// The record's chosen count can differ from the default without having won a comparison: a run that
|
|
1215
|
+
// pinned the parse threads before any record existed stores the pinned count, and the samples of that
|
|
1216
|
+
// one run are no comparison. Only the three-sample comparison makes a count learned.
|
|
1217
|
+
if (gain === undefined) return undefined;
|
|
1218
|
+
return { threads, reason: learnedReason('parse', gain, parseTimes(samples, threads).length) };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* The parse threads this run tries where the record leaves them open: half the default, once in
|
|
1223
|
+
* `exploreRuns` runs of at least `exploreCheckMs`, when the settings the bound program decides have no
|
|
1224
|
+
* candidate left to try (DESIGN 4.4: one dimension at a time, sharing and checkers first).
|
|
1225
|
+
*/
|
|
1226
|
+
function exploreParseThreads(
|
|
1227
|
+
record: RunRecord | undefined,
|
|
1228
|
+
parseDefault: number,
|
|
1229
|
+
explore: boolean,
|
|
1230
|
+
): number | undefined {
|
|
1231
|
+
if (!explore || record === undefined || record.chosen.source === 'learned') return undefined;
|
|
1232
|
+
if (record.runs - record.lastExplore < exploreRuns) return undefined;
|
|
1233
|
+
const samples = usableSamples(record);
|
|
1234
|
+
const checkMs = medianOf(
|
|
1235
|
+
checkTimes(samples, record.chosen.checkers, record.chosen.share, record.chosen.bounded === true),
|
|
1236
|
+
);
|
|
1237
|
+
if (checkMs === undefined || checkMs < exploreCheckMs) return undefined;
|
|
1238
|
+
if (openSetting(record, samples)) return undefined;
|
|
1239
|
+
for (const candidate of parseCandidates(parseDefault, availableCpus())) {
|
|
1240
|
+
const own = parseTimes(samples, candidate);
|
|
1241
|
+
if (own.length >= comparisonSamples || loses(own, parseTimes(samples, parseDefault))) continue;
|
|
1242
|
+
return candidate;
|
|
1243
|
+
}
|
|
1244
|
+
return undefined;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Whether the checker count or sharing still has a candidate exploration would try now, which the run's
|
|
1249
|
+
* binding decides: the parse threads wait for it to close (DESIGN 4.4).
|
|
1250
|
+
*/
|
|
1251
|
+
function openSetting(record: RunRecord, samples: readonly RecordSample[]): boolean {
|
|
1252
|
+
const chosen = record.chosen;
|
|
1253
|
+
const chosenBounded = chosen.bounded === true;
|
|
1254
|
+
const cpus = Math.min(availableCpus(), maxAutoCheckers);
|
|
1255
|
+
const open = (checkers: number, share: boolean) =>
|
|
1256
|
+
openCandidate(samples, checkers, share, chosenBounded, chosen.checkers, chosen.share, chosenBounded);
|
|
1257
|
+
if (recordSharingBand(record) && open(chosen.checkers, !chosen.share)) return true;
|
|
1258
|
+
if (!bandOf(record.shape.library, record.shape.own)) return false;
|
|
1259
|
+
for (const candidate of countCandidates(chosen.checkers, cpus)) if (open(candidate, chosen.share)) return true;
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Why sharing turned on, for the section: the share of the program that is declarations every checker
|
|
1265
|
+
* resolves again, and what the ranges reach, as `66% of files in node_modules; ranges reach 12.1M
|
|
1266
|
+
* library characters for 0.9M checked`.
|
|
1267
|
+
*/
|
|
1268
|
+
function sharingReason(signals: CheckSignals): string {
|
|
1269
|
+
const declarations = signals.declarationShare > signals.nodeModulesShare;
|
|
1270
|
+
const share = declarations ? signals.declarationShare : signals.nodeModulesShare;
|
|
1271
|
+
return (
|
|
1272
|
+
`${Math.round(share * 100)}% of files ` +
|
|
1273
|
+
(declarations ? 'are declarations' : 'in node_modules') +
|
|
1274
|
+
`; ranges reach ${millions(signals.library)}M library characters for ${millions(signals.own)}M checked`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** `chars` in millions, with one decimal: `12.1M`. */
|
|
1279
|
+
function millions(chars: number): string {
|
|
1280
|
+
return (chars / 1_000_000).toFixed(1);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** Where the section writes: the system of a compile, or anything else with `write`. */
|
|
1284
|
+
export interface RunConfigOutput {
|
|
1285
|
+
write(text: string): void;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** What the section's `Records:` line says (`run-records.ts`). */
|
|
1289
|
+
export interface RecordsReport {
|
|
1290
|
+
/** The record's path, as the section prints it (`~` for the home directory). */
|
|
1291
|
+
readonly path?: string;
|
|
1292
|
+
/** Why this run wrote no record. */
|
|
1293
|
+
readonly off?: string;
|
|
1294
|
+
/** Why the record could not be written, where writing was attempted. */
|
|
1295
|
+
readonly unavailable?: string;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* The `--extendedDiagnostics` section, printed after the statistics table: a blank line, `Suggestions:`,
|
|
1300
|
+
* and one line per setting with its value, where the value came from in parentheses, and how to pin it
|
|
1301
|
+
* where no pin chose it. Never printed for `--diagnostics` alone (`emitStatusAndReportStatistics`).
|
|
1302
|
+
*/
|
|
1303
|
+
export function reportRunConfig(out: RunConfigOutput, config: RunConfig, records?: RecordsReport): void {
|
|
1304
|
+
out.write(
|
|
1305
|
+
'\nSuggestions:\n' +
|
|
1306
|
+
suggestion(
|
|
1307
|
+
'Checkers',
|
|
1308
|
+
config.checkers,
|
|
1309
|
+
config.checkersSource,
|
|
1310
|
+
config.checkersPin,
|
|
1311
|
+
`--checkers ${config.checkers}`,
|
|
1312
|
+
config.checkersReasons,
|
|
1313
|
+
) +
|
|
1314
|
+
suggestion(
|
|
1315
|
+
'Sharing',
|
|
1316
|
+
config.share ? 'on' : 'off',
|
|
1317
|
+
config.shareSource,
|
|
1318
|
+
config.sharePin,
|
|
1319
|
+
`TSC_TS_SHARE=${config.share ? '1' : '0'}`,
|
|
1320
|
+
config.shareReasons,
|
|
1321
|
+
) +
|
|
1322
|
+
suggestion(
|
|
1323
|
+
'Bounded posting',
|
|
1324
|
+
config.bounded ? 'on' : 'off',
|
|
1325
|
+
config.boundedSource,
|
|
1326
|
+
config.boundedPin,
|
|
1327
|
+
`TSC_TS_SHARE_BOUNDED=${config.bounded ? '1' : '0'}`,
|
|
1328
|
+
config.boundedReasons,
|
|
1329
|
+
) +
|
|
1330
|
+
suggestion(
|
|
1331
|
+
'Parse threads',
|
|
1332
|
+
config.parseThreads,
|
|
1333
|
+
config.parseSource,
|
|
1334
|
+
config.parsePin,
|
|
1335
|
+
`TSC_TS_PARSE_THREADS=${config.parseThreads}`,
|
|
1336
|
+
config.parseReasons,
|
|
1337
|
+
) +
|
|
1338
|
+
recordsLine(records),
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** The `Records:` line: the record's path, why the run kept none, or why the write failed. */
|
|
1343
|
+
function recordsLine(records: RecordsReport | undefined): string {
|
|
1344
|
+
if (records === undefined) return '';
|
|
1345
|
+
if (records.path !== undefined) return ` Records: ${records.path}\n`;
|
|
1346
|
+
if (records.unavailable !== undefined) return ` Records: unavailable (${records.unavailable})\n`;
|
|
1347
|
+
return ` Records: off (${records.off ?? 'disabled'})\n`;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/** One line of the section: the setting, its value, its source, and the pin for it where none chose it. */
|
|
1351
|
+
function suggestion(
|
|
1352
|
+
setting: string,
|
|
1353
|
+
value: number | string,
|
|
1354
|
+
source: Source,
|
|
1355
|
+
pin: string | undefined,
|
|
1356
|
+
pinHint: string,
|
|
1357
|
+
reasons: readonly string[],
|
|
1358
|
+
): string {
|
|
1359
|
+
let line = ` ${setting}: ${value} (${sourceText(source, pin, reasons)})`;
|
|
1360
|
+
if (pin === undefined) line += `; pin with ${pinHint}`;
|
|
1361
|
+
return line + '\n';
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** A source as the section prints it: `pinned: --checkers`, `default`, `automatic: <reason>`, … */
|
|
1365
|
+
function sourceText(source: Source, pin: string | undefined, reasons: readonly string[]): string {
|
|
1366
|
+
const reason = reasons.join('; ');
|
|
1367
|
+
switch (source) {
|
|
1368
|
+
case Source.Pinned:
|
|
1369
|
+
return `pinned: ${pin ?? 'environment'}`;
|
|
1370
|
+
case Source.Default:
|
|
1371
|
+
return reason === '' ? 'default' : `automatic: ${reason}`;
|
|
1372
|
+
case Source.Static:
|
|
1373
|
+
return reason === '' ? 'automatic' : `automatic: ${reason}`;
|
|
1374
|
+
case Source.Learned:
|
|
1375
|
+
return reason === '' ? 'learned' : `learned: ${reason}`;
|
|
1376
|
+
case Source.Measured:
|
|
1377
|
+
return reason === '' ? 'measured' : `measured: ${reason}`;
|
|
1378
|
+
case Source.Explore:
|
|
1379
|
+
return 'exploring';
|
|
1380
|
+
case Source.Control:
|
|
1381
|
+
return 'control run';
|
|
1382
|
+
}
|
|
1383
|
+
}
|