Skip to content

Commit 40ef382

Browse files
authored
Add --builders to limit number of projects can build concurrently (#2307)
1 parent caa2977 commit 40ef382

28 files changed

+12410
-15
lines changed

internal/core/buildoptions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type BuildOptions struct {
66
Dry Tristate `json:"dry,omitzero"`
77
Force Tristate `json:"force,omitzero"`
88
Verbose Tristate `json:"verbose,omitzero"`
9+
Builders *int `json:"builders,omitzero"`
910
StopBuildOnErrors Tristate `json:"stopBuildOnErrors,omitzero"`
1011

1112
// CompilerOptions are not parsed here and will be available on ParsedBuildCommandLine

internal/diagnostics/diagnostics_generated.go

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

internal/diagnostics/extraDiagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
"category": "Message",
3636
"code": 100008
3737
},
38+
"Set the number of projects to build concurrently.": {
39+
"category": "Message",
40+
"code": 100009
41+
},
42+
"all, unless --singleThreaded is passed.": {
43+
"category": "Message",
44+
"code": 100010
45+
},
3846
"Non-relative paths are not allowed. Did you forget a leading './'?": {
3947
"category": "Error",
4048
"code": 5090
@@ -74,5 +82,9 @@
7482
"Option '--incremental' is only valid with a known configuration file (like 'tsconfig.json') or when '--tsBuildInfoFile' is explicitly provided.": {
7583
"category": "Error",
7684
"code": 5074
85+
},
86+
"Option '{0}' requires value to be greater than '{1}'.": {
87+
"category": "Error",
88+
"code": 5002
7789
}
7890
}

internal/execute/build/buildtask.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ func (t *BuildTask) updateDownstream(orchestrator *Orchestrator, path tspath.Pat
200200
}
201201

202202
func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.Path) {
203+
if orchestrator.buildSemaphore != nil {
204+
orchestrator.buildSemaphore <- struct{}{} // acquire slot
205+
defer func() { <-orchestrator.buildSemaphore }() // release slot
206+
}
203207
t.errors = nil
204208
if orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() {
205209
t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.Building_project_0, orchestrator.relativeFileName(t.config)))

internal/execute/build/orchestrator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ type Orchestrator struct {
6161
tasks *collections.SyncMap[tspath.Path, *BuildTask]
6262
order []string
6363
errors []*ast.Diagnostic
64+
// Semaphore to limit concurrent builds
65+
buildSemaphore chan struct{}
6466

6567
errorSummaryReporter tsc.DiagnosticsReporter
6668
watchStatusReporter tsc.DiagnosticReporter
@@ -396,5 +398,9 @@ func NewOrchestrator(opts Options) *Orchestrator {
396398
} else {
397399
orchestrator.errorSummaryReporter = tsc.CreateReportErrorSummary(opts.Sys, opts.Command.Locale(), opts.Command.CompilerOptions)
398400
}
401+
// If we want to build more than one project at a time, create a semaphore to limit concurrency
402+
if builders := opts.Command.BuildOptions.Builders; builders != nil {
403+
orchestrator.buildSemaphore = make(chan struct{}, *builders)
404+
}
399405
return orchestrator
400406
}

