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  }