Skip to content

Commit b82cd13

Browse files
authored
Restore dependency-depth build scheduling (#64615)
1 parent 0a66074 commit b82cd13

2 files changed

Lines changed: 132 additions & 7 deletions

File tree

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package build_test
22

33
import (
44
"fmt"
5+
"io"
56
"slices"
67
"strings"
78
"testing"
9+
"time"
810

911
"github.com/microsoft/TypeScript/tsc/internal/core"
1012
"github.com/microsoft/TypeScript/tsc/internal/execute/build"
13+
"github.com/microsoft/TypeScript/tsc/internal/execute/tsc"
1114
"github.com/microsoft/TypeScript/tsc/internal/execute/tsctests"
1215
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
1316
"github.com/microsoft/TypeScript/tsc/internal/tspath"
17+
"github.com/microsoft/TypeScript/tsc/internal/vfs"
1418
"gotest.tools/v3/assert"
1519
)
1620

@@ -32,6 +36,123 @@ func TestBuildOrderGenerator(t *testing.T) {
3236
}
3337
}
3438

39+
func TestBuildScheduling(t *testing.T) {
40+
t.Parallel()
41+
for _, operation := range []struct {
42+
name string
43+
project string
44+
refsOnly bool
45+
order []string
46+
}{
47+
{"full build", "", false, []string{"Leaf", "Middle", "Independent", "Root", "Other"}},
48+
{"selected project", "Root", false, []string{"Leaf", "Middle", "Independent", "Root"}},
49+
{"selected references", "Root", true, []string{"Leaf", "Middle", "Independent"}},
50+
} {
51+
t.Run(operation.name, func(t *testing.T) {
52+
t.Parallel()
53+
sys := newSchedulingTestSystem()
54+
independentStarted := make(chan struct{})
55+
sys.fs.onRead = func(path tspath.RootedFilePath) {
56+
if path.BaseName() != "index.ts" {
57+
return
58+
}
59+
switch path.Directory().BaseName() {
60+
case "Leaf":
61+
// Independent must start while Leaf is still occupying a builder.
62+
select {
63+
case <-independentStarted:
64+
return
65+
case <-time.After(30 * time.Second):
66+
t.Error("Independent was queued behind a builder waiting on Leaf")
67+
}
68+
case "Independent":
69+
close(independentStarted)
70+
}
71+
}
72+
command := tsoptions.ParseBuildCommandLine([]string{"--build", "--verbose", "--builders", "2", "Root", "Other"}, sys.FS(), sys.GetCurrentDirectory())
73+
orchestrator := build.NewOrchestrator(build.Options{Sys: sys, Command: command})
74+
var result *build.OrchestratorResult
75+
if operation.refsOnly {
76+
result = orchestrator.BuildReferences(t.Context(), operation.project)
77+
} else {
78+
result = orchestrator.Build(t.Context(), operation.project)
79+
}
80+
assert.Equal(t, result.Result.Status, tsc.ExitStatusSuccess)
81+
assert.Equal(t, result.Statistics.Projects, len(operation.order))
82+
assert.Equal(t, result.Statistics.ProjectsBuilt, len(operation.order))
83+
reported := []string{}
84+
for line := range strings.SplitSeq(sys.output.String(), "\n") {
85+
if strings.Contains(line, "Building project") {
86+
for _, project := range operation.order {
87+
if strings.Contains(line, "'"+project+"/tsconfig.json'") {
88+
reported = append(reported, project)
89+
}
90+
}
91+
}
92+
}
93+
assert.DeepEqual(t, reported, operation.order)
94+
for _, project := range []string{"Leaf", "Middle", "Independent", "Root", "Other"} {
95+
assert.Equal(t, sys.FS().FileExists(sys.GetCurrentDirectory().ResolveFile(project+"/dist/index.js")), slices.Contains(operation.order, project))
96+
}
97+
})
98+
}
99+
}
100+
101+
type schedulingTestFS struct {
102+
vfs.FS
103+
onRead func(tspath.RootedFilePath)
104+
}
105+
106+
func (f *schedulingTestFS) ReadFile(path tspath.RootedFilePath) (string, bool) {
107+
f.onRead(path)
108+
return f.FS.ReadFile(path)
109+
}
110+
111+
type schedulingTestSystem struct {
112+
*tsctests.TestSys
113+
fs *schedulingTestFS
114+
output strings.Builder
115+
}
116+
117+
func (s *schedulingTestSystem) FS() vfs.FS { return s.fs }
118+
func (s *schedulingTestSystem) Writer() io.Writer { return &s.output }
119+
120+
func newSchedulingTestSystem() *schedulingTestSystem {
121+
files := tsctests.FileMap{
122+
"/project/lib.d.ts": `
123+
interface Array<T> {}
124+
interface Boolean {}
125+
interface CallableFunction {}
126+
interface Function {}
127+
interface IArguments {}
128+
interface NewableFunction {}
129+
interface Number {}
130+
interface Object {}
131+
interface RegExp {}
132+
interface String {}
133+
`,
134+
}
135+
deps := map[string][]string{
136+
"Root": {"Middle", "Independent"},
137+
"Middle": {"Leaf"},
138+
"Leaf": {},
139+
"Independent": {},
140+
"Other": {},
141+
}
142+
for project, references := range deps {
143+
files["/project/"+project+"/index.ts"] = "export const value = 1;"
144+
files["/project/"+project+"/tsconfig.json"] = fmt.Sprintf(`{
145+
"compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" },
146+
"files": ["../lib.d.ts", "index.ts"],
147+
"references": [%s]
148+
}`, strings.Join(core.Map(references, func(ref string) string {
149+
return fmt.Sprintf(`{ "path": "../%s" }`, ref)
150+
}), ","))
151+
}
152+
sys := tsctests.NewTscSystem(files, tspath.CaseSensitive, "/project")
153+
return &schedulingTestSystem{TestSys: sys, fs: &schedulingTestFS{FS: sys.FS()}}
154+
}
155+
35156
type buildOrderTestCase struct {
36157
name string
37158
projects []string

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,14 @@ func (o *Orchestrator) ScheduleOrder() []string {
124124
// picked up may not be done yet, so a builder can take a dependent of a slow project and
125125
// wait on that project while a later project's upstream has already finished. The stable
126126
// sort preserves the original order within a depth, and reporting still follows Order().
127-
func (o *Orchestrator) computeScheduleOrder() []*BuildTask {
127+
func (o *Orchestrator) computeScheduleOrder(order []*BuildTask) []*BuildTask {
128128
type scheduleEntry struct {
129129
task *BuildTask
130130
depth int
131131
}
132-
entries := make([]scheduleEntry, len(o.order))
133-
depths := make(map[*BuildTask]int, len(o.order))
134-
for i, task := range o.order {
132+
entries := make([]scheduleEntry, len(order))
133+
depths := make(map[*BuildTask]int, len(order))
134+
for i, task := range order {
135135
depth := 0
136136
for _, upstream := range task.upStream {
137137
depth = max(depth, depths[upstream.task]+1)
@@ -271,7 +271,7 @@ func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.PathKe
271271
for _, project := range projects {
272272
o.setupBuildTask(project, nil, false, &completed, &analyzing, circularityStack)
273273
}
274-
o.scheduleOrder = o.computeScheduleOrder()
274+
o.scheduleOrder = o.computeScheduleOrder(o.order)
275275
if oldTasks != nil {
276276
oldTasks.Range(func(path tspath.PathKey, oldTask *BuildTask) bool {
277277
if task, ok := o.tasks.Load(path); ok && task == oldTask {
@@ -884,7 +884,11 @@ func (o *Orchestrator) buildOrCleanOrder(order []*BuildTask) *OrchestratorResult
884884
// prevReporter = task
885885
// }
886886
buildResult.Statistics.Projects = len(order)
887-
// Builders pick up projects in scheduleOrder; results are reported in Order(), waiting for each project to finish
887+
scheduleOrder := o.scheduleOrder
888+
if len(order) != len(o.order) {
889+
scheduleOrder = o.computeScheduleOrder(order)
890+
}
891+
// Builders pick up selected projects in dependency-depth order; reporting preserves the selected build order.
888892
reported := make(chan struct{})
889893
go func() {
890894
defer close(reported)
@@ -893,7 +897,7 @@ func (o *Orchestrator) buildOrCleanOrder(order []*BuildTask) *OrchestratorResult
893897
task.report(o, task.path, buildResult)
894898
}
895899
}()
896-
o.rangeTasks(order, func(path tspath.PathKey, task *BuildTask) {
900+
o.rangeTasks(scheduleOrder, func(path tspath.PathKey, task *BuildTask) {
897901
o.buildOrCleanProject(task, path)
898902
})
899903
<-reported

0 commit comments

Comments
 (0)