go/internal/gcimporter: avoid setting unnecessary lines in fakeFileSet

As discussed in golang/go#46586, the gcimporter spends large portion of
time just validating fake lines.

Fix this by only setting the lines we need. Benchcmd results for
TestImportStdLib:

name              old time/op         new time/op         delta
TestImportStdLib          1.11s ±40%          0.57s ±58%  -48.17%  (p=0.000 n=18+18)

name              old user-time/op    new user-time/op    delta
TestImportStdLib          1.24s ±46%          0.71s ±61%  -42.84%  (p=0.000 n=18+18)

name              old sys-time/op     new sys-time/op     delta
TestImportStdLib          249ms ±64%          139ms ±47%  -44.19%  (p=0.000 n=19+18)

name              old peak-RSS-bytes  new peak-RSS-bytes  delta
TestImportStdLib         80.1MB ±24%         86.6MB ±13%   +8.06%  (p=0.001 n=20+20)

Fixes golang/go#46586

Change-Id: I5ef99ea9f242fc80d2d17750a8d9b28f7fc71245
Reviewed-on: https://go-review.googlesource.com/c/tools/+/357291
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
diff --git a/go/internal/gcimporter/bimport.go b/go/internal/gcimporter/bimport.go
index b023120..b85de01 100644
--- a/go/internal/gcimporter/bimport.go
+++ b/go/internal/gcimporter/bimport.go
@@ -74,9 +74,10 @@
 		pathList:   []string{""}, // empty string is mapped to 0
 		fake: fakeFileSet{
 			fset:  fset,
-			files: make(map[string]*token.File),
+			files: make(map[string]*fileInfo),
 		},
 	}
+	defer p.fake.setLines() // set lines for files in fset
 
 	// read version info
 	var versionstr string
@@ -338,37 +339,49 @@
 // Synthesize a token.Pos
 type fakeFileSet struct {
 	fset  *token.FileSet
-	files map[string]*token.File
+	files map[string]*fileInfo
 }
 
+type fileInfo struct {
+	file     *token.File
+	lastline int
+}
+
+const maxlines = 64 * 1024
+
 func (s *fakeFileSet) pos(file string, line, column int) token.Pos {
 	// TODO(mdempsky): Make use of column.
 
-	// Since we don't know the set of needed file positions, we
-	// reserve maxlines positions per file.
-	const maxlines = 64 * 1024
+	// Since we don't know the set of needed file positions, we reserve maxlines
+	// positions per file. We delay calling token.File.SetLines until all
+	// positions have been calculated (by way of fakeFileSet.setLines), so that
+	// we can avoid setting unnecessary lines. See also golang/go#46586.
 	f := s.files[file]
 	if f == nil {
-		f = s.fset.AddFile(file, -1, maxlines)
+		f = &fileInfo{file: s.fset.AddFile(file, -1, maxlines)}
 		s.files[file] = f
-		// Allocate the fake linebreak indices on first use.
-		// TODO(adonovan): opt: save ~512KB using a more complex scheme?
-		fakeLinesOnce.Do(func() {
-			fakeLines = make([]int, maxlines)
-			for i := range fakeLines {
-				fakeLines[i] = i
-			}
-		})
-		f.SetLines(fakeLines)
 	}
-
 	if line > maxlines {
 		line = 1
 	}
+	if line > f.lastline {
+		f.lastline = line
+	}
 
-	// Treat the file as if it contained only newlines
-	// and column=1: use the line number as the offset.
-	return f.Pos(line - 1)
+	// Return a fake position assuming that f.file consists only of newlines.
+	return token.Pos(f.file.Base() + line - 1)
+}
+
+func (s *fakeFileSet) setLines() {
+	fakeLinesOnce.Do(func() {
+		fakeLines = make([]int, maxlines)
+		for i := range fakeLines {
+			fakeLines[i] = i
+		}
+	})
+	for _, f := range s.files {
+		f.file.SetLines(fakeLines[:f.lastline])
+	}
 }
 
 var (
diff --git a/go/internal/gcimporter/iimport.go b/go/internal/gcimporter/iimport.go
index 58d4e2b..806030d 100644
--- a/go/internal/gcimporter/iimport.go
+++ b/go/internal/gcimporter/iimport.go
@@ -159,9 +159,10 @@
 
 		fake: fakeFileSet{
 			fset:  fset,
-			files: make(map[string]*token.File),
+			files: make(map[string]*fileInfo),
 		},
 	}
+	defer p.fake.setLines() // set lines for files in fset
 
 	for i, pt := range predeclared() {
 		p.typCache[uint64(i)] = pt