Skip to content

Commit 59933b6

Browse files
adonovangopherbot
authored andcommitted
go/packages: create fewer goroutines
This CL changes the concurrent recursion over the import graph so that instead of creating one goroutine per import edge (the naive approach formerly used by loadRecursive), it creates only one per package. We follow the approach used by 'enqueue' in gopls/internal/cache/analysis.go: we construct the inverse import graph, and keep a count of unfinished successors (direct dependencies). Only when this count drops to zero do we add a package to the queue. Subtle: the import graph must now be materalized not just when NeedImports, but when Syntax or Types are needed too. Also, we add a CPU-limiting semaphore around calls to the parser and type-checker. This seems to improve the BenchmarkNetHTTP by around 10% elapsed time. Before: BenchmarkNetHTTP-8 19 550622678 ns/op 17.12 MB/s 484348024 B/op 4951363 allocs/op After: BenchmarkNetHTTP-8 24 499117113 ns/op 18.89 MB/s 484340136 B/op 4949985 allocs/op Updates golang/go#70094 Change-Id: I54bf17b16238844401aba4ec5a7f6485da5e73fb Reviewed-on: https://go-review.googlesource.com/c/tools/+/624816 Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent f1f7c26 commit 59933b6

File tree

1 file changed

+142
-101
lines changed

1 file changed

+142
-101
lines changed

go/packages/packages.go

