tsc-ts 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +7 -1
  2. package/dist/api/compiler/ast.d.ts +15 -3
  3. package/dist/api/compiler/ast.js +36 -15
  4. package/dist/api/compiler/checker-classes.js +2 -1
  5. package/dist/api/compiler/checker-contextual.d.ts +20 -0
  6. package/dist/api/compiler/checker-contextual.js +111 -1
  7. package/dist/api/compiler/checker-expressions.js +14 -1
  8. package/dist/api/compiler/checker-grammar.d.ts +39 -4
  9. package/dist/api/compiler/checker-grammar.js +239 -75
  10. package/dist/api/compiler/checker-heritage.js +20 -14
  11. package/dist/api/compiler/checker-jsx.js +2 -1
  12. package/dist/api/compiler/checker-modules.js +4 -2
  13. package/dist/api/compiler/checker-source-file.js +1 -1
  14. package/dist/api/compiler/checker-type-factory.js +70 -19
  15. package/dist/api/compiler/checker-type-nodes.d.ts +1 -1
  16. package/dist/api/compiler/checker-type-nodes.js +5 -5
  17. package/dist/api/compiler/checker-unused.js +15 -10
  18. package/dist/api/compiler/checker.d.ts +7 -1
  19. package/dist/api/compiler/checker.js +14 -4
  20. package/dist/api/compiler/compile-program.d.ts +5 -0
  21. package/dist/api/compiler/compile-program.js +8 -0
  22. package/dist/api/compiler/expr-record.d.ts +11 -0
  23. package/dist/api/compiler/expr-record.js +53 -0
  24. package/dist/api/compiler/file-loader.d.ts +5 -0
  25. package/dist/api/compiler/file-loader.js +7 -0
  26. package/dist/api/compiler/identifier-name.d.ts +9 -0
  27. package/dist/api/compiler/identifier-name.js +10 -0
  28. package/dist/api/compiler/jsdoc.js +4 -2
  29. package/dist/api/compiler/parser.d.ts +2 -1
  30. package/dist/api/compiler/parser.js +10 -8
  31. package/dist/api/compiler/printer/factory.js +3 -2
  32. package/dist/api/compiler/printer/printer.js +3 -2
  33. package/dist/api/compiler/program-checker.d.ts +18 -0
  34. package/dist/api/compiler/program-checker.js +27 -0
  35. package/dist/api/compiler/reparser.js +4 -3
  36. package/dist/api/compiler/scanner.d.ts +8 -0
  37. package/dist/api/compiler/scanner.js +58 -42
  38. package/dist/api/compiler/shared-results.d.ts +376 -143
  39. package/dist/api/compiler/shared-results.js +1550 -773
  40. package/dist/api/compiler/transformers/declarations.js +2 -1
  41. package/package.json +15 -10
  42. package/src/compiler/ast.ts +39 -15
  43. package/src/compiler/cgroup.ts +157 -0
  44. package/src/compiler/checker-classes.ts +2 -1
  45. package/src/compiler/checker-contextual.ts +104 -0
  46. package/src/compiler/checker-expressions.ts +16 -1
  47. package/src/compiler/checker-grammar.ts +247 -88
  48. package/src/compiler/checker-heritage.ts +31 -17
  49. package/src/compiler/checker-jsx.ts +2 -1
  50. package/src/compiler/checker-modules.ts +4 -2
  51. package/src/compiler/checker-source-file.ts +1 -1
  52. package/src/compiler/checker-type-factory.ts +67 -21
  53. package/src/compiler/checker-type-nodes.ts +5 -5
  54. package/src/compiler/checker-unused.ts +13 -8
  55. package/src/compiler/checker.ts +15 -4
  56. package/src/compiler/compile-program.ts +8 -0
  57. package/src/compiler/decoder.ts +2 -1
  58. package/src/compiler/doctor-report.ts +133 -0
  59. package/src/compiler/expr-record.ts +59 -0
  60. package/src/compiler/file-loader.ts +8 -0
  61. package/src/compiler/identifier-name.ts +13 -0
  62. package/src/compiler/jsdoc.ts +4 -2
  63. package/src/compiler/parallel-check.ts +364 -146
  64. package/src/compiler/parallel-emit.ts +3 -3
  65. package/src/compiler/parse-thread.ts +24 -2
  66. package/src/compiler/parser.ts +10 -8
  67. package/src/compiler/printer/factory.ts +3 -2
  68. package/src/compiler/printer/printer.ts +4 -2
  69. package/src/compiler/program-checker.ts +28 -0
  70. package/src/compiler/reparser.ts +7 -3
  71. package/src/compiler/run-config.ts +1383 -0
  72. package/src/compiler/run-record-read.ts +87 -0
  73. package/src/compiler/run-records.ts +575 -0
  74. package/src/compiler/scanner.ts +57 -41
  75. package/src/compiler/shared-results.ts +1626 -896
  76. package/src/compiler/thread-entry.ts +3 -1
  77. package/src/compiler/transformers/declarations.ts +2 -1
  78. package/src/compiler/tsc-build.ts +64 -6
  79. package/src/compiler/tsc-cli.ts +33 -4
  80. package/src/compiler/tsc-compile.ts +223 -9
  81. package/src/compiler/tsc-watch.ts +25 -4
@@ -50,8 +50,9 @@ import type { Program } from './program.js';
50
50
  import type { SourceFile } from './ast.js';
51
51
  import { lastSymbolIdValue } from './symbol.js';
52
52
  import { shareMergedGlobals } from './checker-symbols.js';
53
+ import { availableCpus, cgroupMemoryLimit } from './cgroup.js';
53
54
  import { readFileSync } from 'node:fs';
