Skip to content

Commit 6e8a193

Browse files
committed
gopls/internal/debug: integrate flight recorder
This change adds support to gopls for Flight Recorder, the new always-on version of the Go 1.25's runtime's event tracing. Simply visit the gopls debug page and hit Flight Recorder, and you'll immediately see the event trace for the past 30 seconds. Also, a test of basic functionality. Updates golang/go#66843 - in process http.Handler for trace Updates golang/go#63185 - flight recorder runtime API Change-Id: I6335e986990445014a51ed923b5ee7093c723fe9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/555716 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent 25caa76 commit 6e8a193

File tree

4 files changed

+221
-5
lines changed

4 files changed

+221
-5
lines changed

gopls/internal/debug/flight.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2024 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 go1.25
6+
7+
package debug
8+
9+
import (
10+
"bufio"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"os"
15+
"os/exec"
16+
"path/filepath"
17+
"runtime/trace"
18+
"strings"
19+
"sync"
20+
"time"
21+
)
22+
23+
// The FlightRecorder is a global resource, so create at most one per process.
24+
var getRecorder = sync.OnceValues(func() (*trace.FlightRecorder, error) {
25+
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
26+
// half a minute is usually enough to know "what just happened?"
27+
MinAge: 30 * time.Second,
28+
})
29+
if err := fr.Start(); err != nil {
30+
return nil, err
31+
}
32+
return fr, nil
33+
})
34+
35+
func startFlightRecorder() (http.HandlerFunc, error) {
36+
fr, err := getRecorder()
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
// Return a handler that writes the most recent flight record,
42+
// starts a trace viewer server, and redirects to it.
43+
return func(w http.ResponseWriter, r *http.Request) {
44+
errorf := func(format string, args ...any) {
45+
msg := fmt.Sprintf(format, args...)
46+
http.Error(w, msg, http.StatusInternalServerError)
47+
}
48+
49+
// Write the most recent flight record into a temp file.
50+
f, err := os.CreateTemp("", "flightrecord")
51+
if err != nil {
52+
errorf("can't create temp file for flight record: %v", err)
53+
return
54+
}
55+
if _, err := fr.WriteTo(f); err != nil {
56+
f.Close()
57+
errorf("failed to write flight record: %s", err)
58+
return
59+
}
60+
if err := f.Close(); err != nil {
61+
errorf("failed to close flight record: %s", err)
62+
return
63+
}
64+
tracefile, err := filepath.Abs(f.Name())
65+
if err != nil {
66+
errorf("can't absolutize name of trace file: %v", err)
67+
return
68+
}
69+
70+
// Run 'go tool trace' to start a new trace-viewer
71+
// web server process. It will run until gopls terminates.
72+
// (It would be nicer if we could just link it in; see #66843.)
73+
cmd := exec.Command("go", "tool", "trace", tracefile)
74+
75+
// Don't connect trace's std{out,err} to our os.Stderr directly,
76+
// otherwise the child may outlive the parent in tests,
77+
// and 'go test' will complain about unclosed pipes.
78+
// Instead, interpose a pipe that will close when gopls exits.
79+
// See CL 677262 for a better solution (a cmd/trace flag).
80+
// (#66843 is of course better still.)
81+
// Also, this notifies us of the server's readiness and URL.
82+
urlC := make(chan string)
83+
{
84+
r, w, err := os.Pipe()
85+
if err != nil {
86+
errorf("can't create pipe: %v", err)
87+
return
88+
}
89+
go func() {
90+
// Copy from the pipe to stderr,
91+
// keeping an eye out for the "listening on URL" string.
92+
scan := bufio.NewScanner(r)
93+
for scan.Scan() {
94+
line := scan.Text()
95+
if _, url, ok := strings.Cut(line, "Trace viewer is listening on "); ok {
96+
urlC <- url
97+
}
98+
fmt.Fprintln(os.Stderr, line)
99+
}
100+
if err := scan.Err(); err != nil {
101+
log.Printf("reading from pipe to cmd/trace: %v", err)
102+
}
103+
}()
104+
cmd.Stderr = w
105+
cmd.Stdout = w
106+
}
107+
108+
// Suppress the usual cmd/trace behavior of opening a new
109+
// browser tab by setting BROWSER to /usr/bin/true (a no-op).
110+
cmd.Env = append(os.Environ(), "BROWSER=true")
111+
if err := cmd.Start(); err != nil {
112+
errorf("failed to start trace server: %s", err)
113+
return
114+
}
115+
116+
select {
117+
case addr := <-urlC:
118+
// Success! Send a redirect to the new location.
119+
// (This URL bypasses the help screen at /.)
120+
http.Redirect(w, r, addr+"/trace?view=proc", 302)
121+
122+
case <-r.Context().Done():
123+
errorf("canceled")
124+
125+
case <-time.After(10 * time.Second):
126+
errorf("trace viewer failed to start", err)
127+
}
128+
}, nil
129+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 !go1.25
6+
7+
package debug
8+
9+
import (
10+
"errors"
11+
"net/http"
12+
)
13+
14+
func startFlightRecorder() (http.HandlerFunc, error) {
15+
return nil, errors.ErrUnsupported
16+
}

gopls/internal/debug/serve.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,13 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) {
438438
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
439439
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
440440
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
441+
442+
if h, err := startFlightRecorder(); err != nil {
443+
stdlog.Printf("failed to start flight recorder: %v", err) // e.g. go1.24
444+
} else {
445+
mux.HandleFunc("/flightrecorder", h)
446+
}
447+
441448
if i.prometheus != nil {
442449
mux.HandleFunc("/metrics/", i.prometheus.Serve)
443450
}
@@ -468,11 +475,8 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) {
468475
http.Error(w, "made a bug", http.StatusOK)
469476
})
470477

