Skip to content

Commit 2a565a2

Browse files
committed
Choose the checker count from CPUs and program size
The checker count was fixed at 4 because which checker checked a file could change the results, so changing it changed the output. That is no longer the case, and with checkers taking files from each other it only trades time for memory, so the default can follow the machine. Checking is fastest with about one checker per CPU, until the work every checker duplicates (library and shared types) outweighs what another checker adds. Measured on 4 and 8 CPUs, one checker per logical CPU was fastest for nearly every project, hyperthreads included; on 20 CPUs, 8 checkers were fastest for most projects, and only very large programs gained up to 12-16. So the default is one checker per CPU, up to 8, rising with the square root of the checked work up to 16, and no more than one per 32 checked files, but at least the 4 of before when there are as many CPUs. Files whose checking is skipped, such as declaration files under skipLibCheck, count for nothing. Build mode divides the CPUs among the projects it builds concurrently, but never gives a project fewer than the 4 checkers of before, since projects that depend on each other are built one at a time.
1 parent 232406e commit 2a565a2

9 files changed

Lines changed: 90 additions & 9 deletions

File tree

‎tools/scripts/tsc/options.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1697,7 +1697,7 @@ export const options: OptionsModel = {
16971697
group: "commonOptionsWithBuild",
16981698
category: diagnostic("Command-line Options"),
16991699
description: diagnostic("Set the number of checkers per project."),
1700-
defaultValueDescription: diagnostic("4, unless --singleThreaded is passed."),
1700+
defaultValueDescription: diagnostic("Based on the available CPUs and the size of the project, unless --singleThreaded is passed."),
17011701
minValue: 1,
17021702
},
17031703
},