Lines changed: 142 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import (
1616
"go/scanner"
1717
"go/token"
1818
"go/types"
19-
"io"
2019
"log"
2120
"os"
2221
"path/filepath"
2322
"runtime"
2423
"strings"
2524
"sync"
25+
"sync/atomic"
2626
"time"
2727

2828
"golang.org/x/sync/errgroup"
@@ -55,7 +55,7 @@ const (
5555
// NeedName adds Name and PkgPath.
5656
NeedName LoadMode = 1 << iota
5757

58-
// NeedFiles adds GoFiles and OtherFiles.
58+
// NeedFiles adds GoFiles, OtherFiles, and IgnoredFiles
5959
NeedFiles
6060

6161
// NeedCompiledGoFiles adds CompiledGoFiles.
@@ -77,7 +77,7 @@ const (
7777
// NeedSyntax adds Syntax and Fset.
7878
NeedSyntax
7979

80-
// NeedTypesInfo adds TypesInfo.
80+
// NeedTypesInfo adds TypesInfo and Fset.
8181
NeedTypesInfo
8282

8383
// NeedTypesSizes adds TypesSizes.
@@ -691,18 +691,19 @@ func (p *Package) String() string { return p.ID }
691691
// loaderPackage augments Package with state used during the loading phase
692692
type loaderPackage struct {
693693
*Package
694-
importErrors map[string]error // maps each bad import to its error
695-
loadOnce sync.Once
696-
color uint8 // for cycle detection
697-
needsrc bool // load from source (Mode >= LoadTypes)
698-
needtypes bool // type information is either requested or depended on
699-
initial bool // package was matched by a pattern
700-
goVersion int // minor version number of go command on PATH
694+
importErrors map[string]error // maps each bad import to its error
695+
preds []*loaderPackage // packages that import this one
696+
unfinishedSuccs atomic.Int32 // number of direct imports not yet loaded
697+
color uint8 // for cycle detection
698+
needsrc bool // load from source (Mode >= LoadTypes)
699+
needtypes bool // type information is either requested or depended on
700+
initial bool // package was matched by a pattern
701+
goVersion int // minor version number of go command on PATH
701702
}
702703

703704
// loader holds the working state of a single call to load.
704705
type loader struct {
705-
pkgs map[string]*loaderPackage
706+
pkgs map[string]*loaderPackage // keyed by Package.ID
706707
Config
707708
sizes types.Sizes // non-nil if needed by mode
708709
parseCache map[string]*parseValue
@@ -764,7 +765,7 @@ func newLoader(cfg *Config) *loader {
764765
ld.requestedMode = ld.Mode
765766
ld.Mode = impliedLoadMode(ld.Mode)
766767

767-
if ld.Mode&(NeedTypes|NeedSyntax|NeedTypesInfo) != 0 {
768+
if ld.Mode&(NeedSyntax|NeedTypes|NeedTypesInfo) != 0 {
768769
if ld.Fset == nil {
769770
ld.Fset = token.NewFileSet()
770771
}
@@ -805,7 +806,7 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
805806
exportDataInvalid := len(ld.Overlay) > 0 || pkg.ExportFile == "" && pkg.PkgPath != "unsafe"
806807
// This package needs type information if the caller requested types and the package is
807808
// either a root, or it's a non-root and the user requested dependencies ...
808-
needtypes := (ld.Mode&NeedTypes|NeedTypesInfo != 0 && (rootIndex >= 0 || ld.Mode&NeedDeps != 0))
809+
needtypes := (ld.Mode&(NeedTypes|NeedTypesInfo) != 0 && (rootIndex >= 0 || ld.Mode&NeedDeps != 0))
809810
// This package needs source if the call requested source (or types info, which implies source)
810811
// and the package is either a root, or itas a non- root and the user requested dependencies...
811812
needsrc := ((ld.Mode&(NeedSyntax|NeedTypesInfo) != 0 && (rootIndex >= 0 || ld.Mode&NeedDeps != 0)) ||
@@ -830,9 +831,10 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
830831
}
831832
}
832833

833-
if ld.Mode&NeedImports != 0 {
834-
// Materialize the import graph.
835-
834+
// Materialize the import graph if it is needed (NeedImports),
835+
// or if we'll be using loadPackages (Need{Syntax|Types|TypesInfo}).
836+
var leaves []*loaderPackage // packages with no unfinished successors
837+
if ld.Mode&(NeedImports|NeedSyntax|NeedTypes|NeedTypesInfo) != 0 {
836838
const (
837839
white = 0 // new
838840
grey = 1 // in progress
@@ -851,63 +853,76 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
851853
// dependency on a package that does. These are the only packages
852854
// for which we load source code.
853855
var stack []*loaderPackage
854-
var visit func(lpkg *loaderPackage) bool
855-
visit = func(lpkg *loaderPackage) bool {
856-
switch lpkg.color {
857-
case black:
858-
return lpkg.needsrc
859-
case grey:
856+
var visit func(from, lpkg *loaderPackage) bool
857+
visit = func(from, lpkg *loaderPackage) bool {
858+
if lpkg.color == grey {
860859
panic("internal error: grey node")
861860
}
862-
lpkg.color = grey
863-
stack = append(stack, lpkg) // push
864-
stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports
865-
lpkg.Imports = make(map[string]*Package, len(stubs))
866-
for importPath, ipkg := range stubs {
867-
var importErr error
868-
imp := ld.pkgs[ipkg.ID]
869-
if imp == nil {
870-
// (includes package "C" when DisableCgo)
871-
importErr = fmt.Errorf("missing package: %q", ipkg.ID)
872-
} else if imp.color == grey {
873-
importErr = fmt.Errorf("import cycle: %s", stack)
861+
if lpkg.color == white {
862+
lpkg.color = grey
863+
stack = append(stack, lpkg) // push
864+
stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports
865+
lpkg.Imports = make(map[string]*Package, len(stubs))
866+
for importPath, ipkg := range stubs {
867+
var importErr error
868+
imp := ld.pkgs[ipkg.ID]
869+
if imp == nil {
870+
// (includes package "C" when DisableCgo)
871+
importErr = fmt.Errorf("missing package: %q", ipkg.ID)
872+
} else if imp.color == grey {
873+
importErr = fmt.Errorf("import cycle: %s", stack)
874+
}
875+
if importErr != nil {
876+
if lpkg.importErrors == nil {
877+
lpkg.importErrors = make(map[string]error)
878+
}
879+
lpkg.importErrors[importPath] = importErr
880+
continue
881+
}
882+
883+
if visit(lpkg, imp) {
884+
lpkg.needsrc = true
885+
}
886+
lpkg.Imports[importPath] = imp.Package
874887
}
875-
if importErr != nil {
876-
if lpkg.importErrors == nil {
877-
lpkg.importErrors = make(map[string]error)
888+
889+
// -- postorder --
890+
891+
// Complete type information is required for the
892+
// immediate dependencies of each source package.
893+
if lpkg.needsrc && ld.Mode&NeedTypes != 0 {
894+
for _, ipkg := range lpkg.Imports {
895+
ld.pkgs[ipkg.ID].needtypes = true
878896
}
879-
lpkg.importErrors[importPath] = importErr
880-
continue
881897
}
882898

883-
if visit(imp) {
884-
lpkg.needsrc = true
899+
// NeedTypeSizes causes TypeSizes to be set even
900+
// on packages for which types aren't needed.
901+
if ld.Mode&NeedTypesSizes != 0 {
902+
lpkg.TypesSizes = ld.sizes
885903
}
886-
lpkg.Imports[importPath] = imp.Package
887-
}
888904

889-
// Complete type information is required for the
890-
// immediate dependencies of each source package.
891-
if lpkg.needsrc && ld.Mode&NeedTypes != 0 {
892-
for _, ipkg := range lpkg.Imports {
893-
ld.pkgs[ipkg.ID].needtypes = true
905+
// Add packages with no imports directly to the queue of leaves.
906+
if len(lpkg.Imports) == 0 {
907+
leaves = append(leaves, lpkg)
894908
}
909+
910+
stack = stack[:len(stack)-1] // pop
911+
lpkg.color = black
895912
}
896913

897-
// NeedTypeSizes causes TypeSizes to be set even
898-
// on packages for which types aren't needed.
899-
if ld.Mode&NeedTypesSizes != 0 {
900-
lpkg.TypesSizes = ld.sizes
914+
// Add edge from predecessor.
915+
if from != nil {
916+
from.unfinishedSuccs.Add(+1) // incref
917+
lpkg.preds = append(lpkg.preds, from)
901918
}
902-
stack = stack[:len(stack)-1] // pop
903-
lpkg.color = black
904919

905920
return lpkg.needsrc
906921
}
907922

908923
// For each initial package, create its import DAG.
909924
for _, lpkg := range initial {
910-
visit(lpkg)
925+
visit(nil, lpkg)
911926
}
912927

913928
} else {
@@ -920,16 +935,45 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
920935

921936
// Load type data and syntax if needed, starting at
922937
// the initial packages (roots of the import DAG).
923-
if ld.Mode&(NeedTypes|NeedSyntax|NeedTypesInfo) != 0 {
924-
var wg sync.WaitGroup
925-
for _, lpkg := range initial {
926-
wg.Add(1)
927-
go func(lpkg *loaderPackage) {
928-
ld.loadRecursive(lpkg)
929-
wg.Done()
930-
}(lpkg)
938+
if ld.Mode&(NeedSyntax|NeedTypes|NeedTypesInfo) != 0 {
939+
940+
// We avoid using g.SetLimit to limit concurrency as
941+
// it makes g.Go stop accepting work, which prevents
942+
// workers from enqeuing, and thus finishing, and thus
943+
// allowing the group to make progress: deadlock.
944+
//
945+
// Instead we use the ioLimit and cpuLimit semaphores.
946+
g, _ := errgroup.WithContext(ld.Context)
947+
948+
// enqueues adds a package to the type-checking queue.
949+
// It must have no unfinished successors.
950+
var enqueue func(*loaderPackage)
951+
enqueue = func(lpkg *loaderPackage) {
952+
g.Go(func() error {
953+
// Parse and type-check.
954+
ld.loadPackage(lpkg)
955+
956+
// Notify each waiting predecessor,
957+
// and enqueue it when it becomes a leaf.
958+
for _, pred := range lpkg.preds {
959+
if pred.unfinishedSuccs.Add(-1) == 0 { // decref
960+
enqueue(pred)
961+
}
962+
}
963+
964+
return nil
965+
})
966+
}
967+
968+
// Load leaves first, adding new packages
969+
// to the queue as they become leaves.
970+
for _, leaf := range leaves {
971+
enqueue(leaf)
972+
}
973+
974+
if err := g.Wait(); err != nil {
975+
return nil, err // cancelled
931976
}
932-
wg.Wait()
933977
}
934978

935979
// If the context is done, return its error and
@@ -976,7 +1020,7 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
9761020
if ld.requestedMode&NeedSyntax == 0 {
9771021
ld.pkgs[i].Syntax = nil
9781022
}
979-
if ld.requestedMode&NeedTypes == 0 && ld.requestedMode&NeedSyntax == 0 {
1023+
if ld.requestedMode&(NeedSyntax|NeedTypes|NeedTypesInfo) == 0 {
9801024
ld.pkgs[i].Fset = nil
9811025
}
9821026
if ld.requestedMode&NeedTypesInfo == 0 {
@@ -993,31 +1037,10 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
9931037
return result, nil
9941038
}
9951039

996-
// loadRecursive loads the specified package and its dependencies,
997-
// recursively, in parallel, in topological order.
998-
// It is atomic and idempotent.
999-
// Precondition: ld.Mode&NeedTypes.
1000-
func (ld *loader) loadRecursive(lpkg *loaderPackage) {
1001-
lpkg.loadOnce.Do(func() {
1002-
// Load the direct dependencies, in parallel.
1003-
var wg sync.WaitGroup
1004-
for _, ipkg := range lpkg.Imports {
1005-
imp := ld.pkgs[ipkg.ID]
1006-
wg.Add(1)
1007-
go func(imp *loaderPackage) {
1008-
ld.loadRecursive(imp)
1009-
wg.Done()
1010-
}(imp)
1011-
}
1012-
wg.Wait()
1013-
ld.loadPackage(lpkg)
1014-
})
1015-
}
1016-
1017-
// loadPackage loads the specified package.
1040+
// loadPackage loads/parses/typechecks the specified package.
10181041
// It must be called only once per Package,
10191042
// after immediate dependencies are loaded.
1020-
// Precondition: ld.Mode & NeedTypes.
1043+
// Precondition: ld.Mode&(NeedSyntax|NeedTypes|NeedTypesInfo) != 0.
10211044
func (ld *loader) loadPackage(lpkg *loaderPackage) {
10221045
if lpkg.PkgPath == "unsafe" {
10231046
// Fill in the blanks to avoid surprises.
@@ -1053,6 +1076,10 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
10531076
if !lpkg.needtypes && !lpkg.needsrc {
10541077
return
10551078
}
1079+
1080+
// TODO(adonovan): this condition looks wrong:
1081+
// I think it should be lpkg.needtypes && !lpg.needsrc,
1082+
// so that NeedSyntax without NeedTypes can be satisfied by export data.
10561083
if !lpkg.needsrc {
10571084
if err := ld.loadFromExportData(lpkg); err != nil {
10581085
lpkg.Errors = append(lpkg.Errors, Error{
@@ -1169,15 +1196,19 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
11691196
return
11701197
}
11711198

1172-
lpkg.TypesInfo = &types.Info{
1173-
Types: make(map[ast.Expr]types.TypeAndValue),
1174-
Defs: make(map[*ast.Ident]types.Object),
1175-
Uses: make(map[*ast.Ident]types.Object),
1176-
Implicits: make(map[ast.Node]types.Object),
1177-
Instances: make(map[*ast.Ident]types.Instance),
1178-
Scopes: make(map[ast.Node]*types.Scope),
1179-
Selections: make(map[*ast.SelectorExpr]*types.Selection),
1180-
FileVersions: make(map[*ast.File]string),
1199+
// Populate TypesInfo only if needed, as it
1200+
// causes the type checker to work much harder.
1201+
if ld.Config.Mode&NeedTypesInfo != 0 {
1202+
lpkg.TypesInfo = &types.Info{
1203+
Types: make(map[ast.Expr]types.TypeAndValue),
1204+
Defs: make(map[*ast.Ident]types.Object),
1205+
Uses: make(map[*ast.Ident]types.Object),
1206+
Implicits: make(map[ast.Node]types.Object),
1207+
Instances: make(map[*ast.Ident]types.Instance),
1208+
Scopes: make(map[ast.Node]*types.Scope),
1209+
Selections: make(map[*ast.SelectorExpr]*types.Selection),
1210+
FileVersions: make(map[*ast.File]string),
1211+
}
11811212
}
11821213
lpkg.TypesSizes = ld.sizes
11831214

@@ -1231,6 +1262,10 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
12311262
}
12321263
}
12331264

1265+
// Type-checking is CPU intensive.
1266+
cpuLimit <- unit{} // acquire a token
1267+
defer func() { <-cpuLimit }() // release a token
1268+
12341269
typErr := types.NewChecker(tc, ld.Fset, lpkg.Types, lpkg.TypesInfo).Files(lpkg.Syntax)
12351270
lpkg.importErrors = nil // no longer needed
12361271

@@ -1295,8 +1330,11 @@ type importerFunc func(path string) (*types.Package, error)
12951330
func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
12961331

12971332
// We use a counting semaphore to limit
1298-
// the number of parallel I/O calls per process.
1299-
var ioLimit = make(chan bool, 20)
1333+
// the number of parallel I/O calls or CPU threads per process.
1334+
var (
1335+
ioLimit = make(chan unit, 20)
1336+
cpuLimit = make(chan unit, runtime.GOMAXPROCS(0))
1337+
)
13001338

13011339
func (ld *loader) parseFile(filename string) (*ast.File, error) {
13021340
ld.parseCacheMu.Lock()
@@ -1324,14 +1362,17 @@ func (ld *loader) parseFile(filename string) (*ast.File, error) {
13241362
}
13251363
var err error
13261364
if src == nil {
1327-
ioLimit <- true // acquire a token
1365+
ioLimit <- unit{} // acquire a token
13281366
src, err = os.ReadFile(filename)
13291367
<-ioLimit // release a token
13301368
}
13311369
if err != nil {
13321370
v.err = err
13331371
} else {
1372+
// Parsing is CPU intensive.
1373+
cpuLimit <- unit{} // acquire a token
13341374
v.f, v.err = ld.ParseFile(ld.Fset, filename, src)
1375+
<-cpuLimit // release a token
13351376
}
13361377

13371378
close(v.ready)
@@ -1531,4 +1572,4 @@ func usesExportData(cfg *Config) bool {
15311572
return cfg.Mode&NeedExportFile != 0 || cfg.Mode&NeedTypes != 0 && cfg.Mode&NeedDeps == 0
15321573
}
15331574

1534-
var _ interface{} = io.Discard // assert build toolchain is go1.16 or later
1575+
type unit struct{}

0 commit comments

Comments
 (0)