54
- import { availableParallelism, totalmem } from 'node:os';
55
+ import { totalmem } from 'node:os';
55
56
  import {
56
57
  forbiddenWords,
57
58
  isForbidden,
@@ -59,10 +60,68 @@ import {
59
60
  numberLogSize,
60
61
  recordTableSize,
61
62
  resultTableSize,
62
- shareThreads,
63
+ shareStatColumns,
64
+ shareStatExport,
65
+ shareStatPosted,
66
+ shareStatSaved,
67
+ shareStatTranslation,
68
+ shareStatTranslated,
63
69
  stringLogSize,
70
+ usedStride,
64
71
  type ThreadSharingData,
65
72
  } from './shared-results.js';
73
+ import {
74
+ autoCheckers,
75
+ Automaticity,
76
+ CheckSignals,
77
+ checkersPolicy,
78
+ decideCheck,
79
+ learns,
80
+ libraryBoundCount,
81
+ maxAutoCheckers,
82
+ minSharingCheckers,
83
+ sharePin,
84
+ sharingCandidate,
85
+ sharingPeakShare,
86
+ Source,
87
+ tsgoCheckers,
88
+ type CheckDecision,
89
+ type CheckFit,
90
+ type RunConfig,
91
+ } from './run-config.js';
92
+ import type { RecordShape } from './run-records.js';
93
+
94
+ // The tests of the scheduler read these here, next to the plans they choose between.
95
+ export { autoCheckers, libraryBoundCount };
96
+
97
+ /**
98
+ * `TSC_TS_SHARE` for a run without a run configuration (the emit threads, which share no program to
99
+ * translate against): `1` shares, `0` does not, and unset shares nothing (`sharePin`).
100
+ */
101
+ const shareThreads = sharePin(process.env);
102
+
103
+ /**
104
+ * TSC_TS_SHARE_STOP_AT_MS: `t` or `t:level` stops a sharing run's sharing `t` milliseconds in, whatever
105
+ * the run's numbers say, to check the switch: `2` (the default) stops posting and translating, `1`
106
+ * stops posting. Testing only.
107
+ */
108
+ function shareStopHook(): { readonly at: number; readonly level: number } | undefined {
109
+ const value = process.env.TSC_TS_SHARE_STOP_AT_MS;
110
+ if (value === undefined || value === '') return undefined;
111
+ const colon = value.indexOf(':');
112
+ const at = Number(colon < 0 ? value : value.substring(0, colon));
113
+ if (!Number.isFinite(at) || at < 0) return undefined;
114
+ return { at, level: colon >= 0 && value.substring(colon + 1) === '1' ? 1 : 2 };
115
+ }
116
+
117
+ const shareStop = shareStopHook();
118
+
119
+ /**
120
+ * The window a run without a configuration uses before the main thread weighs what sharing saved against
121
+ * what it cost (`sharingNumbers`): 300 ms. A run with one uses the larger of 200 ms and a tenth of the
122
+ * check time its record measured (`RunConfig.shareWindowMs`, `shareWindowOf`).
123
+ */
124
+ const shareWindowMs = 300;
66
125
 
67
126
  /** With TSC_TS_CHECKERS_TRACE: the process's resident and peak resident set in MB (Linux), else 0. */
68
127
  export function residentMB(): number[] {
@@ -140,6 +199,11 @@ export class CheckerResult {
140
199
  readonly units: number[] = [],
141
200
  /** Those of `units` after which its checker had diagnostics in their file (`checkSourceFileRange`). */
142
201
  readonly reportingUnits: number[] = [],
202
+ /**
203
+ * The types, instantiations and transient symbols its checker created, which `--diagnostics` adds to
204
+ * the main thread's (`CheckedProgram.addCheckerCounts`); empty for a result without a checker of its own.
205
+ */
206
+ readonly counts: number[] = [],
143
207
  ) {}
144
208
  }
145
209
 
@@ -184,6 +248,7 @@ export type PackedResult = {
184
248
  readonly reserved: number[];
185
249
  readonly units: Int32Array;
186
250
  readonly reportingUnits: Int32Array;
251
+ readonly counts: number[];
187
252
  };
188
253
 
189
254
  class ResultPacker {
@@ -287,6 +352,7 @@ export function packResult(result: CheckerResult, program: Program): PackedResul
287
352
  reserved: result.reserved,
288
353
  units: Int32Array.from(result.units),
289
354
  reportingUnits: Int32Array.from(result.reportingUnits),
355
+ counts: result.counts,
290
356
  };
291
357
  }
292
358
 
@@ -416,6 +482,7 @@ export function unpackResult(
416
482
  emits,
417
483
  Array.from(packed.units),
418
484
  Array.from(packed.reportingUnits),
485
+ packed.counts,
419
486
  );
420
487
  }
421
488
 
@@ -454,23 +521,9 @@ function checkersEmit(checked: CheckedProgram, emits: boolean): boolean {
454
521
  return emits && parallelCheck.writer !== undefined && options.noEmit !== true && options.listFilesOnly !== true;
455
522
  }
456
523
 
457
- /**
458
- * How the number of checkers is chosen without `--checkers`: `auto` (the default) uses every CPU
459
- * this process may run on (`os.availableParallelism()`, at most `maxAutoCheckers`), and at least
460
- * tsgo's four; fewer than the CPUs, but never fewer than four, when the estimated peak memory of more
461
- * checkers would not fit (`memoryCheckers`). `fixed` (TSC_TS_CHECKERS=fixed) uses tsgo's four.
462
- */
463
- export const checkerPolicy: 'fixed' | 'auto' = process.env.TSC_TS_CHECKERS === 'fixed' ? 'fixed' : 'auto';
464
-
465
- /** tsgo's number of checkers, and the fewest `auto` starts. */
466
- const tsgoCheckers = 4;
467
-
468
- /** The most checkers `auto` starts (measured up to 16 CPUs). */
469
- const maxAutoCheckers = 16;
470
-
471
524
  /**
472
525
  * The number of checker threads for `checkers` (`--checkers`), or 1 to check on this thread. Without
473
- * the option, native builds follow `checkerPolicy` (with `program`, the memory estimate of `auto`
526
+ * the option, native builds follow the checker policy (with `program`, the memory estimate of `auto`
474
527
  * applies); under Node.js, where every thread builds its own copy of the program, tsc-ts checks on
475
528
  * one thread.
476
529
  */