‎tsc/internal/compiler/checkerpool.go‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package compiler
33
import (
44
"context"
55
"math"
6+
"runtime"
67
"slices"
78
"sort"
89
"sync"
@@ -305,6 +306,57 @@ func getCheckerAssociationWeights(baseWeights []int, importCounts []int) []int {
305306
return fileWeights
306307
}
307308

309+
/*
310+
defaultCheckerCount picks the number of checkers when none is given. Which
311+
checker checks a file does not change the results, and checkers take files
312+
from each other once out of their own, so the count only trades time for
313+
memory. Each checker builds much the same library and shared types, so
314+
every checker adds work and memory of its own, and checking is fastest with
315+
about one checker per CPU, up to the point where that duplicated work
316+
dominates:
317+
318+
- With 4 or 8 CPUs, measured with the process restricted to them, one
319+
checker per logical CPU was fastest for nearly every project, and
320+
hyperthreads were nearly as good as cores.
321+
- With 20 CPUs (10 cores), 8 checkers were fastest for most projects;
322+
only very large programs (VS Code) gained up to 12-16.
323+
- Programs with few checked files gain nothing from more checkers than a
324+
few files each, but even those with a few dozen files gain from the 4
325+
checkers that were the default before.
326+
327+
Work is estimated from the files that are checked: those whose checking is
328+
skipped, such as declaration files under skipLibCheck, count for nothing.
329+
*/
330+
func defaultCheckerCount(program *Program) int {
331+
procs := program.opts.CheckerProcs
332+
if procs <= 0 {
333+
procs = runtime.GOMAXPROCS(0)
334+
}
335+
files, weight := 0, 0
336+
for _, file := range program.files {
337+
if !program.SkipTypeChecking(file, true /*ignoreNoCheck*/) {
338+
files++
339+
weight += getCheckerAssociationBaseWeight(file.NodeCount, len(file.Text()))
340+
}
341+
}
342+
limit := min(max(int(defaultCheckerLimit*math.Sqrt(float64(weight)/defaultCheckerLimitWeight)), defaultCheckerLimit), maxDefaultCheckerLimit)
343+
return max(min(procs, limit, (files+filesPerChecker-1)/filesPerChecker), min(procs, minDefaultCheckers))
344+
}
345+
346+
const (
347+
// defaultCheckerLimit is the checker count past which more checkers stop
348+
// paying for their duplicated work, for programs up to
349+
// defaultCheckerLimitWeight of checked work. Larger programs get more, up
350+
// to maxDefaultCheckerLimit, with the square root of their size.
351+
defaultCheckerLimit = 8
352+
defaultCheckerLimitWeight = 8_000_000
353+
maxDefaultCheckerLimit = 16
354+
// filesPerChecker is the number of checked files below which another
355+
// checker does not pay for itself, above minDefaultCheckers.
356+
filesPerChecker = 32
357+
minDefaultCheckers = 4
358+
)
359+
308360
func newCheckerPool(program *Program) *checkerPool {
309361
return newCheckerPoolWithTracing(program, nil)
310362
}
@@ -317,7 +369,7 @@ func newCheckerPoolWithTracing(program *Program, tr *tracing.Tracing) *checkerPo
317369
// of each file given by assign, if set; see ProgramFactories.AssignCheckers.
318370
func newCheckerPoolWithAssignment(program *Program, tr *tracing.Tracing, assign func([]*ast.SourceFile) []int) *checkerPool {
319371
var associations []int
320-
checkerCount := 4
372+
var checkerCount int
321373
if assign != nil {
322374
associations = assign(program.files)
323375
checkerCount = 1
@@ -329,6 +381,8 @@ func newCheckerPoolWithAssignment(program *Program, tr *tracing.Tracing, assign
329381
checkerCount = 1
330382
} else if c := program.Options().Checkers; c != nil {
331383
checkerCount = *c
384+
} else {
385+
checkerCount = defaultCheckerCount(program)
332386
}
333387
checkerCount = max(min(checkerCount, len(program.files), 256), 1)
334388
}

‎tsc/internal/compiler/program.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ type ProgramConfig struct {
5050
// SkipModuleResolution avoids all module and type reference resolution while
5151
// still collecting import metadata needed for emit.
5252
SkipModuleResolution bool
53+
// CheckerProcs is the number of CPUs the checkers of the program may use
54+
// when no checker count is given, or 0 for GOMAXPROCS; see
55+
// defaultCheckerCount.
56+
CheckerProcs int
5357
}
5458

5559
type ProgramHosts struct {

‎tsc/internal/diagnostics/diagnosticMessages.generated.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,7 @@
16961696
"Set_the_number_of_projects_to_build_concurrently_6938": "Set the number of projects to build concurrently.",
16971697
"Deduplicate_packages_with_the_same_name_and_version_6939": "Deduplicate packages with the same name and version.",
16981698
"Allow_loading_external_content_mapper_plugins_that_execute_code_during_compilation_6940": "Allow loading external content mapper plugins that execute code during compilation.",
1699+
"Based_on_the_available_CPUs_and_the_size_of_the_project_unless_singleThreaded_is_passed_6941": "Based on the available CPUs and the size of the project, unless --singleThreaded is passed.",
16991700
"Variable_0_implicitly_has_an_1_type_7005": "Variable '{0}' implicitly has an '{1}' type.",
17001701
"Parameter_0_implicitly_has_an_1_type_7006": "Parameter '{0}' implicitly has an '{1}' type.",
17011702
"Member_0_implicitly_has_an_1_type_7008": "Member '{0}' implicitly has an '{1}' type.",

‎tsc/internal/diagnostics/diagnosticMessages.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6807,6 +6807,10 @@
68076807
"category": "Message",
68086808
"code": 6937
68096809
},
6810+
"Based on the available CPUs and the size of the project, unless --singleThreaded is passed.": {
6811+
"category": "Message",
6812+
"code": 6941
6813+
},
68106814
"Set the number of projects to build concurrently.": {
68116815
"category": "Message",
68126816
"code": 6938

‎tsc/internal/diagnostics/diagnostics_generated.go‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tsc/internal/execute/build/buildtask.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.PathK
249249
compileTimes.BuildInfoReadTime = orchestrator.opts.Sys.Now().Sub(buildInfoReadStart)
250250
parseStart := orchestrator.opts.Sys.Now()
251251
program := compiler.NewProgram(compiler.ProgramOptions{
252-
Config: t.resolved,
253-
Host: compilerHost,
252+
Config: t.resolved,
253+
CheckerProcs: orchestrator.checkerProcs(),
254+
Host: compilerHost,
254255
})
255256
compileTimes.ParseTime = orchestrator.opts.Sys.Now().Sub(parseStart)
256257
changesComputeStart := orchestrator.opts.Sys.Now()

‎tsc/internal/execute/build/orchestrator.go‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"runtime"
78
"slices"
89
"strings"
910
"sync/atomic"
@@ -918,13 +919,26 @@ func (o *Orchestrator) rangeTask(f func(path tspath.PathKey, task *BuildTask)) {
918919
o.rangeTasks(o.order, f)
919920
}
920921

921-
func (o *Orchestrator) rangeTasks(order []*BuildTask, f func(path tspath.PathKey, task *BuildTask)) {
922-
numRoutines := 4
922+
// builderCount returns the number of projects built concurrently.
923+
func (o *Orchestrator) builderCount() int {
923924
if o.opts.Command.CompilerOptions.SingleThreaded.IsTrue() {
924-
numRoutines = 1
925+
return 1
925926
} else if builders := o.opts.Command.BuildOptions.Builders; builders != nil {
926-
numRoutines = *builders
927+
return *builders
927928
}
929+
return 4
930+
}
931+
932+
// checkerProcs returns the CPUs the checkers of each project may use: an
933+
// equal share among the projects built concurrently, but no fewer than the 4
934+
// checkers of before, since projects that depend on each other are built one
935+
// at a time, and the share would leave the rest of the CPUs idle.
936+
func (o *Orchestrator) checkerProcs() int {
937+
return max(runtime.GOMAXPROCS(0)/max(o.builderCount(), 1), 4)
938+
}
939+
940+
func (o *Orchestrator) rangeTasks(order []*BuildTask, f func(path tspath.PathKey, task *BuildTask)) {
941+
numRoutines := o.builderCount()
928942

929943
var currentTaskIndex atomic.Int64
930944
getNextTask := func() (tspath.PathKey, *BuildTask, bool) {

‎tsc/internal/tsoptions/declarations_generated.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)