Skip to content

Commit 5fef1f2

Browse files
committed
gopls/internal/telemetry/cmd/stacks: add cmd/compile support to readPCLineTable
Building the compiler is actually simpler than gopls, since GOTOOLCHAIN is all you need. No need to explicitly git clone anything. Most of this is just minor refactoring to avoid hard-coding gopls details. Updates golang/go#71045. Change-Id: I6a6a636c5d950cec713e358dfd4dddcbd07554fc Reviewed-on: https://go-review.googlesource.com/c/tools/+/642418 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 1335f05 commit 5fef1f2

File tree

2 files changed

+147
-43
lines changed

2 files changed

+147
-43
lines changed

gopls/internal/telemetry/cmd/stacks/stacks.go

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ func main() {
184184
distinctStacks++
185185

186186
info := Info{
187-
Program: prog.Program,
188-
Version: prog.Version,
189-
GoVersion: prog.GoVersion,
190-
GOOS: prog.GOOS,
191-
GOARCH: prog.GOARCH,
192-
Client: clientSuffix,
187+
Program: prog.Program,
188+
ProgramVersion: prog.Version,
189+
GoVersion: prog.GoVersion,
190+
GOOS: prog.GOOS,
191+
GOARCH: prog.GOARCH,
192+
Client: clientSuffix,
193193
}
194194
for stack, count := range prog.Stacks {
195195
counts := stacks[stack]
@@ -432,15 +432,15 @@ func main() {
432432
// Info is used as a key for de-duping and aggregating.
433433
// Do not add detail about particular records (e.g. data, telemetry URL).
434434
type Info struct {
435-
Program string // "golang.org/x/tools/gopls"
436-
Version, GoVersion string // e.g. "gopls/v0.16.1", "go1.23"
437-
GOOS, GOARCH string
438-
Client string // e.g. "vscode"
435+
Program string // "golang.org/x/tools/gopls"
436+
ProgramVersion, GoVersion string // e.g. "v0.16.1", "go1.23"
437+
GOOS, GOARCH string
438+
Client string // e.g. "vscode"
439439
}
440440

441441
func (info Info) String() string {
442442
return fmt.Sprintf("%s@%s %s %s/%s %s",
443-
info.Program, info.Version,
443+
info.Program, info.ProgramVersion,
444444
info.GoVersion, info.GOOS, info.GOARCH,
445445
info.Client)
446446
}
@@ -543,7 +543,7 @@ func writeStackComment(body *bytes.Buffer, stack, id string, jsonURL string, cou
543543
id, jsonURL)
544544

545545
// Read the mapping from symbols to file/line.
546-
pclntab, err := readPCLineTable(info)
546+
pclntab, err := readPCLineTable(info, defaultStacksDir)
547547
if err != nil {
548548
log.Fatal(err)
549549
}
@@ -631,7 +631,7 @@ func frameURL(pclntab map[string]FileLine, info Info, frame string) string {
631631
}
632632

633633
return fmt.Sprintf("https://cs.opensource.google/go/x/tools/+/%s:%s;l=%d",
634-
"gopls/"+info.Version, rest, linenum)
634+
"gopls/"+info.ProgramVersion, rest, linenum)
635635
}
636636

637637
// other x/ module dependency?
@@ -770,63 +770,90 @@ type FileLine struct {
770770
line int
771771
}
772772

773+
const defaultStacksDir = "/tmp/stacks-cache"
774+
773775
// readPCLineTable builds the gopls executable specified by info,
774776
// reads its PC-to-line-number table, and returns the file/line of
775777
// each TEXT symbol.
776-
func readPCLineTable(info Info) (map[string]FileLine, error) {
778+
//
779+
// stacksDir is a semi-durable temp directory (i.e. lasts for at least a few
780+
// hours) to hold recent sources and executables.
781+
func readPCLineTable(info Info, stacksDir string) (map[string]FileLine, error) {
777782
// The stacks dir will be a semi-durable temp directory
778783
// (i.e. lasts for at least hours) holding source trees
779784
// and executables we have built recently.
780785
//
781786
// Each subdir will hold a specific revision.
782-
stacksDir := "/tmp/gopls-stacks"
783787
if err := os.MkdirAll(stacksDir, 0777); err != nil {
784788
return nil, fmt.Errorf("can't create stacks dir: %v", err)
785789
}
786790

787-
// Fetch the source for the tools repo,
788-
// shallow-cloning just the desired revision.
789-
// (Skip if it's already cloned.)
790-
revDir := filepath.Join(stacksDir, info.Version)
791-
if !fileExists(filepath.Join(revDir, "go.mod")) {
792-
// We check for presence of the go.mod file,
793-
// not just the directory itself, as the /tmp reaper
794-
// often removes stale files before removing their directories.
795-
// Remove those stale directories now.
796-
_ = os.RemoveAll(revDir) // ignore errors
797-
798-
log.Printf("cloning tools@gopls/%s", info.Version)
799-
if err := shallowClone(revDir, "https://go.googlesource.com/tools", "gopls/"+info.Version); err != nil {
791+
// When building a subrepo tool, we must clone the source of the
792+
// subrepo, and run go build from that checkout.
793+
//
794+
// When building a main repo tool, no need to clone or change
795+
// directories. GOTOOLCHAIN is sufficient to fetch and build the
796+
// appropriate version.
797+
var buildDir string
798+
switch info.Program {
799+
case "golang.org/x/tools/gopls":
800+
// Fetch the source for the tools repo,
801+
// shallow-cloning just the desired revision.
802+
// (Skip if it's already cloned.)
803+
revDir := filepath.Join(stacksDir, info.ProgramVersion)
804+
if !fileExists(filepath.Join(revDir, "go.mod")) {
805+
// We check for presence of the go.mod file,
806+
// not just the directory itself, as the /tmp reaper
807+
// often removes stale files before removing their directories.
808+
// Remove those stale directories now.
800809
_ = os.RemoveAll(revDir) // ignore errors
801-
return nil, fmt.Errorf("clone: %v", err)
810+
811+
log.Printf("cloning tools@gopls/%s", info.ProgramVersion)
812+
if err := shallowClone(revDir, "https://go.googlesource.com/tools", "gopls/"+info.ProgramVersion); err != nil {
813+
_ = os.RemoveAll(revDir) // ignore errors
814+
return nil, fmt.Errorf("clone: %v", err)
815+
}
802816
}
817+
818+
// gopls is in its own module, we must build from there.
819+
buildDir = filepath.Join(revDir, "gopls")
820+
case "cmd/compile":
821+
// Nothing to do, GOTOOLCHAIN is sufficient.
822+
default:
823+
return nil, fmt.Errorf("don't know how to build unknown program %s", info.Program)
803824
}
804825

826+
// No slashes in file name.
827+
escapedProg := strings.Replace(info.Program, "/", "_", -1)
828+
805829
// Build the executable with the correct GOTOOLCHAIN, GOOS, GOARCH.
806830
// Use -trimpath for normalized file names.
807831
// (Skip if it's already built.)
808-
exe := fmt.Sprintf("exe-%s.%s-%s", info.GoVersion, info.GOOS, info.GOARCH)
809-
cmd := exec.Command("go", "build", "-trimpath", "-o", "../"+exe)
810-
cmd.Stderr = os.Stderr
811-
cmd.Dir = filepath.Join(revDir, "gopls")
812-
cmd.Env = append(os.Environ(),
813-
"GOTOOLCHAIN="+info.GoVersion,
814-
"GOOS="+info.GOOS,
815-
"GOARCH="+info.GOARCH,
816-
)
817-
if !fileExists(filepath.Join(revDir, exe)) {
832+
exe := fmt.Sprintf("exe-%s-%s.%s-%s", escapedProg, info.GoVersion, info.GOOS, info.GOARCH)
833+
exe = filepath.Join(stacksDir, exe)
834+
835+
if !fileExists(exe) {
818836
log.Printf("building %s@%s with %s for %s/%s",
819-
info.Program, info.Version, info.GoVersion, info.GOOS, info.GOARCH)
837+
info.Program, info.ProgramVersion, info.GoVersion, info.GOOS, info.GOARCH)
838+
839+
cmd := exec.Command("go", "build", "-trimpath", "-o", exe, info.Program)
840+
cmd.Stderr = os.Stderr
841+
cmd.Dir = buildDir
842+
cmd.Env = append(os.Environ(),
843+
"GOTOOLCHAIN="+info.GoVersion,
844+
"GOOS="+info.GOOS,
845+
"GOARCH="+info.GOARCH,
846+
"GOWORK=off",
847+
)
820848
if err := cmd.Run(); err != nil {
821-
return nil, fmt.Errorf("building: %v (rm -fr /tmp/gopls-stacks?)", err)
849+
return nil, fmt.Errorf("building: %v (rm -fr %s?)", err, stacksDir)
822850
}
823851
}
824852

825853
// Read pclntab of executable.
826-
cmd = exec.Command("go", "tool", "objdump", exe)
854+
cmd := exec.Command("go", "tool", "objdump", exe)
827855
cmd.Stdout = new(strings.Builder)
828856
cmd.Stderr = os.Stderr
829-
cmd.Dir = revDir
830857
cmd.Env = append(os.Environ(),
831858
"GOTOOLCHAIN="+info.GoVersion,
832859
"GOOS="+info.GOOS,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux || darwin
6+
7+
package main
8+
9+
import (
10+
"testing"
11+
)
12+
13+
func TestReadPCLineTable(t *testing.T) {
14+
if testing.Short() {
15+
// TODO(prattmic): It would be nice to have a unit test that
16+
// didn't require downloading.
17+
t.Skip("downloads source from the internet, skipping in -short")
18+
}
19+
20+
type testCase struct {
21+
name string
22+
info Info
23+
wantSymbol string
24+
wantFileLine FileLine
25+
}
26+
27+
tests := []testCase{
28+
{
29+
name: "gopls",
30+
info: Info{
31+
Program: "golang.org/x/tools/gopls",
32+
ProgramVersion: "v0.16.1",
33+
GoVersion: "go1.23.4",
34+
GOOS: "linux",
35+
GOARCH: "amd64",
36+
},
37+
wantSymbol: "golang.org/x/tools/gopls/internal/cmd.(*Application).Run",
38+
wantFileLine: FileLine{
39+
file: "golang.org/x/tools/gopls/internal/cmd/cmd.go",
40+
line: 230,
41+
},
42+
},
43+
{
44+
name: "compile",
45+
info: Info{
46+
Program: "cmd/compile",
47+
ProgramVersion: "go1.23.4",
48+
GoVersion: "go1.23.4",
49+
GOOS: "linux",
50+
GOARCH: "amd64",
51+
},
52+
wantSymbol: "runtime.main",
53+
wantFileLine: FileLine{
54+
file: "runtime/proc.go",
55+
line: 147,
56+
},
57+
},
58+
}
59+
for _, tc := range tests {
60+
t.Run(tc.name, func(t *testing.T) {
61+
stacksDir := t.TempDir()
62+
pcln, err := readPCLineTable(tc.info, stacksDir)
63+
if err != nil {
64+
t.Fatalf("readPCLineTable got err %v want nil", err)
65+
}
66+
67+
got, ok := pcln[tc.wantSymbol]
68+
if !ok {
69+
t.Fatalf("PCLineTable want entry %s got !ok from pcln %+v", tc.wantSymbol, pcln)
70+
}
71+
72+
if got != tc.wantFileLine {
73+
t.Fatalf("symbol %s got FileLine %+v want %+v", tc.wantSymbol, got, tc.wantFileLine)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)