internal/execute/tsctests/tscbuild_test.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tsctests
33
import (
44
"fmt"
55
"slices"
6+
"strconv"
67
"strings"
78
"testing"
89
"time"
@@ -2174,7 +2175,7 @@ func TestBuildProjectsBuilding(t *testing.T) {
21742175
return files
21752176
}
21762177

2177-
getTestCases := func(pkgCount int) []*tscInput {
2178+
getTestCases := func(pkgCount int, builders int) []*tscInput {
21782179
edits := []*tscEdit{
21792180
{
21802181
caption: "dts doesn't change",
@@ -2199,21 +2200,35 @@ func TestBuildProjectsBuilding(t *testing.T) {
21992200
commandLineArgs: []string{"-b", "-v"},
22002201
edits: edits,
22012202
},
2203+
{
2204+
subScenario: fmt.Sprintf(`when there are %d projects in a solution with --builders %d`, pkgCount, builders),
2205+
files: files(pkgCount),
2206+
cwd: "/user/username/projects/myproject",
2207+
commandLineArgs: []string{"-b", "-v", "--builders", strconv.Itoa(builders)},
2208+
edits: edits,
2209+
},
22022210
{
22032211
subScenario: fmt.Sprintf(`when there are %d projects in a solution`, pkgCount),
22042212
files: files(pkgCount),
22052213
cwd: "/user/username/projects/myproject",
22062214
commandLineArgs: []string{"-b", "-w", "-v"},
22072215
edits: edits,
22082216
},
2217+
{
2218+
subScenario: fmt.Sprintf(`when there are %d projects in a solution with --builders %d`, pkgCount, builders),
2219+
files: files(pkgCount),
2220+
cwd: "/user/username/projects/myproject",
2221+
commandLineArgs: []string{"-b", "-w", "-v", "--builders", strconv.Itoa(builders)},
2222+
edits: edits,
2223+
},
22092224
}
22102225
}
22112226

22122227
testCases := slices.Concat(
2213-
getTestCases(3),
2214-
getTestCases(5),
2215-
getTestCases(8),
2216-
getTestCases(23),
2228+
getTestCases(3, 1),
2229+
getTestCases(5, 2),
2230+
getTestCases(8, 3),
2231+
getTestCases(23, 3),
22172232
)
22182233

22192234
for _, test := range testCases {

internal/tsoptions/commandlineoption.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type CommandLineOption struct {
3838
// What kind of extra validation `validateJsonOptionValue` should do
3939
extraValidation extraValidation
4040

41+
// checks that option with number type has value >= minValue
42+
minValue int
43+
4144
// true or undefined
4245
// used for configDirTemplateSubstitutionOptions
4346
allowConfigDirTemplateSubstitution bool

internal/tsoptions/commandlineparser.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (p *commandLineParser) parseStrings(args []string) {
142142
inputOptionName := getInputOptionName(s)
143143
opt := p.optionsMap.GetOptionDeclarationFromName(inputOptionName, true /*allowShort*/)
144144
if opt != nil {
145-
i = p.parseOptionValue(args, i, opt, nil)
145+
i = p.parseOptionValue(args, i, opt, p.workerDiagnostics.OptionTypeMismatchDiagnostic)
146146
} else {
147147
watchOpt := WatchNameMap.GetOptionDeclarationFromName(inputOptionName, true /*allowShort*/)
148148
if watchOpt != nil {
@@ -256,9 +256,6 @@ func (p *commandLineParser) parseOptionValue(
256256
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
257257
if i >= len(args) {
258258
if opt.Kind != "boolean" {
259-
if diag == nil {
260-
diag = p.workerDiagnostics.OptionTypeMismatchDiagnostic
261-
}
262259
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diag, opt.Name, getCompilerOptionValueTypeString(opt)))
263260
if opt.Kind == "list" {
264261
p.options.Set(opt.Name, []string{})
@@ -276,7 +273,13 @@ func (p *commandLineParser) parseOptionValue(
276273
// !!! Make sure this parseInt matches JS parseInt
277274
num, e := strconv.Atoi(args[i])
278275
if e == nil {
279-
p.options.Set(opt.Name, num)
276+
if num >= opt.minValue {
277+
p.options.Set(opt.Name, num)
278+
} else {
279+
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Option_0_requires_value_to_be_greater_than_1, opt.Name, strconv.Itoa(opt.minValue)))
280+
}
281+
} else {
282+
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diag, opt.Name, "number"))
280283
}
281284
i++
282285
case "boolean":

internal/tsoptions/commandlineparser_test.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,22 @@ func formatNewBaseline(
292292
}
293293

294294
func (f commandLineSubScenario) assertBuildParseResult(t *testing.T) {
295+
t.Helper()
296+
f.assertBuildParseResultWithTsBaseline(t, func() *TestCommandLineParserBuild {
297+
originalBaseline := f.baseline.ReadFile(t)
298+
return parseExistingCompilerBaselineBuild(t, originalBaseline)
299+
})
300+
}
301+
302+
func (f commandLineSubScenario) assertBuildParseResultWithTsBaseline(t *testing.T, getTsBaseline func() *TestCommandLineParserBuild) {
295303
t.Helper()
296304
t.Run(f.testName, func(t *testing.T) {
297305
t.Parallel()
298-
originalBaseline := f.baseline.ReadFile(t)
299-
tsBaseline := parseExistingCompilerBaselineBuild(t, originalBaseline)
306+
307+
var tsBaseline *TestCommandLineParserBuild
308+
if getTsBaseline != nil {
309+
tsBaseline = getTsBaseline()
310+
}
300311

301312
// f.workerDiagnostic is either defined or set to default pointer in `createSubScenario`
302313
parsed := tsoptions.ParseBuildCommandLine(f.commandLine, &tsoptionstest.VfsParseConfigHost{
@@ -305,19 +316,25 @@ func (f commandLineSubScenario) assertBuildParseResult(t *testing.T) {
305316
})
306317

307318
newBaselineProjects := strings.Join(parsed.Projects, ",")
308-
assert.Equal(t, tsBaseline.projects, newBaselineProjects)
319+
if getTsBaseline != nil {
320+
assert.Equal(t, tsBaseline.projects, newBaselineProjects)
321+
}
309322

310323
o, _ := json.Marshal(parsed.BuildOptions)
311324
newParsedBuildOptions := &core.BuildOptions{}
312325
e := json.Unmarshal(o, newParsedBuildOptions)
313326
assert.NilError(t, e)
314-
assert.DeepEqual(t, tsBaseline.options, newParsedBuildOptions, cmpopts.IgnoreUnexported(core.BuildOptions{}))
327+
if getTsBaseline != nil {
328+
assert.DeepEqual(t, tsBaseline.options, newParsedBuildOptions, cmpopts.IgnoreUnexported(core.BuildOptions{}))
329+
}
315330

316331
compilerOpts, _ := json.Marshal(parsed.CompilerOptions)
317332
newParsedCompilerOptions := &core.CompilerOptions{}
318333
e = json.Unmarshal(compilerOpts, newParsedCompilerOptions)
319334
assert.NilError(t, e)
320-
assert.DeepEqual(t, tsBaseline.compilerOptions, newParsedCompilerOptions, cmpopts.IgnoreUnexported(core.CompilerOptions{}))
335+
if getTsBaseline != nil {
336+
assert.DeepEqual(t, tsBaseline.compilerOptions, newParsedCompilerOptions, cmpopts.IgnoreUnexported(core.CompilerOptions{}))
337+
}
321338

322339
newParsedWatchOptions := core.WatchOptions{}
323340
e = json.Unmarshal(o, &newParsedWatchOptions)
@@ -478,6 +495,18 @@ func TestParseBuildCommandLine(t *testing.T) {
478495
for _, testCase := range parseCommandLineSubScenarios {
479496
testCase.createSubScenario("parseBuildOptions").assertBuildParseResult(t)
480497
}
498+
499+
extraScenarios := []*subScenarioInput{
500+
{`parse --builders`, []string{"--builders", "2"}},
501+
{`--singleThreaded and --builders together`, []string{"--singleThreaded", "--builders", "2"}},
502+
{`reports error when --builders is 0`, []string{"--builders", "0"}},
503+
{`reports error when --builders is negative`, []string{"--builders", "-1"}},
504+
{`reports error when --builders is invalid type`, []string{"--builders", "invalid"}},
505+
}
506+
507+
for _, testCase := range extraScenarios {
508+
testCase.createSubScenario("parseBuildOptions").assertBuildParseResultWithTsBaseline(t, nil)
509+
}
481510
}
482511

483512
func TestAffectsBuildInfo(t *testing.T) {

internal/tsoptions/declsbuild.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ var OptionsForBuild = []*CommandLineOption{
4949
Kind: "boolean",
5050
DefaultValueDescription: false,
5151
},
52+
{
53+
Name: "builders",
54+
Kind: CommandLineOptionTypeNumber,
55+
Category: diagnostics.Command_line_Options,
56+
Description: diagnostics.Set_the_number_of_projects_to_build_concurrently,
57+
DefaultValueDescription: diagnostics.X_all_unless_singleThreaded_is_passed,
58+
minValue: 1,
59+
},
5260
{
5361
Name: "stopBuildOnErrors",
5462
Category: diagnostics.Command_line_Options,

0 commit comments

Comments
 (0)