diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go
index ae5c516..cab12bc 100644
--- a/src/runtime/export_test.go
+++ b/src/runtime/export_test.go
@@ -2102,6 +2102,15 @@
return string(buf)
}
+// PrintBacklog returns a copy of the runtime's print backlog.
+func PrintBacklog() []byte {
+ b := make([]byte, len(printBacklog))
+ printlock()
+ copy(b, printBacklog[:])
+ printunlock()
+ return b
+}
+
// DumpPrint returns the output of print(v).
func DumpPrint[T any](v T) string {
gp := getg()
diff --git a/src/runtime/print.go b/src/runtime/print.go
index 3abdf17..8283359 100644
--- a/src/runtime/print.go
+++ b/src/runtime/print.go
@@ -67,7 +67,14 @@
// For both these reasons, let a thread acquire the printlock 'recursively'.
func printlock() {
- mp := getg().m
+ gp := getg()
+ if gp.writebuf != nil && gp.m.dying == 0 {
+ // Output is being diverted into this goroutine's own buffer
+ // (see gwrite), so there is nothing shared to protect. Once
+ // the M is dying gwrite writes to stderr instead, so keep the lock.
+ return
+ }
+ mp := gp.m
mp.locks++ // do not reschedule between printlock++ and lock(&debuglock).
mp.printlock++
if mp.printlock == 1 {
@@ -77,7 +84,11 @@
}
func printunlock() {
- mp := getg().m
+ gp := getg()
+ if gp.writebuf != nil && gp.m.dying == 0 {
+ return
+ }
+ mp := gp.m
mp.printlock--
if mp.printlock == 0 {
unlock(&debuglock)
@@ -90,7 +101,6 @@
if len(b) == 0 {
return
}
- recordForPanic(b)
gp := getg()
// Don't use the writebuf if gp.m is dying. We want anything
// written through gwrite to appear in the terminal rather
@@ -98,6 +108,7 @@
// Note that we can't just clear writebuf in the gp.m.dying case
// because a panic isn't allowed to have any write barriers.
if gp == nil || gp.writebuf == nil || gp.m.dying > 0 {
+ recordForPanic(b)
writeErr(b)
return
}
diff --git a/src/runtime/print_test.go b/src/runtime/print_test.go
index cfc27ed..1229df4 100644
--- a/src/runtime/print_test.go
+++ b/src/runtime/print_test.go
@@ -5,11 +5,131 @@
package runtime_test
import (
+ "bytes"
"math"
"runtime"
+ "strconv"
+ "strings"
+ "sync"
"testing"
)
+// printlock is skipped while a goroutine's output is diverted into its own
+// writebuf, so exercise that path from several goroutines at once and check
+// that each one still gets exactly its own output.
+func TestPrintConcurrentWritebuf(t *testing.T) {
+ const goroutines = 8
+ const iters = 200
+
+ var wg sync.WaitGroup
+ for g := range goroutines {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := range iters {
+ v := g*iters + i
+ if got, want := runtime.DumpPrint(v), strconv.Itoa(v); got != want {
+ t.Errorf("DumpPrint(%d) = %q, want %q", v, got, want)
+ return
+ }
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+// Stack writes through the same path. Concurrent callers asking only for
+// their own stack must each get one well-formed traceback.
+func TestStackConcurrent(t *testing.T) {
+ const goroutines = 8
+ const iters = 50
+
+ var wg sync.WaitGroup
+ for range goroutines {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ buf := make([]byte, 8192)
+ for range iters {
+ s := string(buf[:runtime.Stack(buf, false)])
+ if !strings.HasPrefix(s, "goroutine ") {
+ t.Errorf("Stack does not begin with %q: %.64q", "goroutine ", s)
+ return
+ }
+ if n := strings.Count(s, "\ngoroutine "); n != 0 {
+ t.Errorf("Stack(all=false) reported %d other goroutines:\n%s", n, s)
+ return
+ }
+ if !strings.Contains(s, "runtime_test.TestStackConcurrent") {
+ t.Errorf("Stack is missing its own frame:\n%s", s)
+ return
+ }
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+// Diverted output must not reach the print backlog, which is a global that
+// recordForPanic maintains under printlock. If it did, concurrent Stack calls
+// would race on it, and each call would also overwrite the crash context the
+// backlog exists to preserve.
+func TestStackLeavesPrintBacklogAlone(t *testing.T) {
+ var wg sync.WaitGroup
+ for range 8 {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ buf := make([]byte, 8192)
+ for range 50 {
+ runtime.Stack(buf, false)
+ runtime.DumpPrint(12345)
+ }
+ }()
+ }
+ wg.Wait()
+
+ // Look for text only a traceback produces.
+ backlog := runtime.PrintBacklog()
+ for _, marker := range []string{"TestStackLeavesPrintBacklogAlone", "goroutine "} {
+ if bytes.Contains(backlog, []byte(marker)) {
+ t.Errorf("print backlog contains traceback text %q:\n%s",
+ marker, bytes.Trim(backlog, "\x00"))
+ }
+ }
+}
+
+func BenchmarkStack(b *testing.B) {
+ buf := make([]byte, 8192)
+ for b.Loop() {
+ runtime.Stack(buf, false)
+ }
+}
+
+func BenchmarkStackParallel(b *testing.B) {
+ b.RunParallel(func(pb *testing.PB) {
+ buf := make([]byte, 8192)
+ for pb.Next() {
+ runtime.Stack(buf, false)
+ }
+ })
+}
+
+func BenchmarkStackAll(b *testing.B) {
+ var wg sync.WaitGroup
+ stop := make(chan struct{})
+ for range 32 {
+ wg.Add(1)
+ go func() { defer wg.Done(); <-stop }()
+ }
+ buf := make([]byte, 1<<16)
+ for b.Loop() {
+ runtime.Stack(buf, true)
+ }
+ close(stop)
+ wg.Wait()
+}
+
func FuzzPrintFloat64(f *testing.F) {
f.Add(math.SmallestNonzeroFloat64)
f.Add(math.MaxFloat64)