Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize performance for binder, imports and packages #1702

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 43 additions & 24 deletions codegen/config/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"go/token"
"go/types"
"golang.org/x/tools/go/packages"

"github.com/99designs/gqlgen/codegen/templates"
"github.com/99designs/gqlgen/internal/code"
Expand All @@ -15,11 +16,12 @@ var ErrTypeNotFound = errors.New("unable to find type")

// Binder connects graphql types to golang types using static analysis
type Binder struct {
pkgs *code.Packages
schema *ast.Schema
cfg *Config
References []*TypeReference
SawInvalid bool
pkgs *code.Packages
schema *ast.Schema
cfg *Config
References []*TypeReference
SawInvalid bool
objectCache map[string]map[string]types.Object
}

func (c *Config) NewBinder() *Binder {
Expand Down Expand Up @@ -112,45 +114,62 @@ func (b *Binder) FindObject(pkgName string, typeName string) (types.Object, erro
if pkgName == "" {
return nil, fmt.Errorf("package cannot be nil")
}
fullName := typeName
if pkgName != "" {
fullName = pkgName + "." + typeName
}

pkg := b.pkgs.LoadWithTypes(pkgName)
if pkg == nil {
err := b.pkgs.Errors()
if err != nil {
return nil, fmt.Errorf("package could not be loaded: %s: %w", fullName, err)
return nil, fmt.Errorf("package could not be loaded: %s.%s: %w", pkgName, typeName, err)
}
return nil, fmt.Errorf("required package was not loaded: %s", fullName)
return nil, fmt.Errorf("required package was not loaded: %s.%s", pkgName, typeName)
}

if b.objectCache == nil {
b.objectCache = make(map[string]map[string]types.Object, b.pkgs.Count())
}

defsIndex, ok := b.objectCache[pkgName]
if !ok {
defsIndex = indexDefs(pkg)
b.objectCache[pkgName] = defsIndex
}

// function based marshalers take precedence
for astNode, def := range pkg.TypesInfo.Defs {
// only look at defs in the top scope
if def == nil || def.Parent() == nil || def.Parent() != pkg.Types.Scope() {
continue
}
if val, ok := defsIndex["Marshal"+typeName]; ok {
return val, nil
}

if astNode.Name == "Marshal"+typeName {
return def, nil
}
if val, ok := defsIndex[typeName]; ok {
return val, nil
}

// then look for types directly
return nil, fmt.Errorf("%w: %s.%s", ErrTypeNotFound, pkgName, typeName)
}

func indexDefs(pkg *packages.Package) map[string]types.Object {

res := make(map[string]types.Object)

scope := pkg.Types.Scope()
for astNode, def := range pkg.TypesInfo.Defs {
// only look at defs in the top scope
if def == nil || def.Parent() == nil || def.Parent() != pkg.Types.Scope() {
if def == nil {
continue
}
parent := def.Parent()
if parent == nil || parent != scope {
continue
}

if astNode.Name == typeName {
return def, nil
if _, ok := res[astNode.Name]; !ok {
// The above check may not be really needed, it is only here to have a consistent behavior with
// previous implementation of FindObject() function which only honored the first inclusion of a def.
// If this is still needed, we can consider something like sync.Map.LoadOrStore() to avoid two lookups.
res[astNode.Name] = def
}
}

return nil, fmt.Errorf("%w: %s", ErrTypeNotFound, fullName)
return res
}

func (b *Binder) PointerTo(ref *TypeReference) *TypeReference {
Expand Down
77 changes: 63 additions & 14 deletions internal/code/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ func NameForDir(dir string) string {
return SanitizePackageName(filepath.Base(dir))
}

type goModuleSearchResult struct {
path string
goModPath string
moduleName string
}

var goModuleRootCache = map[string]goModuleSearchResult{}

// goModuleRoot returns the root of the current go module if there is a go.mod file in the directory tree
// If not, it returns false
func goModuleRoot(dir string) (string, bool) {
Expand All @@ -53,28 +61,69 @@ func goModuleRoot(dir string) (string, bool) {
panic(err)
}
dir = filepath.ToSlash(dir)
modDir := dir
assumedPart := ""

var dirs = []string{dir}
result := goModuleSearchResult{}

for {
f, err := ioutil.ReadFile(filepath.Join(modDir, "go.mod"))
if err == nil {
// found it, stop searching
return string(modregex.FindSubmatch(f)[1]) + assumedPart, true
modDir := dirs[len(dirs)-1]

if val, ok := goModuleRootCache[dir]; ok {
result = val
break
}

assumedPart = "/" + filepath.Base(modDir) + assumedPart
parentDir, err := filepath.Abs(filepath.Join(modDir, ".."))
if err != nil {
panic(err)
if content, err := ioutil.ReadFile(filepath.Join(modDir, "go.mod")); err == nil {
moduleName := string(modregex.FindSubmatch(content)[1])
result = goModuleSearchResult{
path: moduleName,
goModPath: modDir,
moduleName: moduleName,
}
goModuleRootCache[modDir] = result
break
}

if parentDir == modDir {
// Walked all the way to the root and didnt find anything :'(
if modDir == "" || modDir == "." || modDir == "/" || strings.HasSuffix(modDir, "\\") {
// Reached the top of the file tree which means go.mod file is not found
// Set root folder with a sentinel cache value
goModuleRootCache[modDir] = result
break
}
modDir = parentDir

dirs = append(dirs, filepath.Dir(modDir))
}

// create a cache for each path in a tree traversed, except the top one as it is already cached
for _, d := range dirs[:len(dirs)-1] {
if result.moduleName == "" {
// go.mod is not found in the tree, so the same sentinel value fits all the directories in a tree
goModuleRootCache[d] = result
} else {
if relPath, err := filepath.Rel(result.goModPath, d); err != nil {
panic(err)
} else {
path := result.moduleName
relPath := filepath.ToSlash(relPath)
if !strings.HasSuffix(relPath, "/") {
path += "/"
}
path += relPath

goModuleRootCache[d] = goModuleSearchResult{
path: path,
goModPath: result.goModPath,
moduleName: result.moduleName,
}
}
}
}

res := goModuleRootCache[dir]
if res.moduleName == "" {
return "", false
}
return "", false
return res.path, true
}

// ImportPathForDir takes a path and returns a golang import path for the package
Expand Down
11 changes: 11 additions & 0 deletions internal/code/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func (p *Packages) addToCache(pkg *packages.Package) {

// Load works the same as LoadAll, except a single package at a time.
func (p *Packages) Load(importPath string) *packages.Package {
// Quick cache check first to avoid expensive allocations of LoadAll()
if p.packages != nil {
if pkg, ok := p.packages[importPath]; ok {
return pkg
}
}

pkgs := p.LoadAll(importPath)
if len(pkgs) == 0 {
return nil
Expand Down Expand Up @@ -183,6 +190,10 @@ func (p *Packages) Errors() PkgErrors {
return res
}

func (p *Packages) Count() int {
return len(p.packages)
}

type PkgErrors []error

func (p PkgErrors) Error() string {
Expand Down