Skip to content

Commit 00b8474

Browse files
committed
cmd/trace: don't filter events for profile by whether they have stack
Right now the profile-from-trace code blindly discards events that don't have a stack, but this means it can discard 'end' events for goroutine time ranges that don't have stacks, like when a goroutine exits a syscall. This means we drop stack samples we *do* have, because we correctly already only use the stack trace of the corresponding 'start' event for a time-range-of-interest anyway. This change means that some events will be tracked that have no stack in their start event, but that's fine. It won't end up in the profile anyway because the stack is empty! And the rest of the code appears to be robust to an empty stack already. Thank you to Rhys Hiltner for reporting this issue and for the reproducer, which I have worked into a test for this change. Fixes golang#74850. Change-Id: I943b97ecf6b82803e4a778a0f83a14473d32254e Reviewed-on: https://go-review.googlesource.com/c/go/+/694156 Reviewed-by: Rhys Hiltner <[email protected]> Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent e36c5ae commit 00b8474

File tree

4 files changed

+157
-16
lines changed

4 files changed

+157
-16
lines changed

src/cmd/trace/pprof.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,6 @@ func makeComputePprofFunc(state trace.GoState, trackReason func(string) bool) co
153153
if ev.Kind() != trace.EventStateTransition {
154154
continue
155155
}
156-
stack := ev.Stack()
157-
if stack == trace.NoStack {
158-
continue
159-
}
160156

161157
// The state transition has to apply to a goroutine.
162158
st := ev.StateTransition()

src/cmd/trace/pprof_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 main
6+
7+
import (
8+
"bytes"
9+
"net/http"
10+
"os"
11+
"runtime/trace"
12+
"strings"
13+
"testing"
14+
"testing/synctest"
15+
"time"
16+
17+
"internal/trace/testtrace"
18+
)
19+
20+
// Regression test for go.dev/issue/74850.
21+
func TestSyscallProfile74850(t *testing.T) {
22+
testtrace.MustHaveSyscallEvents(t)
23+
24+
var buf bytes.Buffer
25+
err := trace.Start(&buf)
26+
if err != nil {
27+
t.Fatalf("start tracing: %v", err)
28+
}
29+
30+
synctest.Test(t, func(t *testing.T) {
31+
go hidden1(t)
32+
go hidden2(t)
33+
go visible(t)
34+
synctest.Wait()
35+
time.Sleep(1 * time.Millisecond)
36+
synctest.Wait()
37+
})
38+
trace.Stop()
39+
40+
if t.Failed() {
41+
return
42+
}
43+
44+
parsed, err := parseTrace(&buf, int64(buf.Len()))
45+
if err != nil {
46+
t.Fatalf("parsing trace: %v", err)
47+
}
48+
49+
records, err := pprofByGoroutine(computePprofSyscall(), parsed)(&http.Request{})
50+
if err != nil {
51+
t.Fatalf("failed to generate pprof: %v\n", err)
52+
}
53+
54+
for _, r := range records {
55+
t.Logf("Record: n=%d, total=%v", r.Count, r.Time)
56+
for _, f := range r.Stack {
57+
t.Logf("\t%s", f.Func)
58+
t.Logf("\t\t%s:%d @ 0x%x", f.File, f.Line, f.PC)
59+
}
60+
}
61+
if len(records) == 0 {
62+
t.Error("empty profile")
63+
}
64+
65+
// Make sure we see the right frames.
66+
wantSymbols := []string{"cmd/trace.visible", "cmd/trace.hidden1", "cmd/trace.hidden2"}
67+
haveSymbols := make([]bool, len(wantSymbols))
68+
for _, r := range records {
69+
for _, f := range r.Stack {
70+
for i, s := range wantSymbols {
71+
if strings.Contains(f.Func, s) {
72+
haveSymbols[i] = true
73+
}
74+
}
75+
}
76+
}
77+
for i, have := range haveSymbols {
78+
if !have {
79+
t.Errorf("expected %s in syscall profile", wantSymbols[i])
80+
}
81+
}
82+
}
83+
84+
func stat(t *testing.T) {
85+
_, err := os.Stat(".")
86+
if err != nil {
87+
t.Errorf("os.Stat: %v", err)
88+
}
89+
}
90+
91+
func hidden1(t *testing.T) {
92+
stat(t)
93+
}
94+
95+
func hidden2(t *testing.T) {
96+
stat(t)
97+
stat(t)
98+
}
99+
100+
func visible(t *testing.T) {
101+
stat(t)
102+
time.Sleep(1 * time.Millisecond)
103+
}

src/go/build/deps_test.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -742,12 +742,6 @@ var depsRules = `
742742
FMT, encoding/binary, internal/trace/version, internal/trace/internal/tracev1, container/heap, math/rand
743743
< internal/trace;
744744
745-
regexp, internal/trace, internal/trace/raw, internal/txtar
746-
< internal/trace/testtrace;
747-
748-
regexp, internal/txtar, internal/trace, internal/trace/raw
749-
< internal/trace/internal/testgen;
750-
751745
# cmd/trace dependencies.
752746
FMT,
753747
embed,
@@ -792,14 +786,24 @@ var depsRules = `
792786
< testing/internal/testdeps;
793787
794788
# Test-only packages can have anything they want
795-
FMT, compress/gzip, embed, encoding/binary < encoding/json/internal/jsontest;
796-
CGO, internal/syscall/unix < net/internal/cgotest;
797-
FMT < math/big/internal/asmgen;
798789
799-
FMT, testing < internal/cgrouptest;
800-
C, CGO < internal/runtime/cgobench;
790+
FMT, compress/gzip, embed, encoding/binary
791+
< encoding/json/internal/jsontest;
792+
793+
CGO, internal/syscall/unix
794+
< net/internal/cgotest;
795+
796+
FMT, testing
797+
< internal/cgrouptest;
798+
799+
regexp, internal/trace, internal/trace/raw, internal/txtar, testing
800+
< internal/trace/testtrace;
801+
802+
C, CGO
803+
< internal/runtime/cgobench;
804+
805+
# Generate-only packages can have anything they want.
801806
802-
# Generate-only packages can have anything they want
803807
container/heap,
804808
encoding/binary,
805809
fmt,
@@ -812,6 +816,12 @@ var depsRules = `
812816
strings,
813817
sync
814818
< internal/runtime/gc/internal/gen;
819+
820+
regexp, internal/txtar, internal/trace, internal/trace/raw
821+
< internal/trace/internal/testgen;
822+
823+
FMT
824+
< math/big/internal/asmgen;
815825
`
816826

817827
// listStdPkgs returns the same list of packages as "go list std".
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 testtrace
6+
7+
import (
8+
"runtime"
9+
"testing"
10+
)
11+
12+
// MustHaveSyscallEvents skips the current test if the current
13+
// platform does not support true system call events.
14+
func MustHaveSyscallEvents(t *testing.T) {
15+
if HasSyscallEvents() {
16+
return
17+
}
18+
t.Skip("current platform has no true syscall events")
19+
}
20+
21+
// HasSyscallEvents returns true if the current platform
22+
// has real syscall events available.
23+
func HasSyscallEvents() bool {
24+
switch runtime.GOOS {
25+
case "js", "wasip1":
26+
// js and wasip1 emulate system calls by blocking on channels
27+
// while yielding back to the environment. They never actually
28+
// call entersyscall/exitsyscall.
29+
return false
30+
}
31+
return true
32+
}

0 commit comments

Comments
 (0)