Skip to content

Commit

Permalink
runtime: add execution tracer v2 behind GOEXPERIMENT=exectracer2
Browse files Browse the repository at this point in the history
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 <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
mknyszek committed Nov 10, 2023
1 parent f7c5cbb commit 43ffe2a
Show file tree
Hide file tree
Showing 100 changed files with 11,790 additions and 67 deletions.
8 changes: 8 additions & 0 deletions src/cmd/compile/internal/test/inl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ func TestIntendedInlining(t *testing.T) {
"(*puintptr).set",
"(*wbBuf).get1",
"(*wbBuf).get2",

// Trace-related ones.
"traceLocker.ok",
"traceEnabled",
},
"runtime/internal/sys": {},
"runtime/internal/math": {
Expand Down Expand Up @@ -249,6 +253,10 @@ func TestIntendedInlining(t *testing.T) {
want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "TrailingZeros32")
want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Bswap32")
}
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" {
// runtime/internal/atomic.Loaduintptr is only intrinsified on these platforms.
want["runtime"] = append(want["runtime"], "traceAcquire")
}
if bits.UintSize == 64 {
// mix is only defined on 64-bit architectures
want["runtime"] = append(want["runtime"], "mix")
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/trace/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"context"
"flag"
"fmt"
"internal/goexperiment"
traceparser "internal/trace"
"os"
"reflect"
Expand Down Expand Up @@ -330,6 +331,9 @@ func TestAnalyzeAnnotationGC(t *testing.T) {
// If savetraces flag is set, the captured trace will be saved in the named file.
func traceProgram(t *testing.T, f func(), name string) error {
t.Helper()
if goexperiment.ExecTracer2 {
t.Skip("skipping because test programs are covered elsewhere for the new tracer")
}
buf := new(bytes.Buffer)
if err := trace.Start(buf); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/trace/trace_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package main
import (
"bytes"
"cmd/internal/traceviewer"
"internal/goexperiment"
traceparser "internal/trace"
"io"
"runtime"
Expand All @@ -23,6 +24,9 @@ import (
// that preexisted when the tracing started were not counted
// as threads in syscall. See golang.org/issues/22574.
func TestGoroutineInSyscall(t *testing.T) {
if goexperiment.ExecTracer2 {
t.Skip("skipping because this test is obsolete and incompatible with the new tracer")
}
// Start one goroutine blocked in syscall.
//
// TODO: syscall.Pipe used to cause the goroutine to
Expand Down
25 changes: 24 additions & 1 deletion src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,12 +605,35 @@ var depsRules = `
syscall
< os/exec/internal/fdtest;
FMT
< internal/diff, internal/txtar;
FMT, container/heap, math/rand
< internal/trace;
# v2 execution trace parser.
FMT
< internal/diff, internal/txtar;
< internal/trace/v2/event;
internal/trace/v2/event
< internal/trace/v2/event/go122;
FMT, io, internal/trace/v2/event/go122
< internal/trace/v2/version;
FMT, encoding/binary, internal/trace/v2/version
< internal/trace/v2/raw;
FMT, encoding/binary, internal/trace/v2/version
< internal/trace/v2;
regexp, internal/trace/v2, internal/trace/v2/raw, internal/txtar
< internal/trace/v2/testtrace;
regexp, internal/txtar, internal/trace/v2, internal/trace/v2/raw
< internal/trace/v2/internal/testgen/go122;
# Coverage.
FMT, crypto/md5, encoding/binary, regexp, sort, text/tabwriter, unsafe,
internal/coverage, internal/coverage/uleb128
< internal/coverage/cmerge,
Expand Down
9 changes: 9 additions & 0 deletions src/internal/goexperiment/exp_exectracer2_off.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/internal/goexperiment/exp_exectracer2_on.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/internal/goexperiment/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@ type Flags struct {
// AllocHeaders enables a different, more efficient way for the GC to
// manage heap metadata.
AllocHeaders bool

// ExecTracer2 controls whether to use the new execution trace
// implementation.
ExecTracer2 bool
}
256 changes: 256 additions & 0 deletions src/internal/trace/v2/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains data types that all implementations of the trace format
// parser need to provide to the rest of the package.

package trace

import (
"fmt"
"strings"

"internal/trace/v2/event"
"internal/trace/v2/event/go122"
"internal/trace/v2/version"
)

// maxArgs is the maximum number of arguments for "plain" events,
// i.e. anything that could reasonably be represented as a Base.
const maxArgs = 5

// baseEvent is the basic unprocessed event. This serves as a common
// fundamental data structure across.
type baseEvent struct {
typ event.Type
time Time
args [maxArgs - 1]uint64
}

// extra returns a slice representing extra available space in args
// that the parser can use to pass data up into Event.
func (e *baseEvent) extra(v version.Version) []uint64 {
switch v {
case version.Go122:
return e.args[len(go122.Specs()[e.typ].Args)-1:]
}
panic(fmt.Sprintf("unsupported version: go 1.%d", v))
}

// evTable contains the per-generation data necessary to
// interpret an individual event.
type evTable struct {
freq frequency
strings dataTable[stringID, string]
stacks dataTable[stackID, stack]

// extraStrings are strings that get generated during
// parsing but haven't come directly from the trace, so
// they don't appear in strings.
extraStrings []string
extraStringIDs map[string]extraStringID
nextExtra extraStringID
}

// addExtraString adds an extra string to the evTable and returns
// a unique ID for the string in the table.
func (t *evTable) addExtraString(s string) extraStringID {
if s == "" {
return 0
}
if t.extraStringIDs == nil {
t.extraStringIDs = make(map[string]extraStringID)
}
if id, ok := t.extraStringIDs[s]; ok {
return id
}
t.nextExtra++
id := t.nextExtra
t.extraStrings = append(t.extraStrings, s)
t.extraStringIDs[s] = id
return id
}

// getExtraString returns the extra string for the provided ID.
// The ID must have been produced by addExtraString for this evTable.
func (t *evTable) getExtraString(id extraStringID) string {
if id == 0 {
return ""
}
return t.extraStrings[id-1]
}

// dataTable is a mapping from EIs to Es.
type dataTable[EI ~uint64, E any] struct {
present []uint8
dense []E
sparse map[EI]E
}

// insert tries to add a mapping from id to s.
//
// Returns an error if a mapping for id already exists, regardless
// of whether or not s is the same in content. This should be used
// for validation during parsing.
func (d *dataTable[EI, E]) insert(id EI, data E) error {
if d.sparse == nil {
d.sparse = make(map[EI]E)
}
if existing, ok := d.get(id); ok {
return fmt.Errorf("multiple %Ts with the same ID: id=%d, new=%v, existing=%v", data, id, data, existing)
}
d.sparse[id] = data
return nil
}

// compactify attempts to compact sparse into dense.
//
// This is intended to be called only once after insertions are done.
func (d *dataTable[EI, E]) compactify() {
if d.sparse == nil || len(d.dense) != 0 {
// Already compactified.
return
}
// Find the range of IDs.
maxID := EI(0)
minID := ^EI(0)
for id := range d.sparse {
if id > maxID {
maxID = id
}
if id < minID {
minID = id
}
}
// We're willing to waste at most 2x memory.
if int(maxID-minID) > 2*len(d.sparse) {
return
}
if int(minID) > len(d.sparse) {
return
}
size := int(maxID) + 1
d.present = make([]uint8, (size+7)/8)
d.dense = make([]E, size)
for id, data := range d.sparse {
d.dense[id] = data
d.present[id/8] |= uint8(1) << (id % 8)
}
d.sparse = nil
}

// get returns the E for id or false if it doesn't
// exist. This should be used for validation during parsing.
func (d *dataTable[EI, E]) get(id EI) (E, bool) {
if id == 0 {
return *new(E), true
}
if int(id) < len(d.dense) {
if d.present[id/8]&(uint8(1)<<(id%8)) != 0 {
return d.dense[id], true
}
} else if d.sparse != nil {
if data, ok := d.sparse[id]; ok {
return data, true
}
}
return *new(E), false
}

// forEach iterates over all ID/value pairs in the data table.
func (d *dataTable[EI, E]) forEach(yield func(EI, E) bool) bool {
for id, value := range d.dense {
if d.present[id/8]&(uint8(1)<<(id%8)) == 0 {
continue
}
if !yield(EI(id), value) {
return false
}
}
if d.sparse == nil {
return true
}
for id, value := range d.sparse {
if !yield(id, value) {
return false
}
}
return true
}

// mustGet returns the E for id or panics if it fails.
//
// This should only be used if id has already been validated.
func (d *dataTable[EI, E]) mustGet(id EI) E {
data, ok := d.get(id)
if !ok {
panic(fmt.Sprintf("expected id %d in %T table", id, data))
}
return data
}

// frequency is nanoseconds per timestamp unit.
type frequency float64

// mul multiplies an unprocessed to produce a time in nanoseconds.
func (f frequency) mul(t timestamp) Time {
return Time(float64(t) * float64(f))
}

// stringID is an index into the string table for a generation.
type stringID uint64

// extraStringID is an index into the extra string table for a generation.
type extraStringID uint64

// stackID is an index into the stack table for a generation.
type stackID uint64

// cpuSample represents a CPU profiling sample captured by the trace.
type cpuSample struct {
schedCtx
time Time
stack stackID
}

// asEvent produces a complete Event from a cpuSample. It needs
// the evTable from the generation that created it.
//
// We don't just store it as an Event in generation to minimize
// the amount of pointer data floating around.
func (s cpuSample) asEvent(table *evTable) Event {
// TODO(mknyszek): This is go122-specific, but shouldn't be.
// Generalize this in the future.
e := Event{
table: table,
ctx: s.schedCtx,
base: baseEvent{
typ: go122.EvCPUSample,
time: s.time,
},
}
e.base.args[0] = uint64(s.stack)
return e
}

// stack represents a goroutine stack sample.
type stack struct {
frames []frame
}

func (s stack) String() string {
var sb strings.Builder
for _, frame := range s.frames {
fmt.Fprintf(&sb, "\t%#v\n", frame)
}
return sb.String()
}

// frame represents a single stack frame.
type frame struct {
pc uint64
funcID stringID
fileID stringID
line uint64
}
Loading

0 comments on commit 43ffe2a

Please sign in to comment.