471-
if err := http.Serve(listener, mux); err != nil {
472-
event.Error(ctx, "Debug server failed", err)
473-
return
474-
}
475-
event.Log(ctx, "Debug server finished")
478+
err := http.Serve(listener, mux) // always non-nil
479+
event.Error(ctx, "Debug server failed", err)
476480
}()
477481
return i.listenedDebugAddress, nil
478482
}
@@ -650,6 +654,7 @@ body {
650654
<a href="/metrics">Metrics</a>
651655
<a href="/rpc">RPC</a>
652656
<a href="/trace">Trace</a>
657+
<a href="/flightrecorder">Flight recorder</a>
653658
<a href="/analysis">Analysis</a>
654659
<hr>
655660
<h1>{{template "title" .}}</h1>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
package web_test
6+
7+
import (
8+
"encoding/json"
9+
"runtime"
10+
"testing"
11+
12+
"golang.org/x/tools/gopls/internal/protocol"
13+
"golang.org/x/tools/gopls/internal/protocol/command"
14+
. "golang.org/x/tools/gopls/internal/test/integration"
15+
"golang.org/x/tools/internal/testenv"
16+
)
17+
18+
// TestFlightRecorder checks that the flight recorder is minimally functional.
19+
func TestFlightRecorder(t *testing.T) {
20+
// The usual UNIX mechanisms cause timely termination of the
21+
// cmd/trace process, but this doesn't happen on Windows,
22+
// leading to CI failures because of process->file locking.
23+
// Rather than invent a complex mechanism, skip the test:
24+
// this feature is only for gopls developers anyway.
25+
// Better long term solutions are CL 677262 and issue #66843.
26+
if runtime.GOOS == "windows" {
27+
t.Skip("not reliable on windows")
28+
}
29+
testenv.NeedsGo1Point(t, 25)
30+
31+
const files = `
32+
-- go.mod --
33+
module example.com
34+
35+
-- a/a.go --
36+
package a
37+
38+
const A = 1
39+
`
40+
41+
Run(t, files, func(t *testing.T, env *Env) {
42+
env.OpenFile("a/a.go")
43+
44+
// Start the debug server.
45+
var result command.DebuggingResult
46+
env.ExecuteCommand(&protocol.ExecuteCommandParams{
47+
Command: command.StartDebugging.String(),
48+
Arguments: []json.RawMessage{json.RawMessage("{}")}, // no args -> pick port
49+
}, &result)
50+
uri := result.URLs[0]
51+
t.Logf("StartDebugging: URLs[0] = %s", uri)
52+
53+
// Check the debug server page is sensible.
54+
doc1 := get(t, uri)
55+
checkMatch(t, true, doc1, "Gopls server information")
56+
checkMatch(t, true, doc1, `<a href="/flightrecorder">Flight recorder</a>`)
57+
58+
// "Click" the Flight Recorder link.
59+
// It should redirect to the web server
60+
// of a "go tool trace" process.
61+
// The resulting web page is entirely programmatic,
62+
// so we check for an arbitrary expected symbol.
63+
doc2 := get(t, uri+"/flightrecorder")
64+
checkMatch(t, true, doc2, `onTraceViewerImportFail`)
65+
})
66+
}

0 commit comments

Comments
 (0)