// Copyright 2014 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. package trace_test import ( "bytes" "context" "flag" "fmt" "internal/goexperiment" "internal/profile" "internal/race" "internal/trace" "io" "net" "os" "runtime" "runtime/pprof" . "runtime/trace" "strconv" "strings" "sync" "testing" "time" ) var ( saveTraces = flag.Bool("savetraces", false, "save traces collected by tests") ) // TestEventBatch tests Flush calls that happen during Start // don't produce corrupted traces. func TestEventBatch(t *testing.T) { if race.Enabled { t.Skip("skipping in race mode") } if IsEnabled() { t.Skip("skipping because -test.trace is set") } if testing.Short() { t.Skip("skipping in short mode") } if goexperiment.ExecTracer2 { t.Skip("skipping because this test is incompatible with the new tracer") } // During Start, bunch of records are written to reflect the current // snapshot of the program, including state of each goroutines. // And some string constants are written to the trace to aid trace // parsing. This test checks Flush of the buffer occurred during // this process doesn't cause corrupted traces. // When a Flush is called during Start is complicated // so we test with a range of number of goroutines hoping that one // of them triggers Flush. // This range was chosen to fill up a ~64KB buffer with traceEvGoCreate // and traceEvGoWaiting events (12~13bytes per goroutine). for g := 4950; g < 5050; g++ { n := g t.Run("G="+strconv.Itoa(n), func(t *testing.T) { var wg sync.WaitGroup wg.Add(n) in := make(chan bool, 1000) for i := 0; i < n; i++ { go func() { <-in wg.Done() }() } buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } for i := 0; i < n; i++ { in <- true } wg.Wait() Stop() _, err := trace.Parse(buf, "") if err == trace.ErrTimeOrder { t.Skipf("skipping trace: %v", err) } if err != nil { t.Fatalf("failed to parse trace: %v", err) } }) } } func TestTraceStartStop(t *testing.T) { if IsEnabled() { t.Skip("skipping because -test.trace is set") } buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } Stop() size := buf.Len() if size == 0 { t.Fatalf("trace is empty") } time.Sleep(100 * time.Millisecond) if size != buf.Len() { t.Fatalf("trace writes after stop: %v -> %v", size, buf.Len()) } saveTrace(t, buf, "TestTraceStartStop") } func TestTraceDoubleStart(t *testing.T) { if IsEnabled() { t.Skip("skipping because -test.trace is set") } Stop() buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } if err := Start(buf); err == nil { t.Fatalf("succeed to start tracing second time") } Stop() Stop() } func TestTrace(t *testing.T) { if IsEnabled() { t.Skip("skipping because -test.trace is set") } if goexperiment.ExecTracer2 { // An equivalent test exists in internal/trace/v2. t.Skip("skipping because this test is incompatible with the new tracer") } buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } Stop() saveTrace(t, buf, "TestTrace") _, err := trace.Parse(buf, "") if err == trace.ErrTimeOrder { t.Skipf("skipping trace: %v", err) } if err != nil { t.Fatalf("failed to parse trace: %v", err) } } func parseTrace(t *testing.T, r io.Reader) ([]*trace.Event, map[uint64]*trace.GDesc) { res, err := trace.Parse(r, "") if err == trace.ErrTimeOrder { t.Skipf("skipping trace: %v", err) } if err != nil { t.Fatalf("failed to parse trace: %v", err) } gs := trace.GoroutineStats(res.Events) for goid := range gs { // We don't do any particular checks on the result at the moment. // But still check that RelatedGoroutines does not crash, hang, etc. _ = trace.RelatedGoroutines(res.Events, goid) } return res.Events, gs } func testBrokenTimestamps(t *testing.T, data []byte) { // On some processors cputicks (used to generate trace timestamps) // produce non-monotonic timestamps. It is important that the parser // distinguishes logically inconsistent traces (e.g. missing, excessive // or misordered events) from broken timestamps. The former is a bug // in tracer, the latter is a machine issue. // So now that we have a consistent trace, test that (1) parser does // not return a logical error in case of broken timestamps // and (2) broken timestamps are eventually detected and reported. trace.BreakTimestampsForTesting = true defer func() { trace.BreakTimestampsForTesting = false }() for i := 0; i < 1e4; i++ { _, err := trace.Parse(bytes.NewReader(data), "") if err == trace.ErrTimeOrder { return } if err != nil { t.Fatalf("failed to parse trace: %v", err) } } } func TestTraceStress(t *testing.T) { switch runtime.GOOS { case "js", "wasip1": t.Skip("no os.Pipe on " + runtime.GOOS) } if IsEnabled() { t.Skip("skipping because -test.trace is set") } if testing.Short() { t.Skip("skipping in -short mode") } if goexperiment.ExecTracer2 { // An equivalent test exists in internal/trace/v2. t.Skip("skipping because this test is incompatible with the new tracer") } var wg sync.WaitGroup done := make(chan bool) // Create a goroutine blocked before tracing. wg.Add(1) go func() { <-done wg.Done() }() // Create a goroutine blocked in syscall before tracing. rp, wp, err := os.Pipe() if err != nil { t.Fatalf("failed to create pipe: %v", err) } defer func() { rp.Close() wp.Close() }() wg.Add(1) go func() { var tmp [1]byte rp.Read(tmp[:]) <-done wg.Done() }() time.Sleep(time.Millisecond) // give the goroutine above time to block buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } procs := runtime.GOMAXPROCS(10) time.Sleep(50 * time.Millisecond) // test proc stop/start events go func() { runtime.LockOSThread() for { select { case <-done: return default: runtime.Gosched() } } }() runtime.GC() // Trigger GC from malloc. n := int(1e3) if isMemoryConstrained() { // Reduce allocation to avoid running out of // memory on the builder - see issue/12032. n = 512 } for i := 0; i < n; i++ { _ = make([]byte, 1<<20) } // Create a bunch of busy goroutines to load all Ps. for p := 0; p < 10; p++ { wg.Add(1) go func() { // Do something useful. tmp := make([]byte, 1<<16) for i := range tmp { tmp[i]++ } _ = tmp <-done wg.Done() }() } // Block in syscall. wg.Add(1) go func() { var tmp [1]byte rp.Read(tmp[:]) <-done wg.Done() }() // Test timers. timerDone := make(chan bool) go func() { time.Sleep(time.Millisecond) timerDone <- true }() <-timerDone // A bit of network. ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen failed: %v", err) } defer ln.Close() go func() { c, err := ln.Accept() if err != nil { return } time.Sleep(time.Millisecond) var buf [1]byte c.Write(buf[:]) c.Close() }() c, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Fatalf("dial failed: %v", err) } var tmp [1]byte c.Read(tmp[:]) c.Close() go func() { runtime.Gosched() select {} }() // Unblock helper goroutines and wait them to finish. wp.Write(tmp[:]) wp.Write(tmp[:]) close(done) wg.Wait() runtime.GOMAXPROCS(procs) Stop() saveTrace(t, buf, "TestTraceStress") trace := buf.Bytes() parseTrace(t, buf) testBrokenTimestamps(t, trace) } // isMemoryConstrained reports whether the current machine is likely // to be memory constrained. // This was originally for the openbsd/arm builder (Issue 12032). // TODO: move this to testenv? Make this look at memory? Look at GO_BUILDER_NAME? func isMemoryConstrained() bool { if runtime.GOOS == "plan9" { return true } switch runtime.GOARCH { case "arm", "mips", "mipsle": return true } return false } // Do a bunch of various stuff (timers, GC, network, etc) in a separate goroutine. // And concurrently with all that start/stop trace 3 times. func TestTraceStressStartStop(t *testing.T) { switch runtime.GOOS { case "js", "wasip1": t.Skip("no os.Pipe on " + runtime.GOOS) } if IsEnabled() { t.Skip("skipping because -test.trace is set") } if goexperiment.ExecTracer2 { // An equivalent test exists in internal/trace/v2. t.Skip("skipping because this test is incompatible with the new tracer") } defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(8)) outerDone := make(chan bool) go func() { defer func() { outerDone <- true }() var wg sync.WaitGroup done := make(chan bool) wg.Add(1) go func() { <-done wg.Done() }() rp, wp, err := os.Pipe() if err != nil { t.Errorf("failed to create pipe: %v", err) return } defer func() { rp.Close() wp.Close() }() wg.Add(1) go func() { var tmp [1]byte rp.Read(tmp[:]) <-done wg.Done() }() time.Sleep(time.Millisecond) go func() { runtime.LockOSThread() for { select { case <-done: return default: runtime.Gosched() } } }() runtime.GC() // Trigger GC from malloc. n := int(1e3) if isMemoryConstrained() { // Reduce allocation to avoid running out of // memory on the builder. n = 512 } for i := 0; i < n; i++ { _ = make([]byte, 1<<20) } // Create a bunch of busy goroutines to load all Ps. for p := 0; p < 10; p++ { wg.Add(1) go func() { // Do something useful. tmp := make([]byte, 1<<16) for i := range tmp { tmp[i]++ } _ = tmp <-done wg.Done() }() } // Block in syscall. wg.Add(1) go func() { var tmp [1]byte rp.Read(tmp[:]) <-done wg.Done() }() runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) // Test timers. timerDone := make(chan bool) go func() { time.Sleep(time.Millisecond) timerDone <- true }() <-timerDone // A bit of network. ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Errorf("listen failed: %v", err) return } defer ln.Close() go func() { c, err := ln.Accept() if err != nil { return } time.Sleep(time.Millisecond) var buf [1]byte c.Write(buf[:]) c.Close() }() c, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Errorf("dial failed: %v", err) return } var tmp [1]byte c.Read(tmp[:]) c.Close() go func() { runtime.Gosched() select {} }() // Unblock helper goroutines and wait them to finish. wp.Write(tmp[:]) wp.Write(tmp[:]) close(done) wg.Wait() }() for i := 0; i < 3; i++ { buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } time.Sleep(time.Millisecond) Stop() saveTrace(t, buf, "TestTraceStressStartStop") trace := buf.Bytes() parseTrace(t, buf) testBrokenTimestamps(t, trace) } <-outerDone } func TestTraceFutileWakeup(t *testing.T) { if IsEnabled() { t.Skip("skipping because -test.trace is set") } if goexperiment.ExecTracer2 { t.Skip("skipping because this test is incompatible with the new tracer") } buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(8)) c0 := make(chan int, 1) c1 := make(chan int, 1) c2 := make(chan int, 1) const procs = 2 var done sync.WaitGroup done.Add(4 * procs) for p := 0; p < procs; p++ { const iters = 1e3 go func() { for i := 0; i < iters; i++ { runtime.Gosched() c0 <- 0 } done.Done() }() go func() { for i := 0; i < iters; i++ { runtime.Gosched() <-c0 } done.Done() }() go func() { for i := 0; i < iters; i++ { runtime.Gosched() select { case c1 <- 0: case c2 <- 0: } } done.Done() }() go func() { for i := 0; i < iters; i++ { runtime.Gosched() select { case <-c1: case <-c2: } } done.Done() }() } done.Wait() Stop() saveTrace(t, buf, "TestTraceFutileWakeup") events, _ := parseTrace(t, buf) // Check that (1) trace does not contain EvFutileWakeup events and // (2) there are no consecutive EvGoBlock/EvGCStart/EvGoBlock events // (we call runtime.Gosched between all operations, so these would be futile wakeups). gs := make(map[uint64]int) for _, ev := range events { switch ev.Type { case trace.EvFutileWakeup: t.Fatalf("found EvFutileWakeup event") case trace.EvGoBlockSend, trace.EvGoBlockRecv, trace.EvGoBlockSelect: if gs[ev.G] == 2 { t.Fatalf("goroutine %v blocked on %v at %v right after start", ev.G, trace.EventDescriptions[ev.Type].Name, ev.Ts) } if gs[ev.G] == 1 { t.Fatalf("goroutine %v blocked on %v at %v while blocked", ev.G, trace.EventDescriptions[ev.Type].Name, ev.Ts) } gs[ev.G] = 1 case trace.EvGoStart: if gs[ev.G] == 1 { gs[ev.G] = 2 } default: delete(gs, ev.G) } } } func TestTraceCPUProfile(t *testing.T) { if IsEnabled() { t.Skip("skipping because -test.trace is set") } if goexperiment.ExecTracer2 { // An equivalent test exists in internal/trace/v2. t.Skip("skipping because this test is incompatible with the new tracer") } cpuBuf := new(bytes.Buffer) if err := pprof.StartCPUProfile(cpuBuf); err != nil { t.Skipf("failed to start CPU profile: %v", err) } buf := new(bytes.Buffer) if err := Start(buf); err != nil { t.Fatalf("failed to start tracing: %v", err) } dur := 100 * time.Millisecond func() { // Create a region in the execution trace. Set and clear goroutine // labels fully within that region, so we know that any CPU profile // sample with the label must also be eligible for inclusion in the // execution trace. ctx := context.Background() defer StartRegion(ctx, "cpuHogger").End() pprof.Do(ctx, pprof.Labels("tracing", "on"), func(ctx context.Context) { cpuHogger(cpuHog1, &salt1, dur) }) // Be sure the execution trace's view, when filtered to this goroutine // via the explicit goroutine ID in each event, gets many more samples // than the CPU profiler when filtered to this goroutine via labels. cpuHogger(cpuHog1, &salt1, dur) }() Stop() pprof.StopCPUProfile() saveTrace(t, buf, "TestTraceCPUProfile") prof, err := profile.Parse(cpuBuf) if err != nil { t.Fatalf("failed to parse CPU profile: %v", err) } // Examine the CPU profiler's view. Filter it to only include samples from // the single test goroutine. Use labels to execute that filter: they should // apply to all work done while that goroutine is getg().m.curg, and they // should apply to no other goroutines. pprofSamples := 0 pprofStacks := make(map[string]int) for _, s := range prof.Sample { if s.Label["tracing"] != nil { var fns []string var leaf string for _, loc := range s.Location { for _, line := range loc.Line { fns = append(fns, fmt.Sprintf("%s:%d", line.Function.Name, line.Line)) leaf = line.Function.Name } } // runtime.sigprof synthesizes call stacks when "normal traceback is // impossible or has failed", using particular placeholder functions // to represent common failure cases. Look for those functions in // the leaf position as a sign that the call stack and its // symbolization are more complex than this test can handle. // // TODO: Make the symbolization done by the execution tracer and CPU // profiler match up even in these harder cases. See #53378. switch leaf { case "runtime._System", "runtime._GC", "runtime._ExternalCode", "runtime._VDSO": continue } stack := strings.Join(fns, " ") samples := int(s.Value[0]) pprofSamples += samples pprofStacks[stack] += samples } } if pprofSamples == 0 { t.Skipf("CPU profile did not include any samples while tracing was active\n%s", prof) } // Examine the execution tracer's view of the CPU profile samples. Filter it // to only include samples from the single test goroutine. Use the goroutine // ID that was recorded in the events: that should reflect getg().m.curg, // same as the profiler's labels (even when the M is using its g0 stack). totalTraceSamples := 0 traceSamples := 0 traceStacks := make(map[string]int) events, _ := parseTrace(t, buf) var hogRegion *trace.Event for _, ev := range events { if ev.Type == trace.EvUserRegion && ev.Args[1] == 0 && ev.SArgs[0] == "cpuHogger" { // mode "0" indicates region start hogRegion = ev } } if hogRegion == nil { t.Fatalf("execution trace did not identify cpuHogger goroutine") } else if hogRegion.Link == nil { t.Fatalf("execution trace did not close cpuHogger region") } for _, ev := range events { if ev.Type == trace.EvCPUSample { totalTraceSamples++ if ev.G == hogRegion.G { traceSamples++ var fns []string for _, frame := range ev.Stk { if frame.Fn != "runtime.goexit" { fns = append(fns, fmt.Sprintf("%s:%d", frame.Fn, frame.Line)) } } stack := strings.Join(fns, " ") traceStacks[stack]++ } } } // The execution trace may drop CPU profile samples if the profiling buffer // overflows. Based on the size of profBufWordCount, that takes a bit over // 1900 CPU samples or 19 thread-seconds at a 100 Hz sample rate. If we've // hit that case, then we definitely have at least one full buffer's worth // of CPU samples, so we'll call that success. overflowed := totalTraceSamples >= 1900 if traceSamples < pprofSamples { t.Logf("execution trace did not include all CPU profile samples; %d in profile, %d in trace", pprofSamples, traceSamples) if !overflowed { t.Fail() } } for stack, traceSamples := range traceStacks { pprofSamples := pprofStacks[stack] delete(pprofStacks, stack) if traceSamples < pprofSamples { t.Logf("execution trace did not include all CPU profile samples for stack %q; %d in profile, %d in trace", stack, pprofSamples, traceSamples) if !overflowed { t.Fail() } } } for stack, pprofSamples := range pprofStacks { t.Logf("CPU profile included %d samples at stack %q not present in execution trace", pprofSamples, stack) if !overflowed { t.Fail() } } if t.Failed() { t.Logf("execution trace CPU samples:") for stack, samples := range traceStacks { t.Logf("%d: %q", samples, stack) } t.Logf("CPU profile:\n%v", prof) } } func cpuHogger(f func(x int) int, y *int, dur time.Duration) { // We only need to get one 100 Hz clock tick, so we've got // a large safety buffer. // But do at least 500 iterations (which should take about 100ms), // otherwise TestCPUProfileMultithreaded can fail if only one // thread is scheduled during the testing period. t0 := time.Now() accum := *y for i := 0; i < 500 || time.Since(t0) < dur; i++ { accum = f(accum) } *y = accum } var ( salt1 = 0 ) // The actual CPU hogging function. // Must not call other functions nor access heap/globals in the loop, // otherwise under race detector the samples will be in the race runtime. func cpuHog1(x int) int { return cpuHog0(x, 1e5) } func cpuHog0(x, n int) int { foo := x for i := 0; i < n; i++ { if i%1000 == 0 { // Spend time in mcall, stored as gp.m.curg, with g0 running runtime.Gosched() } if foo > 0 { foo *= foo } else { foo *= foo + 1 } } return foo } func saveTrace(t *testing.T, buf *bytes.Buffer, name string) { if !*saveTraces { return } if err := os.WriteFile(name+".trace", buf.Bytes(), 0600); err != nil { t.Errorf("failed to write trace file: %s", err) } }