refactor/importgraph: reduce I/O concurrency to avoid EMFILE

Use an concurrency-limiting semaphore to reduce I/O parallelism in Import.

Also, start the producer in a new goroutine so that it runs in parallel
with the consumer.  Paradoxically, this reduces the peak number of
goroutines.

Also, in buildutil.ForEachPackage, make the concurrency limiting
semaphore global, since I/O parallelism is a process-wide resource.

Change-Id: I282b717c50603361826e5675077c9f464c874132
Reviewed-on: https://go-review.googlesource.com/18215
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/refactor/importgraph/graph.go b/refactor/importgraph/graph.go
index 8a833dd..7afb180 100644
--- a/refactor/importgraph/graph.go
+++ b/refactor/importgraph/graph.go
@@ -68,76 +68,78 @@
 
 	ch := make(chan interface{})
 
-	sema := make(chan int, 20) // I/O concurrency limiting semaphore
-	var wg sync.WaitGroup
-	buildutil.ForEachPackage(ctxt, func(path string, err error) {
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
+	go func() {
+		sema := make(chan int, 20) // I/O concurrency limiting semaphore
+		var wg sync.WaitGroup
+		buildutil.ForEachPackage(ctxt, func(path string, err error) {
 			if err != nil {
 				ch <- pathError{path, err}
 				return
 			}
 
-			sema <- 1
-			bp, err := ctxt.Import(path, "", buildutil.AllowVendor)
-			<-sema
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
 
-			if err != nil {
-				if _, ok := err.(*build.NoGoError); ok {
-					// empty directory is not an error
-				} else {
-					ch <- pathError{path, err}
-				}
-				// Even in error cases, Import usually returns a package.
-			}
+				sema <- 1
+				bp, err := ctxt.Import(path, "", buildutil.AllowVendor)
+				<-sema
 
-			// absolutize resolves an import path relative
-			// to the current package bp.
-			// The absolute form may contain "vendor".
-			//
-			// The vendoring feature slows down Build by 3×.
-			// Here are timings from a 1400 package workspace:
-			//    1100ms: current code (with vendor check)
-			//     880ms: with a nonblocking cache around ctxt.IsDir
-			//     840ms: nonblocking cache with duplicate suppression
-			//     340ms: original code (no vendor check)
-			// TODO(adonovan): optimize, somehow.
-			absolutize := func(path string) string { return path }
-			if buildutil.AllowVendor != 0 {
-				memo := make(map[string]string)
-				absolutize = func(path string) string {
-					canon, ok := memo[path]
-					if !ok {
-						sema <- 1
-						bp2, _ := ctxt.Import(path, bp.Dir, build.FindOnly|buildutil.AllowVendor)
-						<-sema
-
-						if bp2 != nil {
-							canon = bp2.ImportPath
-						} else {
-							canon = path
-						}
-						memo[path] = canon
+				if err != nil {
+					if _, ok := err.(*build.NoGoError); ok {
+						// empty directory is not an error
+					} else {
+						ch <- pathError{path, err}
 					}
-					return canon
+					// Even in error cases, Import usually returns a package.
 				}
-			}
 
-			if bp != nil {
-				for _, imp := range bp.Imports {
-					ch <- importEdge{path, absolutize(imp)}
+				// absolutize resolves an import path relative
+				// to the current package bp.
+				// The absolute form may contain "vendor".
+				//
+				// The vendoring feature slows down Build by 3×.
+				// Here are timings from a 1400 package workspace:
+				//    1100ms: current code (with vendor check)
+				//     880ms: with a nonblocking cache around ctxt.IsDir
+				//     840ms: nonblocking cache with duplicate suppression
+				//     340ms: original code (no vendor check)
+				// TODO(adonovan): optimize, somehow.
+				absolutize := func(path string) string { return path }
+				if buildutil.AllowVendor != 0 {
+					memo := make(map[string]string)
+					absolutize = func(path string) string {
+						canon, ok := memo[path]
+						if !ok {
+							sema <- 1
+							bp2, _ := ctxt.Import(path, bp.Dir, build.FindOnly|buildutil.AllowVendor)
+							<-sema
+
+							if bp2 != nil {
+								canon = bp2.ImportPath
+							} else {
+								canon = path
+							}
+							memo[path] = canon
+						}
+						return canon
+					}
 				}
-				for _, imp := range bp.TestImports {
-					ch <- importEdge{path, absolutize(imp)}
+
+				if bp != nil {
+					for _, imp := range bp.Imports {
+						ch <- importEdge{path, absolutize(imp)}
+					}
+					for _, imp := range bp.TestImports {
+						ch <- importEdge{path, absolutize(imp)}
+					}
+					for _, imp := range bp.XTestImports {
+						ch <- importEdge{path, absolutize(imp)}
+					}
 				}
-				for _, imp := range bp.XTestImports {
-					ch <- importEdge{path, absolutize(imp)}
-				}
-			}
-		}()
-	})
-	go func() {
+
+			}()
+		})
 		wg.Wait()
 		close(ch)
 	}()