Skip to content

Commit 43ffe2a

Browse files
committed
runtime: add execution tracer v2 behind GOEXPERIMENT=exectracer2
This change mostly implements the design described in #60773 and includes a new scalable parser for the new trace format, available in internal/trace/v2. I'll leave this commit message short because this is clearly an enormous CL with a lot of detail. This change does not hook up the new tracer into cmd/trace yet. A follow-up CL will handle that. For #60773. Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-linux-amd64-longtest-race Change-Id: I5d2aca2cc07580ed3c76a9813ac48ec96b157de0 Reviewed-on: https://go-review.googlesource.com/c/go/+/494187 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent f7c5cbb commit 43ffe2a

File tree

100 files changed

+11790
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+11790
-67
lines changed

src/cmd/compile/internal/test/inl_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func TestIntendedInlining(t *testing.T) {
9393
"(*puintptr).set",
9494
"(*wbBuf).get1",
9595
"(*wbBuf).get2",
96+
97+
// Trace-related ones.
98+
"traceLocker.ok",
99+
"traceEnabled",
96100
},
97101
"runtime/internal/sys": {},
98102
"runtime/internal/math": {
@@ -249,6 +253,10 @@ func TestIntendedInlining(t *testing.T) {
249253
want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "TrailingZeros32")
250254
want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Bswap32")
251255
}
256+
if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "mips" || runtime.GOARCH == "mips64" || runtime.GOARCH == "ppc64" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
257+
// runtime/internal/atomic.Loaduintptr is only intrinsified on these platforms.
258+
want["runtime"] = append(want["runtime"], "traceAcquire")
259+
}
252260
if bits.UintSize == 64 {
253261
// mix is only defined on 64-bit architectures
254262
want["runtime"] = append(want["runtime"], "mix")

src/cmd/trace/annotations_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"context"
1212
"flag"
1313
"fmt"
14+
"internal/goexperiment"
1415
traceparser "internal/trace"
1516
"os"
1617
"reflect"
@@ -330,6 +331,9 @@ func TestAnalyzeAnnotationGC(t *testing.T) {
330331
// If savetraces flag is set, the captured trace will be saved in the named file.
331332
func traceProgram(t *testing.T, f func(), name string) error {
332333
t.Helper()
334+
if goexperiment.ExecTracer2 {
335+
t.Skip("skipping because test programs are covered elsewhere for the new tracer")
336+
}
333337
buf := new(bytes.Buffer)
334338
if err := trace.Start(buf); err != nil {
335339
return err

src/cmd/trace/trace_unix_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package main
99
import (
1010
"bytes"
1111
"cmd/internal/traceviewer"
12+
"internal/goexperiment"
1213
traceparser "internal/trace"
1314
"io"
1415
"runtime"
@@ -23,6 +24,9 @@ import (
2324
// that preexisted when the tracing started were not counted
2425
// as threads in syscall. See golang.org/issues/22574.
2526
func TestGoroutineInSyscall(t *testing.T) {
27+
if goexperiment.ExecTracer2 {
28+
t.Skip("skipping because this test is obsolete and incompatible with the new tracer")
29+
}
2630
// Start one goroutine blocked in syscall.
2731
//
2832
// TODO: syscall.Pipe used to cause the goroutine to

src/go/build/deps_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,12 +605,35 @@ var depsRules = `
605605
syscall
606606
< os/exec/internal/fdtest;
607607
608+
FMT
609+
< internal/diff, internal/txtar;
610+
608611
FMT, container/heap, math/rand
609612
< internal/trace;
610613
614+
# v2 execution trace parser.
611615
FMT
612-
< internal/diff, internal/txtar;
616+
< internal/trace/v2/event;
617+
618+
internal/trace/v2/event
619+
< internal/trace/v2/event/go122;
620+
621+
FMT, io, internal/trace/v2/event/go122
622+
< internal/trace/v2/version;
623+
624+
FMT, encoding/binary, internal/trace/v2/version
625+
< internal/trace/v2/raw;
626+
627+
FMT, encoding/binary, internal/trace/v2/version
628+
< internal/trace/v2;
629+
630+
regexp, internal/trace/v2, internal/trace/v2/raw, internal/txtar
631+
< internal/trace/v2/testtrace;
632+
633+
regexp, internal/txtar, internal/trace/v2, internal/trace/v2/raw
634+
< internal/trace/v2/internal/testgen/go122;
613635
636+
# Coverage.
614637
FMT, crypto/md5, encoding/binary, regexp, sort, text/tabwriter, unsafe,
615638
internal/coverage, internal/coverage/uleb128
616639
< internal/coverage/cmerge,

src/internal/goexperiment/exp_exectracer2_off.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/internal/goexperiment/exp_exectracer2_on.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/internal/goexperiment/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,8 @@ type Flags struct {
123123
// AllocHeaders enables a different, more efficient way for the GC to
124124
// manage heap metadata.
125125
AllocHeaders bool
126+
127+
// ExecTracer2 controls whether to use the new execution trace
128+
// implementation.
129+
ExecTracer2 bool
126130
}

src/internal/trace/v2/base.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
// Copyright 2023 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+
// This file contains data types that all implementations of the trace format
6+
// parser need to provide to the rest of the package.
7+
8+
package trace
9+
10+
import (
11+
"fmt"
12+
"strings"
13+
14+
"internal/trace/v2/event"
15+
"internal/trace/v2/event/go122"
16+
"internal/trace/v2/version"
17+
)
18+
19+
// maxArgs is the maximum number of arguments for "plain" events,
20+
// i.e. anything that could reasonably be represented as a Base.
21+
const maxArgs = 5
22+
23+
// baseEvent is the basic unprocessed event. This serves as a common
24+
// fundamental data structure across.
25+
type baseEvent struct {
26+
typ event.Type
27+
time Time
28+
args [maxArgs - 1]uint64
29+
}
30+
31+
// extra returns a slice representing extra available space in args
32+
// that the parser can use to pass data up into Event.
33+
func (e *baseEvent) extra(v version.Version) []uint64 {
34+
switch v {
35+
case version.Go122:
36+
return e.args[len(go122.Specs()[e.typ].Args)-1:]
37+
}
38+
panic(fmt.Sprintf("unsupported version: go 1.%d", v))
39+
}
40+
41+
// evTable contains the per-generation data necessary to
42+
// interpret an individual event.
43+
type evTable struct {
44+
freq frequency
45+
strings dataTable[stringID, string]
46+
stacks dataTable[stackID, stack]
47+
48+
// extraStrings are strings that get generated during
49+
// parsing but haven't come directly from the trace, so
50+
// they don't appear in strings.
51+
extraStrings []string
52+
extraStringIDs map[string]extraStringID
53+
nextExtra extraStringID
54+
}
55+
56+
// addExtraString adds an extra string to the evTable and returns
57+
// a unique ID for the string in the table.
58+
func (t *evTable) addExtraString(s string) extraStringID {
59+
if s == "" {
60+
return 0
61+
}
62+
if t.extraStringIDs == nil {
63+
t.extraStringIDs = make(map[string]extraStringID)
64+
}
65+
if id, ok := t.extraStringIDs[s]; ok {
66+
return id
67+
}
68+
t.nextExtra++
69+
id := t.nextExtra
70+
t.extraStrings = append(t.extraStrings, s)
71+
t.extraStringIDs[s] = id
72+
return id
73+
}
74+
75+
// getExtraString returns the extra string for the provided ID.
76+
// The ID must have been produced by addExtraString for this evTable.
77+
func (t *evTable) getExtraString(id extraStringID) string {
78+
if id == 0 {
79+
return ""
80+
}
81+
return t.extraStrings[id-1]
82+
}
83+
84+
// dataTable is a mapping from EIs to Es.
85+
type dataTable[EI ~uint64, E any] struct {
86+
present []uint8
87+
dense []E
88+
sparse map[EI]E
89+
}
90+
91+
// insert tries to add a mapping from id to s.
92+
//
93+
// Returns an error if a mapping for id already exists, regardless
94+
// of whether or not s is the same in content. This should be used
95+
// for validation during parsing.
96+
func (d *dataTable[EI, E]) insert(id EI, data E) error {
97+
if d.sparse == nil {
98+
d.sparse = make(map[EI]E)
99+
}
100+
if existing, ok := d.get(id); ok {
101+
return fmt.Errorf("multiple %Ts with the same ID: id=%d, new=%v, existing=%v", data, id, data, existing)
102+
}
103+
d.sparse[id] = data
104+
return nil
105+
}
106+
107+
// compactify attempts to compact sparse into dense.
108+
//
109+
// This is intended to be called only once after insertions are done.
110+
func (d *dataTable[EI, E]) compactify() {
111+
if d.sparse == nil || len(d.dense) != 0 {
112+
// Already compactified.
113+
return
114+
}
115+
// Find the range of IDs.
116+
maxID := EI(0)
117+
minID := ^EI(0)
118+
for id := range d.sparse {
119+
if id > maxID {
120+
maxID = id
121+
}
122+
if id < minID {
123+
minID = id
124+
}
125+
}
126+
// We're willing to waste at most 2x memory.
127+
if int(maxID-minID) > 2*len(d.sparse) {
128+
return
129+
}
130+
if int(minID) > len(d.sparse) {
131+
return
132+
}
133+
size := int(maxID) + 1
134+
d.present = make([]uint8, (size+7)/8)
135+
d.dense = make([]E, size)
136+
for id, data := range d.sparse {
137+
d.dense[id] = data
138+
d.present[id/8] |= uint8(1) << (id % 8)
139+
}
140+
d.sparse = nil
141+
}
142+
143+
// get returns the E for id or false if it doesn't
144+
// exist. This should be used for validation during parsing.
145+
func (d *dataTable[EI, E]) get(id EI) (E, bool) {
146+
if id == 0 {
147+
return *new(E), true
148+
}
149+
if int(id) < len(d.dense) {
150+
if d.present[id/8]&(uint8(1)<<(id%8)) != 0 {
151+
return d.dense[id], true
152+
}
153+
} else if d.sparse != nil {
154+
if data, ok := d.sparse[id]; ok {
155+
return data, true
156+
}
157+
}
158+
return *new(E), false
159+
}
160+
161+
// forEach iterates over all ID/value pairs in the data table.
162+
func (d *dataTable[EI, E]) forEach(yield func(EI, E) bool) bool {
163+
for id, value := range d.dense {
164+
if d.present[id/8]&(uint8(1)<<(id%8)) == 0 {
165+
continue
166+
}
167+
if !yield(EI(id), value) {
168+
return false
169+
}
170+
}
171+
if d.sparse == nil {
172+
return true
173+
}
174+
for id, value := range d.sparse {
175+
if !yield(id, value) {
176+
return false
177+
}
178+
}
179+
return true
180+
}
181+
182+
// mustGet returns the E for id or panics if it fails.
183+
//
184+
// This should only be used if id has already been validated.
185+
func (d *dataTable[EI, E]) mustGet(id EI) E {
186+
data, ok := d.get(id)
187+
if !ok {
188+
panic(fmt.Sprintf("expected id %d in %T table", id, data))
189+
}
190+
return data
191+
}
192+
193+
// frequency is nanoseconds per timestamp unit.
194+
type frequency float64
195+
196+
// mul multiplies an unprocessed to produce a time in nanoseconds.
197+
func (f frequency) mul(t timestamp) Time {
198+
return Time(float64(t) * float64(f))
199+
}
200+
201+
// stringID is an index into the string table for a generation.
202+
type stringID uint64
203+
204+
// extraStringID is an index into the extra string table for a generation.
205+
type extraStringID uint64
206+
207+
// stackID is an index into the stack table for a generation.
208+
type stackID uint64
209+
210+
// cpuSample represents a CPU profiling sample captured by the trace.
211+
type cpuSample struct {
212+
schedCtx
213+
time Time
214+
stack stackID
215+
}
216+
217+
// asEvent produces a complete Event from a cpuSample. It needs
218+
// the evTable from the generation that created it.
219+
//
220+
// We don't just store it as an Event in generation to minimize
221+
// the amount of pointer data floating around.
222+
func (s cpuSample) asEvent(table *evTable) Event {
223+
// TODO(mknyszek): This is go122-specific, but shouldn't be.
224+
// Generalize this in the future.
225+
e := Event{
226+
table: table,
227+
ctx: s.schedCtx,
228+
base: baseEvent{
229+
typ: go122.EvCPUSample,
230+
time: s.time,
231+
},
232+
}
233+
e.base.args[0] = uint64(s.stack)
234+
return e
235+
}
236+
237+
// stack represents a goroutine stack sample.
238+
type stack struct {
239+
frames []frame
240+
}
241+
242+
func (s stack) String() string {
243+
var sb strings.Builder
244+
for _, frame := range s.frames {
245+
fmt.Fprintf(&sb, "\t%#v\n", frame)
246+
}
247+
return sb.String()
248+
}
249+
250+
// frame represents a single stack frame.
251+
type frame struct {
252+
pc uint64
253+
funcID stringID
254+
fileID stringID
255+
line uint64
256+
}

0 commit comments

Comments
 (0)