Skip to content

Commit 9b2c3f9

Browse files
committed
go/types/objectpath: avoid sorting methods for gopls
Gopls can rely on a deterministic ordering of methods, and therefore does not need to sort in the objectpath algorithm, which turns out to be enormously expensive on certain repositories (see golang/go#61178). Add hooks to skip method sorting during encode/decode, and use this for analysis facts. This CL is intentionally surgical, to avoid unintended side-effects for this change in behavior. Notably, other uses of objectpath are unaffected. For golang/go#61178 Change-Id: If915dab45b837e23ae5b841e3a9367aa0b53df89 Reviewed-on: https://go-review.googlesource.com/c/tools/+/511216 Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent ebb9ee3 commit 9b2c3f9

File tree

6 files changed

+103
-32
lines changed

6 files changed

+103
-32
lines changed

Diff for: go/analysis/unitchecker/unitchecker.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
294294
}
295295
return nil, nil // no .vetx file, no facts
296296
}
297-
facts, err := facts.NewDecoder(pkg).Decode(read)
297+
facts, err := facts.NewDecoder(pkg).Decode(false, read)
298298
if err != nil {
299299
return nil, err
300300
}
@@ -393,7 +393,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
393393
results[i].diagnostics = act.diagnostics
394394
}
395395

396-
data := facts.Encode()
396+
data := facts.Encode(false)
397397
if err := ioutil.WriteFile(cfg.VetxOutput, data, 0666); err != nil {
398398
return nil, fmt.Errorf("failed to write analysis facts: %v", err)
399399
}

Diff for: go/types/objectpath/objectpath.go

+67-19
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sort"
3030
"strconv"
3131
"strings"
32+
_ "unsafe"
3233