@@ -487,8 +540,8 @@ export function checkerThreads(
487
540
  function defaultCheckers(program: Program | undefined): number {
488
541
  if (!sharesPublishedGraphs) return 1;
489
542
  if (program !== undefined && checksLittle(program)) return 1;
490
- if (checkerPolicy === 'fixed') return tsgoCheckers;
491
- const cpus = Math.min(availableParallelism(), maxAutoCheckers);
543
+ if (checkersPolicy(process.env) === 'fixed') return tsgoCheckers;
544
+ const cpus = Math.min(availableCpus(), maxAutoCheckers);
492
545
  return autoCheckers(cpus, program === undefined ? Number.MAX_SAFE_INTEGER : memoryCheckers(program));
493
546
  }
494
547
 
@@ -514,24 +567,6 @@ function checksLittle(program: Program): boolean {
514
567
  return true;
515
568
  }
516
569
 
517
- /** `auto` with `cpus` CPUs when the memory estimate fits `fit` checkers: tsgo's four at least. */
518
- export function autoCheckers(cpus: number, fit: number): number {
519
- return Math.max(tsgoCheckers, Math.min(cpus, fit));
520
- }
521
-
522
- /**
523
- * The number of threads that parse ahead of a program checked by `checkers` checkers: every CPU this
524
- * process may run on, as tsgo parses on every core whatever its checker count (parse threads hand
525
- * their files to the shared program; what else they allocate is reused by the checker threads), but
526
- * one. The main thread hands out the chunks they load: with a thread on every CPU, it waits for a CPU
527
- * each time a thread returns a chunk, and so does that thread for its next chunk (VS Code's parse took
528
- * 18-57 ms longer with 16 threads on 16 CPUs than with 15).
529
- */
530
- export function parseThreads(checkers: number): number {
531
- const cpus = availableParallelism();
532
- return Math.max(1, Math.min(cpus - 1, Math.max(checkers, Math.min(cpus, maxAutoCheckers))));
533
- }
534
-
535
570
  /**
536
571
  * The program's memory per character of source text, where it can't be measured (`residentBytes`):
537
572
  * 23-28 bytes on the benchmark apps when checking starts (parse threads' arenas included).
@@ -567,9 +602,14 @@ const lastResortShare = 0.95;
567
602
  * bytes, for a program of `programBytes` (`firstCheckerShare`, `checkerBytesPerDeclarationChar`);
568
603
  * at least 1.
569
604
  */
570
- export function checkersFittingMemory(programBytes: number, declarationChars: number, memory: number): number {
571
- const peak = programBytes * (1 + firstCheckerShare);
572
- const perChecker = Math.max(1, declarationChars * checkerBytesPerDeclarationChar);
605
+ export function checkersFittingMemory(
606
+ programBytes: number,
607
+ declarationChars: number,
608
+ memory: number,
609
+ peakShare: number = 1,
610
+ ): number {
611
+ const peak = programBytes * (1 + firstCheckerShare) * peakShare;
612
+ const perChecker = Math.max(1, declarationChars * checkerBytesPerDeclarationChar * peakShare);
573
613
  return Math.max(1, 1 + Math.floor((memoryShare * memory - peak) / perChecker));
574
614
  }
575
615
 
@@ -586,59 +626,6 @@ export function memoryLimit(): number {
586
626
  return limit;
587
627
  }
588
628
 
589
- /**
590
- * The smallest memory limit of this process's cgroup and its ancestors (Linux), or 0 where there is
591
- * none or it is unknown: `memory.max` and `memory.high` under cgroup v2, `memory.limit_in_bytes`
592
- * under v1. `process.constrainedMemory()` does not always see these: libuv reads only the process's
593
- * own cgroup, and scriptc's runtime only the root of the cgroup mount, which is the process's own
594
- * cgroup only inside a cgroup namespace.
595
- */
596
- export function cgroupMemoryLimit(): number {
597
- let membership: string;
598
- try {
599
- membership = readFileSync('/proc/self/cgroup', 'utf8');
600
- } catch {
601
- return 0;
602
- }
603
- let limit = 0;
604
- const lower = (value: number): void => {
605
- if (value > 0 && (limit === 0 || value < limit)) limit = value;
606
- };
607
- for (const line of membership.split('\n')) {
608
- // "0::/path" (v2), "N:memory:/path" or "N:cpu,memory:/path" (v1).
609
- const first = line.indexOf(':');
610
- const second = line.indexOf(':', first + 1);
611
- if (first < 0 || second < 0) continue;
612
- const controllers = line.substring(first + 1, second);
613
- const path = line.substring(second + 1);
614
- if (controllers === '') {
615
- for (let dir = path; ; dir = dir.substring(0, dir.lastIndexOf('/'))) {
616
- const base = '/sys/fs/cgroup' + (dir === '/' ? '' : dir);
617
- lower(cgroupLimitValue(base + '/memory.max'));
618
- lower(cgroupLimitValue(base + '/memory.high'));
619
- if (dir === '/' || dir === '' || dir.lastIndexOf('/') < 0) break;
620
- }
621
- } else if (controllers.split(',').indexOf('memory') >= 0) {
622
- lower(cgroupLimitValue('/sys/fs/cgroup/memory' + (path === '/' ? '' : path) + '/memory.limit_in_bytes'));
623
- }
624
- }
625
- return limit;
626
- }
627
-
628
- /** A cgroup memory limit file's value in bytes, or 0 for `max`, an unreadable file or no number. */
629
- function cgroupLimitValue(file: string): number {
630
- let text: string;
631
- try {
632
- text = readFileSync(file, 'utf8').trim();
633
- } catch {
634
- return 0;
635
- }
636
- if (text === '' || text === 'max') return 0;
637
- const value = Number(text);
638
- // cgroup v1 reports no limit as a page-aligned value near 2^63.
639
- return Number.isFinite(value) && value > 0 && value < 2 ** 62 ? value : 0;
640
- }
641
-
642
629
  /** The program whose text lengths `textLengths` holds. */
643
630
  let lengthsProgram: Program | undefined = undefined;
644
631
  let lengthsOfProgram: number[] = [];
@@ -664,6 +651,27 @@ export function textLengths(program: Program): number[] {
664
651
  * or an estimate from its text where that is unknown.
665
652
  */
666
653
  export function memoryCheckers(program: Program): number {
654
+ const estimate = memoryEstimate(program);
655
+ const fit = checkersFittingMemory(estimate.programBytes, estimate.declarationChars, estimate.memory);
656
+ if (process.env.TSC_TS_CHECKERS_TRACE !== undefined)
657
+ process.stderr.write(
658
+ `checkers: ${estimate.chars} source chars (${estimate.declarationChars} in declaration files), program ${Math.round(estimate.programBytes / 1048576)}MB${estimate.resident > 0 ? '' : ' (estimated)'}, memory ${Math.round(estimate.memory / 1048576)}MB fits ${fit} checkers\n`,
659
+ );
660
+ return fit;
661
+ }
662
+
663
+ /** What the memory fit reads of a program: its text, the declarations in it, and this process's memory. */
664
+ interface MemoryEstimate {
665
+ readonly chars: number;
666
+ readonly declarationChars: number;
667
+ readonly programBytes: number;
668
+ /** Whether `programBytes` is the resident set (`0` where it is an estimate from the text). */
669
+ readonly resident: number;
670
+ readonly memory: number;
671
+ }
672
+
673
+ /** The memory estimate of `program`, of its text (`textLengths`, computed once) and this process's memory. */
674
+ function memoryEstimate(program: Program): MemoryEstimate {
667
675
  let chars = 0;
668
676
  let declarationChars = 0;
669
677
  const files = program.getSourceFiles();
@@ -673,14 +681,24 @@ export function memoryCheckers(program: Program): number {
673
681
  if (files[i]!.isDeclarationFile) declarationChars += length[i]!;
674
682
  }
675
683
  const resident = residentBytes();
676
- const programBytes = resident > 0 ? resident : chars * programBytesPerChar;
677
- const memory = memoryLimit();
678
- const fit = checkersFittingMemory(programBytes, declarationChars, memory);
679
- if (process.env.TSC_TS_CHECKERS_TRACE !== undefined)
680
- process.stderr.write(
681
- `checkers: ${chars} source chars (${declarationChars} in declaration files), program ${Math.round(programBytes / 1048576)}MB${resident > 0 ? '' : ' (estimated)'}, memory ${Math.round(memory / 1048576)}MB fits ${fit} checkers\n`,
682
- );
683
- return fit;
684
+ return {
685
+ chars,
686
+ declarationChars,
687
+ programBytes: resident > 0 ? resident : chars * programBytesPerChar,
688
+ resident,
689
+ memory: memoryLimit(),
690
+ };
691
+ }
692
+
693
+ /**
694
+ * The checkers whose estimated peak memory fits `program` (`checkersFittingMemory`), without sharing and
695
+ * with the peak sharing adds (`sharingPeakShare`), which the sharing rule requires of the count it keeps.
696
+ */
697
+ export function memoryFit(program: Program): CheckFit {
698
+ const estimate = memoryEstimate(program);
699
+ return {
700
+ sharing: checkersFittingMemory(estimate.programBytes, estimate.declarationChars, estimate.memory, sharingPeakShare),
701
+ };
684
702
  }
685
703
 
686
704
  /**
@@ -1464,10 +1482,13 @@ export function planSchedule(
1464
1482
  }
1465
1483
  let all = 0;
1466
1484
  for (const i of order) all += cost[i]!;
1467
- // Files that cost more than a 32nd of a checker's share go first, costliest first: at the end of
1468
- // a range they would make its checker finish last.
1485
+ // Files that cost more than a 128th of the program go first, costliest first: at the end of a range
1486
+ // they would make its checker finish last. A fixed fraction of the program, rather than of a
1487
+ // checker's share, keeps the cut in proportion to how many checkers drain the shared queue: it is a
1488
+ // 32nd of a share at 4 checkers and an eighth at 16, where a range is shorter and a file of the
1489
+ // same size would otherwise sit in it.
1469
1490
  let heavyCount = count;
1470
- while (heavyCount < order.length && cost[order[heavyCount]!]! * count * 32 >= all) heavyCount++;
1491
+ while (heavyCount < order.length && cost[order[heavyCount]!]! * 128 >= all) heavyCount++;
1471
1492
  // The groups go to the shared queue as one entry each, among the costliest files, by total cost.
1472
1493
  const groups = checkGroups(checked, count, groupLoad, costs);
1473
1494
  const grouped = new Set<number>();
@@ -1650,9 +1671,13 @@ function threadSharingOf(
1650
1671
  order: readonly number[],
1651
1672
  units: readonly number[],
1652
1673
  files: number,
1674
+ bounded: boolean,
1653
1675
  ): ThreadSharingData[] {
1654
1676
  const takers = new SharedArrayBuffer(4 * files);
1655
1677
  const forbidden = new SharedArrayBuffer(4 * files * forbiddenWords(count));
1678
+ // The checkers that missed on a key in each file, which a post requires (`ThreadSharing.demanded`). An unbounded
1679
+ // run posts as Phase 1 does and never reads it, so it gets a word rather than a bit per checker per file.
1680
+ const wanted = bounded ? new SharedArrayBuffer(4 * files * forbiddenWords(count)) : new SharedArrayBuffer(4);
1656
1681
  const handoff = new SharedArrayBuffer(4 * (3 + order.length));
1657
1682
  new Int32Array(handoff)[2] = count;
1658
1683
  const taken = new Int32Array(takers);
@@ -1670,17 +1695,23 @@ function threadSharingOf(
1670
1695
  const stringLogs: SharedArrayBuffer[] = [];
1671
1696
  for (let i = 0; i < count; i++) {
1672
1697
  numberLogs.push(new SharedArrayBuffer(4 * numberLogSize));
1673
- recordTables.push(new SharedArrayBuffer(8 * recordTableSize));
1698
+ recordTables.push(new SharedArrayBuffer(4 * recordTableSize));
1674
1699
  stringLogs.push(new SharedArrayBuffer(2 * stringLogSize));
1675
1700
  }
1676
- const used = new SharedArrayBuffer(8 * count);
1701
+ const used = new SharedArrayBuffer(4 * usedStride * count);
1702
+ // The stop switch (0 runs, 1 stops posting, 2 stops posting and translating) and the counters the
1703
+ // main thread reads at its window, a row per checker.
1704
+ const control = new SharedArrayBuffer(4);
1705
+ const stats = new SharedArrayBuffer(8 * shareStatColumns * count);
1677
1706
  const data: ThreadSharingData[] = [];
1678
1707
  for (let i = 0; i < count; i++)
1679
1708
  data.push({
1680
1709
  index: i,
1681
1710
  count,
1711
+ bounded,
1682
1712
  takers,
1683
1713
  forbidden,
1714
+ wanted,
1684
1715
  handoff,
1685
1716
  typeTable,
1686
1717
  returnTable,
@@ -1690,6 +1721,8 @@ function threadSharingOf(
1690
1721
  recordTables,
1691
1722
  stringLogs,
1692
1723
  used,
1724
+ control,
1725
+ stats,
1693
1726
  });
1694
1727
  return data;
1695
1728
  }
@@ -1903,6 +1936,7 @@ export function checkFromQueue(
1903
1936
  emits,
1904
1937
  unitsDone,
1905
1938
  reportingUnits,
1939
+ onMainThread ? [] : [checked.checker.typeCount, checked.checker.totalInstantiations, checked.checker.symbolCount],
1906
1940
  );
1907
1941
  }
1908
1942
 
@@ -1920,50 +1954,179 @@ export function mainThreadChecks(shared: boolean): boolean {
1920
1954
  }
1921
1955
 
1922
1956
  /**
1923
- * When a checker's own files are a small part of what it resolves (`libraryBoundCheckers`): the least
1924
- * text, in characters, of the libraries the files of a checker's range reach, on average over the
1925
- * ranges, and the least multiple of the text of the range's own files it is.
1957
+ * Whether the run reads the memory fit of sharing (`checkDecisionOf`): the static rule reads it where
1958
+ * the signals point at sharing, and a run the learning rules read may adopt sharing the static rule
1959
+ * leaves off, or explore it, when its samples show the configuration faster (DESIGN 4.3-4.4).
1960
+ *
1961
+ * A pin on sharing chooses sharing without a rule, but where it pins sharing on the learning rules still
1962
+ * compare checker counts with it, and every count they may adopt reads how many fit memory with sharing's
1963
+ * peak: a pinned count that no rule can change, and `TSC_TS_AUTO=0`, read nothing.
1926
1964
  */
1927
- const libraryBoundText = 6_000_000;
1928
- const libraryBoundShare = 5;
1965
+ export function sharingFitNeeded(config: RunConfig, signals: CheckSignals): boolean {
1966
+ if (config.automaticity === Automaticity.Off) return false;
1967
+ if (config.shareSource === Source.Pinned) {
1968
+ return (
1969
+ config.share &&
1970
+ config.checkersSource !== Source.Pinned &&
1971
+ learns(config.automaticity) &&
1972
+ config.record !== undefined
1973
+ );
1974
+ }
1975
+ if (sharingCandidate(signals)) return true;
1976
+ return learns(config.automaticity) && config.record !== undefined;
1977
+ }
1929
1978
 
1930
1979
  /**
1931
- * The number of checkers for `count` checkers whose ranges reach `library` characters of libraries
1932
- * and check `own` characters each, on average: three quarters of them (tsgo's four at least) where
1933
- * every checker spends most of its time resolving the same large libraries (`libraryBoundText`,
1934
- * `libraryBoundShare`), else `count`.
1980
+ * What `decideCheck` needs of the run besides its signals: whether the checker threads of a shared
1981
+ * program may share results (`startParallelCheck` makes the same test), and, where sharing is possible
1982
+ * and a rule that may choose it reads the estimate, how many checkers fit memory with sharing's peak
1983
+ * (`sharingFitNeeded`). The estimate costs a pass over the program's files and its memory limit, so it
1984
+ * is computed only where a rule reads it: a run without a record pays only where the static rule's
1985
+ * signals point at sharing, and one checker, a program that is not shared, a pinned run or
1986
+ * `TSC_TS_AUTO=0` pay nothing.
1987
+ */
1988
+ function checkDecisionOf(config: RunConfig, signals: CheckSignals, program: Program, shared: boolean): CheckDecision {
1989
+ // `mainThreadChecks` is false whenever the program is shared (`!shared`), as sharing needs.
1990
+ const possible = shared && signals.count > 1 && signals.count <= maxSharingCheckers;
1991
+ const fit = possible && sharingFitNeeded(config, signals) ? memoryFit(program) : undefined;
1992
+ // A run whose record the learning rules read also names the shape its samples describe, the CPUs a
1993
+ // count may use, the memory a configuration may not exceed and the time a control sample ages from.
1994
+ const learning = config.records || config.record !== undefined;
1995
+ return {
1996
+ possible,
1997
+ fit,
1998
+ shape: learning ? recordShapeOf(program, signals) : undefined,
1999
+ cpus: Math.min(availableCpus(), maxAutoCheckers),
2000
+ memoryBytes: memoryLimit(),
2001
+ now: Math.floor(Date.now() / 1000),
2002
+ };
2003
+ }
2004
+
2005
+ /** The program's shape for its record (`RecordShape`), with the shares the sharing rule reads as `F`. */
2006
+ function recordShapeOf(program: Program, signals: CheckSignals): RecordShape {
2007
+ const files = program.getSourceFiles();
2008
+ let chars = 0;
2009
+ let declarationChars = 0;
2010
+ for (const file of files) {
2011
+ chars += file.end;
2012
+ if (file.isDeclarationFile) declarationChars += file.end;
2013
+ }
2014
+ return {
2015
+ files: files.length,
2016
+ chars,
2017
+ declarationChars,
2018
+ checkChars: chars - declarationChars,
2019
+ library: Math.round(signals.library),
2020
+ own: Math.round(signals.own),
2021
+ redundancy: signals.redundancy,
2022
+ fileShare: Math.max(signals.declarationShare, signals.nodeModulesShare),
2023
+ };
2024
+ }
2025
+
2026
+ /** What the counters of `count` checkers say at the sharing window: milliseconds saved and spent. */
2027
+ interface SharingNumbers {
2028
+ /** What taking posted results saved, over the checkers: the computations they did not repeat. */
2029
+ readonly saved: number;
2030
+ /**
2031
+ * What posting cost: exporting and translating results. Translating a result reads its records in
2032
+ * place inside `ThreadSharing.translatePosted`'s timed translation, so `translationMs` holds that
2033
+ * reading too; counting a separate unpacking time would charge the window for it twice.
2034
+ */
2035
+ readonly spent: number;
2036
+ readonly posted: number;
2037
+ readonly translated: number;
2038
+ }
2039
+
2040
+ /**
2041
+ * The counters of `count` checkers' rows of `stats` (`ThreadSharing.writeStats`), summed. The tests read
2042
+ * this, next to the window it feeds.
2043
+ */
2044
+ export function sharingNumbers(stats: Float64Array, count: number): SharingNumbers {
2045
+ let saved = 0;
2046
+ let spent = 0;
2047
+ let posted = 0;
2048
+ let translated = 0;
2049
+ for (let i = 0; i < count; i++) {
2050
+ const row = i * shareStatColumns;
2051
+ saved += stats[row + shareStatSaved]!;
2052
+ spent += stats[row + shareStatExport]! + stats[row + shareStatTranslation]!;
2053
+ posted += stats[row + shareStatPosted]!;
2054
+ translated += stats[row + shareStatTranslated]!;
2055
+ }
2056
+ return { saved, spent, posted, translated };
2057
+ }
2058
+
2059
+ /**
2060
+ * The control a sharing run's numbers ask for: 2 when it saved less than 1.5 times what it spent, 1
2061
+ * when the posts go untaken (under 5% of them, and at least one), else 0. Stopping takes a minute at
2062
+ * worst, when the window's numbers are still too early to tell.
2063
+ */
2064
+ export function stopLevel(numbers: SharingNumbers): number {
2065
+ if (numbers.saved < 1.5 * numbers.spent) return 2;
2066
+ if (numbers.posted > 0 && numbers.translated / numbers.posted < 0.05) return 1;
2067
+ return 0;
2068
+ }
2069
+
2070
+ /**
2071
+ * Whether the rules of a run of `config` at `count` checkers read the check signals (`checkSignals`).
2072
+ * Two rules read them:
1935
2073
  *
1936
- * Each checker resolves the declarations its files reach, whatever else it checks. Where they are
1937
- * millions of characters, such as an Effect-based project's, and much more than the checker's own
1938
- * files, a checker costs seconds of CPU and hundreds of MB before its files get cheap, and the last
1939
- * quarter of the checkers (on 16 CPUs of 8 cores, the ones sharing a core with another) repeat that
1940
- * work rather than save time: T3 Code's web and server projects ran 3% and 1% faster with 12
1941
- * checkers than with 16, and used 16% and 8% less memory. Where checkers resolve less, or mostly
1942
- * check their own files, 12 of 16 checkers were slower: Excalidraw (1.4M library and 0.15M own
1943
- * characters per range) by 6%, Sentry (5.7M and 3.0M) and the tsc-ts sources by about 11%.
2074
+ * - the library-bound cut (`decideCheck`), wherever the count is not pinned and is above tsgo's four.
2075
+ * This is the decision a run made before the automatic selection, and `TSC_TS_AUTO=0` keeps it: with
2076
+ * selection off the count is not cut for sharing either, but dropping the signals would leave the
2077
+ * cut nothing to read and start checkers the cut would not;
2078
+ * - the sharing rule, at `minSharingCheckers` or more, where neither the count nor sharing is pinned
2079
+ * (the cut may leave the count sharing was measured at, so the signals are read before it).
2080
+ *
2081
+ * A pinned count and a pinned sharing read neither, and pay nothing for the pass over the ranges.
1944
2082
  */
1945
- export function libraryBoundCount(count: number, library: number, own: number): number {
1946
- return library >= libraryBoundText && library >= libraryBoundShare * own
1947
- ? Math.max(tsgoCheckers, Math.ceil((count * 3) / 4))
1948
- : count;
2083
+ export function signalsNeeded(config: RunConfig, count: number): boolean {
2084
+ const cut = config.checkersSource === Source.Default && count > tsgoCheckers;
2085
+ if (config.automaticity === Automaticity.Off) return cut;
2086
+ return cut || (config.shareSource === Source.Default && count >= minSharingCheckers);
1949
2087
  }
1950
2088
 
1951
2089
  /**
1952
- * The number of checkers for `plan`, made with `count` checkers that `auto` chose
1953
- * (`libraryBoundCount` of what its ranges reach and check).
2090
+ * What `plan`, made with `count` checkers, shows about the cost of checking (`CheckSignals`): the
2091
+ * libraries a range reaches and the files it checks, the share of that a second range reaches too, and
2092
+ * the shares of declaration and node_modules files of the program. Computed where a rule may read them
2093
+ * (`signalsNeeded`) and left at zero elsewhere: a pinned count needs none of them, and the pass over
2094
+ * every range's libraries costs a millisecond on a large program.
1954
2095
  */
1955
- function libraryBoundCheckers(checked: CheckedProgram, plan: CheckPlan, costs: CheckCosts, count: number): number {
1956
- if (checkerPolicy !== 'auto' || checked.program.options.checkers !== undefined || count <= tsgoCheckers) return count;
2096
+ function checkSignals(
2097
+ checked: CheckedProgram,
2098
+ plan: CheckPlan,
2099
+ costs: CheckCosts,
2100
+ count: number,
2101
+ config: RunConfig | undefined = undefined,
2102
+ ): CheckSignals {
2103
+ const ranges = plan.ranges.length >> 1;
2104
+ const signals = new CheckSignals(count, ranges);
1957
2105
  const program = checked.program;
2106
+ // What a rule would read, what the run's record writes (`config.records`), or what a doctor child
2107
+ // reports (`config.measure`), is computed; a pinned count with records off and no report pays nothing.
2108
+ const deciding =
2109
+ config === undefined
2110
+ ? checkersPolicy(process.env) === 'auto' && program.options.checkers === undefined && count > tsgoCheckers
2111
+ : signalsNeeded(config, count);
2112
+ const wanted = deciding || (config !== undefined && (config.records || config.measure));
2113
+ if (!wanted || ranges === 0) return signals;
1958
2114
  const files = program.getSourceFiles();
1959
2115
  const length = textLengths(program);
1960
2116
  const isChecked: boolean[] = [];
1961
- for (let i = 0; i < files.length; i++) isChecked.push(false);
2117
+ let declarationFiles = 0;
2118
+ let nodeModulesFiles = 0;
2119
+ for (let i = 0; i < files.length; i++) {
2120
+ isChecked.push(false);
2121
+ if (files[i]!.isDeclarationFile) declarationFiles++;
2122
+ if (files[i]!.path.indexOf('/node_modules/') >= 0) nodeModulesFiles++;
2123
+ }
1962
2124
  for (const i of costs.order) isChecked[i] = true;
1963
- const ranges = plan.ranges.length >> 1;
1964
- if (ranges === 0) return count;
1965
2125
  let library = 0;
1966
2126
  let own = 0;
2127
+ // The libraries every range reaches together, each counted once (`CheckSignals.redundancy`).
2128
+ const union = new Set<number>();
2129
+ let unionChars = 0;
1967
2130
  for (let r = 0; r < ranges; r++) {
1968
2131
  // The libraries the range's files import, and what they reach through other libraries (as checkCosts counts them).
1969
2132
  const reached = new Set<number>();
@@ -1980,6 +2143,10 @@ function libraryBoundCheckers(checked: CheckedProgram, plan: CheckPlan, costs: C
1980
2143
  while (stack.length > 0) {
1981
2144
  const k = stack.pop()!;
1982
2145
  library += length[k]!;
2146
+ if (!union.has(k)) {
2147
+ union.add(k);
2148
+ unionChars += length[k]!;
2149
+ }
1983
2150
  for (const t of costs.imports[k] ?? [])
1984
2151
  if (
1985
2152
  !reached.has(t) &&
@@ -1992,18 +2159,22 @@ function libraryBoundCheckers(checked: CheckedProgram, plan: CheckPlan, costs: C
1992
2159
  }
1993
2160
  }
1994
2161
  }
1995
- const fewer = libraryBoundCount(count, library / ranges, own / ranges);
2162
+ signals.library = library / ranges;
2163
+ signals.own = own / ranges;
2164
+ signals.redundancy = (library - unionChars) / (library + own);
2165
+ signals.declarationShare = declarationFiles / files.length;
2166
+ signals.nodeModulesShare = nodeModulesFiles / files.length;
2167
+ if (config !== undefined) config.signals = signals;
1996
2168
  if (process.env.TSC_TS_CHECKERS_TRACE !== undefined)
1997
2169
  process.stderr.write(
1998
- `checkers: ranges reach ${Math.round(library / ranges / 1000)}K library chars and check ${Math.round(own / ranges / 1000)}K on average: ${fewer} checkers\n`,
2170
+ `checkers: ranges reach ${Math.round(signals.library / 1000)}K library chars and check ${Math.round(signals.own / 1000)}K on average, ${Math.round(signals.redundancy * 100)}% of it reached twice: ${libraryBoundCount(count, signals.library, signals.own)} checkers\n`,
1999
2171
  );
2000
- return fewer;
2172
+ return signals;
2001
2173
  }
2002
2174
 
2003
2175
  /**
2004
- * Checks `checked` with `checkers` checkers (fewer where `auto` chose them and they would mostly
2005
- * resolve the same libraries: `libraryBoundCheckers`) and calls `done` with the wall time once
2006
- * `checked` holds every file's diagnostics.
2176
+ * Checks `checked` with `checkers` checkers (fewer where the configuration chooses so: `decideCheck`,
2177
+ * run-config.ts) and calls `done` with the wall time once `checked` holds every file's diagnostics.
2007
2178
  */
2008
2179
  /**
2009
2180
  * Shares the program of `checked` with checker threads (scriptc builds): this thread's checker merges the
@@ -2037,7 +2208,8 @@ export function shareWithCheckers(checked: CheckedProgram): void {
2037
2208
  * `emitsTo`, the emit results go only there, by file index, and nothing else the threads find reaches
2038
2209
  * the program: a declaration thread (parallel-emit.ts), while this thread's checker checks. With
2039
2210
  * `declarationsTo`, the same holds for the declaration errors of a `noEmit` program
2040
- * (`checkersReportDeclarations`).
2211
+ * (`checkersReportDeclarations`). `config` is the run's configuration: the count it chose is decided
2212
+ * here, once the program is bound and the plan made, and sharing is what it holds.
2041
2213
  */
2042
2214
  export function startParallelCheck(
2043
2215
  checked: CheckedProgram,
@@ -2048,6 +2220,7 @@ export function startParallelCheck(
2048
2220
  checking: boolean = true,
2049
2221
  emitsTo: (EmitResult | undefined)[] | undefined = undefined,
2050
2222
  declarationsTo: (Diagnostic[] | undefined)[] | undefined = undefined,
2223
+ config: RunConfig | undefined = undefined,
2051
2224
  ): void {
2052
2225
  let count = checkers;
2053
2226
  const start = performance.now();
@@ -2067,7 +2240,12 @@ export function startParallelCheck(
2067
2240
  const orderStart = performance.now();
2068
2241
  const costs = costOrder(checked);
2069
2242
  let plan = planSchedule(checked, count, costs, checking);
2070
- const fewer = libraryBoundCheckers(checked, plan, costs, count);
2243
+ const signals = checkSignals(checked, plan, costs, count, config);
2244
+ if (config !== undefined) config.signals = signals;
2245
+ const fewer =
2246
+ config === undefined
2247
+ ? libraryBoundCount(signals.count, signals.library, signals.own)
2248
+ : decideCheck(config, signals, checkDecisionOf(config, signals, program, shared));
2071
2249
  if (fewer < count) {
2072
2250
  count = fewer;
2073
2251
  plan = planSchedule(checked, count, costs, checking);
@@ -2109,7 +2287,13 @@ export function startParallelCheck(
2109
2287
  const firstThread = mainChecks ? 1 : 0;
2110
2288
  let remaining = count - firstThread;
2111
2289
  let ownDone = false;
2290
+ /** The sharing window's timer, where the threads share results; cleared when they are done. */
2291
+ let shareStopTimer: ReturnType<typeof setTimeout> | undefined = undefined;
2112
2292
  const finish = (): void => {
2293
+ if (shareStopTimer !== undefined) {
2294
+ clearTimeout(shareStopTimer);
2295
+ shareStopTimer = undefined;
2296
+ }
2113
2297
  if (emitsTo !== undefined || declarationsTo !== undefined) {
2114
2298
  mark('emitted on checker threads');
2115
2299
  done(performance.now() - start);
@@ -2151,6 +2335,7 @@ export function startParallelCheck(
2151
2335
  done(performance.now() - start);
2152
2336
  };
2153
2337
  const record = (result: CheckerResult): void => {
2338
+ checked.addCheckerCounts(result.counts);
2154
2339
  if (declarationsTo !== undefined) {
2155
2340
  for (let k = 0; k < result.declarations.length; k++) declarationsTo[result.files[k]!] = result.declarations[k]!;
2156
2341
  if (trace)
@@ -2188,11 +2373,43 @@ export function startParallelCheck(
2188
2373
  if (remaining === 0 && ownDone) finish();
2189
2374
  };
2190
2375
  const diagnosticFiles = new Map<string, DiagnosticFile>();
2191
- // TSC_TS_SHARE: checker threads share results (shared-results.ts).
2376
+ // Whether checker threads share results (shared-results.ts), which the run's configuration decides;
2377
+ // the emit threads of parallel-emit.ts have no configuration and never share (count 1).
2378
+ const sharesResults = config === undefined ? shareThreads === true : config.share;
2192
2379
  const sharing =
2193
- shareThreads && shared && !mainChecks && count > 1 && count <= maxSharingCheckers
2194
- ? threadSharingOf(count, order, units, files.length)
2380
+ sharesResults && shared && !mainChecks && count > 1 && count <= maxSharingCheckers
2381
+ ? threadSharingOf(count, order, units, files.length, config !== undefined && config.bounded)
2195
2382
  : undefined;
2383
+ if (sharing !== undefined) {
2384
+ // The main thread is idle while the checkers run, so a timer reads their counters at the window and
2385
+ // stops what does not pay (control 1 or 2); every checker computes what it does not take, so the
2386
+ // diagnostics stay the same.
2387
+ const control = new Int32Array(sharing[0]!.control);
2388
+ const stats = new Float64Array(sharing[0]!.stats);
2389
+ const stop = shareStop;
2390
+ const at = stop !== undefined ? stop.at : config !== undefined ? config.shareWindowMs : shareWindowMs;
2391
+ shareStopTimer = setTimeout(() => {
2392
+ const numbers = sharingNumbers(stats, count);
2393
+ if (stop !== undefined) Atomics.store(control, 0, stop.level);
2394
+ else {
2395
+ const level = stopLevel(numbers);
2396
+ if (level === 0) return;
2397
+ Atomics.store(control, 0, level);
2398
+ }
2399
+ // The section says what the window saw, instead of the rule that turned sharing on.
2400
+ if (config !== undefined) {
2401
+ config.shareSource = Source.Measured;
2402
+ config.shareReasons.length = 0;
2403
+ config.shareReasons.push(
2404
+ `stopped after ${at} ms: ${Math.round(numbers.saved)} ms saved for ${Math.round(numbers.spent)} ms spent`,
2405
+ );
2406
+ }
2407
+ if (trace)
2408
+ process.stderr.write(
2409
+ `checkers: stopped sharing after ${at} ms: ${Math.round(numbers.saved)} ms saved for ${Math.round(numbers.spent)} ms spent, ${numbers.translated}/${numbers.posted} posted results taken\n`,
2410
+ );
2411
+ }, at);
2412
+ }
2196
2413
  const sharingStarted: boolean[] = [];
2197
2414
  for (let i = 0; i < count; i++) sharingStarted.push(false);
2198
2415
  const startSharingOf = (i: number): ThreadSharingData => {
@@ -2232,7 +2449,7 @@ export function startParallelCheck(
2232
2449
  });
2233
2450
  worker.on('error', (error: unknown) => {
2234
2451
  // Sharing results (TSC_TS_SHARE) is still checked for such failures, which the main thread hides otherwise.
2235
- if (trace || shareThreads) process.stderr.write(`checkers: thread ${i} failed: ${String(error)}\n`);
2452
+ if (trace || sharesResults) process.stderr.write(`checkers: thread ${i} failed: ${String(error)}\n`);
2236
2453
  threadDone();
2237
2454
  });
2238
2455
  };
@@ -2362,6 +2579,7 @@ class Revival {
2362
2579
  copy.emits.map((emit) => this.emit(emit)),
2363
2580
  copy.units,
2364
2581
  copy.reportingUnits,
2582
+ copy.counts,
2365
2583
  );
2366
2584
  }
2367
2585