github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/builder/jobs.go (about) 1 package builder 2 3 // This file implements a job runner for the compiler, which runs jobs in 4 // parallel while taking care of dependencies. 5 6 import ( 7 "container/heap" 8 "errors" 9 "fmt" 10 "runtime" 11 "sort" 12 "strings" 13 "time" 14 ) 15 16 // Set to true to enable logging in the job runner. This may help to debug 17 // concurrency or performance issues. 18 const jobRunnerDebug = false 19 20 type jobState uint8 21 22 const ( 23 jobStateQueued jobState = iota // not yet running 24 jobStateRunning // running 25 jobStateFinished // finished running 26 ) 27 28 // compileJob is a single compiler job, comparable to a single Makefile target. 29 // It is used to orchestrate various compiler tasks that can be run in parallel 30 // but that have dependencies and thus have limitations in how they can be run. 31 type compileJob struct { 32 description string // description, only used for logging 33 dependencies []*compileJob 34 result string // result (path) 35 run func(*compileJob) (err error) 36 err error // error if finished 37 duration time.Duration // how long it took to run this job (only set after finishing) 38 } 39 40 // dummyCompileJob returns a new *compileJob that produces an output without 41 // doing anything. This can be useful where a *compileJob producing an output is 42 // expected but nothing needs to be done, for example for a load from a cache. 43 func dummyCompileJob(result string) *compileJob { 44 return &compileJob{ 45 description: "<dummy>", 46 result: result, 47 } 48 } 49 50 // runJobs runs the indicated job and all its dependencies. For every job, all 51 // the dependencies are run first. It returns the error of the first job that 52 // fails. 53 // It runs all jobs in the order of the dependencies slice, depth-first. 54 // Therefore, if some jobs are preferred to run before others, they should be 55 // ordered as such in the job dependencies. 56 func runJobs(job *compileJob, sema chan struct{}) error { 57 if sema == nil { 58 // Have a default, if the semaphore isn't set. This is useful for 59 // tests. 60 sema = make(chan struct{}, runtime.NumCPU()) 61 } 62 if cap(sema) == 0 { 63 return errors.New("cannot 0 jobs at a time") 64 } 65 66 // Create a slice of jobs to run, where all dependencies are run in order. 67 jobs := []*compileJob{} 68 addedJobs := map[*compileJob]struct{}{} 69 var addJobs func(*compileJob) 70 addJobs = func(job *compileJob) { 71 if _, ok := addedJobs[job]; ok { 72 return 73 } 74 for _, dep := range job.dependencies { 75 addJobs(dep) 76 } 77 jobs = append(jobs, job) 78 addedJobs[job] = struct{}{} 79 } 80 addJobs(job) 81 82 waiting := make(map[*compileJob]map[*compileJob]struct{}, len(jobs)) 83 dependents := make(map[*compileJob][]*compileJob, len(jobs)) 84 jidx := make(map[*compileJob]int) 85 var ready intHeap 86 for i, job := range jobs { 87 jidx[job] = i 88 if len(job.dependencies) == 0 { 89 // This job is ready to run. 90 ready.Push(i) 91 continue 92 } 93 94 // Construct a map for dependencies which the job is currently waiting on. 95 waitDeps := make(map[*compileJob]struct{}) 96 waiting[job] = waitDeps 97 98 // Add the job to the dependents list of each dependency. 99 for _, dep := range job.dependencies { 100 dependents[dep] = append(dependents[dep], job) 101 waitDeps[dep] = struct{}{} 102 } 103 } 104 105 // Create a channel to accept notifications of completion. 106 doneChan := make(chan *compileJob) 107 108 // Send each job in the jobs slice to a worker, taking care of job 109 // dependencies. 110 numRunningJobs := 0 111 var totalTime time.Duration 112 start := time.Now() 113 for len(ready.IntSlice) > 0 || numRunningJobs != 0 { 114 var completed *compileJob 115 if len(ready.IntSlice) > 0 { 116 select { 117 case sema <- struct{}{}: 118 // Start a job. 119 job := jobs[heap.Pop(&ready).(int)] 120 if jobRunnerDebug { 121 fmt.Println("## start: ", job.description) 122 } 123 go runJob(job, doneChan) 124 numRunningJobs++ 125 continue 126 127 case completed = <-doneChan: 128 // A job completed. 129 } 130 } else { 131 // Wait for a job to complete. 132 completed = <-doneChan 133 } 134 numRunningJobs-- 135 <-sema 136 if jobRunnerDebug { 137 fmt.Println("## finished:", completed.description, "(time "+completed.duration.String()+")") 138 } 139 if completed.err != nil { 140 // Wait for any current jobs to finish. 141 for numRunningJobs != 0 { 142 <-doneChan 143 numRunningJobs-- 144 } 145 146 // The build failed. 147 return completed.err 148 } 149 150 // Update total run time. 151 totalTime += completed.duration 152 153 // Update dependent jobs. 154 for _, j := range dependents[completed] { 155 wait := waiting[j] 156 delete(wait, completed) 157 if len(wait) == 0 { 158 // This job is now ready to run. 159 ready.Push(jidx[j]) 160 delete(waiting, j) 161 } 162 } 163 } 164 if len(waiting) != 0 { 165 // There is a dependency cycle preventing some jobs from running. 166 return errDependencyCycle{waiting} 167 } 168 169 // Some statistics, if debugging. 170 if jobRunnerDebug { 171 // Total duration of running all jobs. 172 duration := time.Since(start) 173 fmt.Println("## total: ", duration) 174 175 // The individual time of each job combined. On a multicore system, this 176 // should be lower than the total above. 177 fmt.Println("## job sum: ", totalTime) 178 } 179 180 return nil 181 } 182 183 type errDependencyCycle struct { 184 waiting map[*compileJob]map[*compileJob]struct{} 185 } 186 187 func (err errDependencyCycle) Error() string { 188 waits := make([]string, 0, len(err.waiting)) 189 for j, wait := range err.waiting { 190 deps := make([]string, 0, len(wait)) 191 for dep := range wait { 192 deps = append(deps, dep.description) 193 } 194 sort.Strings(deps) 195 196 waits = append(waits, fmt.Sprintf("\t%s is waiting for [%s]", 197 j.description, strings.Join(deps, ", "), 198 )) 199 } 200 sort.Strings(waits) 201 return "deadlock:\n" + strings.Join(waits, "\n") 202 } 203 204 type intHeap struct { 205 sort.IntSlice 206 } 207 208 func (h *intHeap) Push(x interface{}) { 209 h.IntSlice = append(h.IntSlice, x.(int)) 210 } 211 212 func (h *intHeap) Pop() interface{} { 213 x := h.IntSlice[len(h.IntSlice)-1] 214 h.IntSlice = h.IntSlice[:len(h.IntSlice)-1] 215 return x 216 } 217 218 // runJob runs a compile job and notifies doneChan of completion. 219 func runJob(job *compileJob, doneChan chan *compileJob) { 220 start := time.Now() 221 if job.run != nil { 222 err := job.run(job) 223 if err != nil { 224 job.err = err 225 } 226 } 227 job.duration = time.Since(start) 228 doneChan <- job 229 }