3334
"golang.org/x/tools/internal/typeparams"
3435
)
@@ -121,8 +122,17 @@ func For(obj types.Object) (Path, error) {
121122
// An Encoder amortizes the cost of encoding the paths of multiple objects.
122123
// The zero value of an Encoder is ready to use.
123124
type Encoder struct {
124-
scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects
125-
namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods()
125+
scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects
126+
namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods()
127+
skipMethodSorting bool
128+
}
129+
130+
// Exposed to gopls via golang.org/x/tools/internal/typesinternal
131+
// TODO(golang/go#61443): eliminate this parameter one way or the other.
132+
//
133+
//go:linkname skipMethodSorting
134+
func skipMethodSorting(enc *Encoder) {
135+
enc.skipMethodSorting = true
126136
}
127137

128138
// For returns the path to an object relative to its package,
@@ -314,16 +324,31 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
314324
// Inspect declared methods of defined types.
315325
if T, ok := o.Type().(*types.Named); ok {
316326
path = append(path, opType)
317-
// Note that method index here is always with respect
318-
// to canonical ordering of methods, regardless of how
319-
// they appear in the underlying type.
320-
for i, m := range enc.namedMethods(T) {
321-
path2 := appendOpArg(path, opMethod, i)
322-
if m == obj {
323-
return Path(path2), nil // found declared method
327+
if !enc.skipMethodSorting {
328+
// Note that method index here is always with respect
329+
// to canonical ordering of methods, regardless of how
330+
// they appear in the underlying type.
331+
for i, m := range enc.namedMethods(T) {
332+
path2 := appendOpArg(path, opMethod, i)
333+
if m == obj {
334+
return Path(path2), nil // found declared method
335+
}
336+
if r := find(obj, m.Type(), append(path2, opType), nil); r != nil {
337+
return Path(r), nil
338+
}
324339
}
325-
if r := find(obj, m.Type(), append(path2, opType), nil); r != nil {
326-
return Path(r), nil
340+
} else {
341+
// This branch must match the logic in the branch above, using go/types
342+
// APIs without sorting.
343+
for i := 0; i < T.NumMethods(); i++ {
344+
m := T.Method(i)
345+
path2 := appendOpArg(path, opMethod, i)
346+
if m == obj {
347+
return Path(path2), nil // found declared method
348+
}
349+
if r := find(obj, m.Type(), append(path2, opType), nil); r != nil {
350+
return Path(r), nil
351+
}
327352
}
328353
}
329354
}
@@ -418,10 +443,23 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) {
418443
path := make([]byte, 0, len(name)+8)
419444
path = append(path, name...)
420445
path = append(path, opType)
421-
for i, m := range enc.namedMethods(named) {
422-
if m == meth {
423-
path = appendOpArg(path, opMethod, i)
424-
return Path(path), true
446+
447+
if !enc.skipMethodSorting {
448+
for i, m := range enc.namedMethods(named) {
449+
if m == meth {
450+
path = appendOpArg(path, opMethod, i)
451+
return Path(path), true
452+
}
453+
}
454+
} else {
455+
// This branch must match the logic of the branch above, using go/types
456+
// APIs without sorting.
457+
for i := 0; i < named.NumMethods(); i++ {
458+
m := named.Method(i)
459+
if m == meth {
460+
path = appendOpArg(path, opMethod, i)
461+
return Path(path), true
462+
}
425463
}
426464
}
427465

@@ -534,6 +572,12 @@ func findTypeParam(obj types.Object, list *typeparams.TypeParamList, path []byte
534572

535573
// Object returns the object denoted by path p within the package pkg.
536574
func Object(pkg *types.Package, p Path) (types.Object, error) {
575+
return object(pkg, p, false)
576+
}
577+
578+
// Note: the skipMethodSorting parameter must match the value of
579+
// Encoder.skipMethodSorting used during encoding.
580+
func object(pkg *types.Package, p Path, skipMethodSorting bool) (types.Object, error) {
537581
if p == "" {
538582
return nil, fmt.Errorf("empty path")
539583
}
@@ -697,11 +741,15 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
697741
obj = t.Method(index) // Id-ordered
698742

699743
case *types.Named:
700-
methods := namedMethods(t) // (unmemoized)
701-
if index >= len(methods) {
702-
return nil, fmt.Errorf("method index %d out of range [0-%d)", index, len(methods))
744+
if index >= t.NumMethods() {
745+
return nil, fmt.Errorf("method index %d out of range [0-%d)", index, t.NumMethods())
746+
}
747+
if skipMethodSorting {
748+
obj = t.Method(index)
749+
} else {
750+
methods := namedMethods(t) // (unmemoized)
751+
obj = methods[index] // Id-ordered
703752
}
704-
obj = methods[index] // Id-ordered
705753

706754
default:
707755
return nil, fmt.Errorf("cannot apply %q to %s (got %T, want interface or named)", code, t, t)

Diff for: gopls/internal/lsp/cache/analysis.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1172,7 +1172,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
11721172
// of PkgPaths and objectpaths.)
11731173

11741174
// Read and decode analysis facts for each direct import.
1175-
factset, err := pkg.factsDecoder.Decode(func(pkgPath string) ([]byte, error) {
1175+
factset, err := pkg.factsDecoder.Decode(true, func(pkgPath string) ([]byte, error) {
11761176
if !hasFacts {
11771177
return nil, nil // analyzer doesn't use facts, so no vdeps
11781178
}
@@ -1314,7 +1314,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
13141314
panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact))
13151315
}
13161316

1317-
factsdata := factset.Encode()
1317+
factsdata := factset.Encode(true)
13181318
return result, &actionSummary{
13191319
Diagnostics: diagnostics,
13201320
Facts: factsdata,

Diff for: internal/facts/facts.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848

4949
"golang.org/x/tools/go/analysis"
5050
"golang.org/x/tools/go/types/objectpath"
51+
"golang.org/x/tools/internal/typesinternal"
5152
)
5253

5354
const debug = false
@@ -204,7 +205,9 @@ type GetPackageFunc = func(pkgPath string) *types.Package
204205
//
205206
// Concurrent calls to Decode are safe, so long as the
206207
// [GetPackageFunc] (if any) is also concurrency-safe.
207-
func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error) {
208+
//
209+
// TODO(golang/go#61443): eliminate skipMethodSorting one way or the other.
210+
func (d *Decoder) Decode(skipMethodSorting bool, read func(pkgPath string) ([]byte, error)) (*Set, error) {
208211
// Read facts from imported packages.
209212
// Facts may describe indirectly imported packages, or their objects.
210213
m := make(map[key]analysis.Fact) // one big bucket
@@ -244,7 +247,7 @@ func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error
244247
key := key{pkg: factPkg, t: reflect.TypeOf(f.Fact)}
245248
if f.Object != "" {
246249
// object fact
247-
obj, err := objectpath.Object(factPkg, f.Object)
250+
obj, err := typesinternal.ObjectpathObject(factPkg, f.Object, skipMethodSorting)
248251
if err != nil {
249252
// (most likely due to unexported object)
250253
// TODO(adonovan): audit for other possibilities.
@@ -268,7 +271,11 @@ func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error
268271
//
269272
// It may fail if one of the Facts could not be gob-encoded, but this is
270273
// a sign of a bug in an Analyzer.
271-
func (s *Set) Encode() []byte {
274+
func (s *Set) Encode(skipMethodSorting bool) []byte {
275+
encoder := new(objectpath.Encoder)
276+
if skipMethodSorting {
277+
typesinternal.SkipEncoderMethodSorting(encoder)
278+
}
272279

273280
// TODO(adonovan): opt: use a more efficient encoding
274281
// that avoids repeating PkgPath for each fact.
@@ -283,7 +290,7 @@ func (s *Set) Encode() []byte {
283290
}
284291
var object objectpath.Path
285292
if k.obj != nil {
286-
path, err := objectpath.For(k.obj)
293+
path, err := encoder.For(k.obj)
287294
if err != nil {
288295
if debug {
289296
log.Printf("discarding fact %s about %s\n", fact, k.obj)

Diff for: internal/facts/facts_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups)
311311
}
312312

313313
// decode
314-
facts, err := facts.NewDecoder(pkg).Decode(read)
314+
facts, err := facts.NewDecoder(pkg).Decode(false, read)
315315
if err != nil {
316316
t.Fatalf("Decode failed: %v", err)
317317
}
@@ -345,7 +345,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups)
345345
}
346346

347347
// encode
348-
factmap[pkg.Path()] = facts.Encode()
348+
factmap[pkg.Path()] = facts.Encode(false)
349349
}
350350
}
351351

@@ -413,7 +413,7 @@ func TestFactFilter(t *testing.T) {
413413
}
414414

415415
obj := pkg.Scope().Lookup("A")
416-
s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil })
416+
s, err := facts.NewDecoder(pkg).Decode(false, func(pkgPath string) ([]byte, error) { return nil, nil })
417417
if err != nil {
418418
t.Fatal(err)
419419
}
@@ -526,7 +526,7 @@ func TestMalformed(t *testing.T) {
526526
packages[pkg.Path()] = pkg
527527

528528
// decode facts
529-
facts, err := facts.NewDecoder(pkg).Decode(read)
529+
facts, err := facts.NewDecoder(pkg).Decode(false, read)
530530
if err != nil {
531531
t.Fatalf("Decode failed: %v", err)
532532
}
@@ -553,7 +553,7 @@ func TestMalformed(t *testing.T) {
553553
}
554554

555555
// encode facts
556-
factmap[pkg.Path()] = facts.Encode()
556+
factmap[pkg.Path()] = facts.Encode(false)
557557
}
558558
})
559559
}

Diff for: internal/typesinternal/types.go

+16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"go/types"
1212
"reflect"
1313
"unsafe"
14+
15+
"golang.org/x/tools/go/types/objectpath"
1416
)
1517

1618
func SetUsesCgo(conf *types.Config) bool {
@@ -50,3 +52,17 @@ func ReadGo116ErrorData(err types.Error) (code ErrorCode, start, end token.Pos,
5052
}
5153

5254
var SetGoVersion = func(conf *types.Config, version string) bool { return false }
55+
56+
// SkipEncoderMethodSorting marks the encoder as not requiring sorted methods,
57+
// as an optimization for gopls (which guarantees the order of parsed source files).
58+
//
59+
// TODO(golang/go#61443): eliminate this parameter one way or the other.
60+
//
61+
//go:linkname SkipEncoderMethodSorting golang.org/x/tools/go/types/objectpath.skipMethodSorting
62+
func SkipEncoderMethodSorting(enc *objectpath.Encoder)
63+
64+
// ObjectpathObject is like objectpath.Object, but allows suppressing method
65+
// sorting (which is not necessary for gopls).
66+
//
67+
//go:linkname ObjectpathObject golang.org/x/tools/go/types/objectpath.object
68+
func ObjectpathObject(pkg *types.Package, p objectpath.Path, skipMethodSorting bool) (types.Object, error)

0 commit comments

Comments
 (0)