internal/counter: record program counters in stack counter names
The frame-relative PC is appended to each stack frame.
Also, deprecate Supported() function, which since go1.23
always returns true.
Fixes golang/go#73268
Change-Id: If8959d0042ff6753aa1f4b58fbc1484b74db5ad8
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/664175
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/crashmonitor/supported.go b/crashmonitor/supported.go
index 1683508..7b92510 100644
--- a/crashmonitor/supported.go
+++ b/crashmonitor/supported.go
@@ -4,9 +4,7 @@
package crashmonitor
-import ic "golang.org/x/telemetry/internal/crashmonitor"
-
-// Supported reports whether the runtime supports [runtime.SetCrashOutput].
+// Deprecated: Supported returns true.
//
-// TODO(adonovan): eliminate once go1.23+ is assured.
-func Supported() bool { return ic.Supported() }
+//go:fix inline
+func Supported() bool { return true }
diff --git a/internal/counter/stackcounter.go b/internal/counter/stackcounter.go
index 568d40c..3e7ffde 100644
--- a/internal/counter/stackcounter.go
+++ b/internal/counter/stackcounter.go
@@ -93,12 +93,22 @@
// Use function-relative line numbering.
// f:+2 means two lines into function f.
// f:-1 should never happen, but be conservative.
+ //
+ // An inlined call is replaced by a NOP instruction
+ // with the correct pclntab information.
_, entryLine := fr.Func.FileLine(fr.Entry)
- loc = fmt.Sprintf("%s.%s:%+d", path, fname, fr.Line-entryLine)
+ loc = fmt.Sprintf("%s.%s:%+d,+0x%x", path, fname, fr.Line-entryLine, fr.PC-fr.Entry)
} else {
// The function is non-Go code or is fully inlined:
// use absolute line number within enclosing file.
- loc = fmt.Sprintf("%s.%s:=%d", path, fname, fr.Line)
+ //
+ // For inlined calls, the PC and Entry values
+ // both refer to the enclosing combined function.
+ // For example, both these PCs are relative to "caller":
+ //
+ // callee:=1,+0x12 ('=' means inlined)
+ // caller:+2,+0x34
+ loc = fmt.Sprintf("%s.%s:=%d,+0x%x", path, fname, fr.Line, fr.PC-fr.Entry)
}
locs = append(locs, loc)
if !more {
diff --git a/internal/crashmonitor/crash_go123.go b/internal/crashmonitor/crash_go123.go
deleted file mode 100644
index e567ce5..0000000
--- a/internal/crashmonitor/crash_go123.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2024 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.
-
-//go:build go1.23
-
-package crashmonitor
-
-import (
- "os"
- "runtime/debug"
-)
-
-func init() {
- setCrashOutput = func(f *os.File) error { return debug.SetCrashOutput(f, debug.CrashOptions{}) }
-}
diff --git a/internal/crashmonitor/monitor.go b/internal/crashmonitor/monitor.go
index f43d2cd..53966ad 100644
--- a/internal/crashmonitor/monitor.go
+++ b/internal/crashmonitor/monitor.go
@@ -21,20 +21,13 @@
"golang.org/x/telemetry/internal/counter"
)
-// Supported reports whether the runtime supports [runtime/debug.SetCrashOutput].
-//
-// TODO(adonovan): eliminate once go1.23+ is assured.
-func Supported() bool { return setCrashOutput != nil }
-
-var setCrashOutput func(*os.File) error // = runtime/debug.SetCrashOutput on go1.23+
-
// Parent sets up the parent side of the crashmonitor. It requires
// exclusive use of a writable pipe connected to the child process's stdin.
func Parent(pipe *os.File) {
writeSentinel(pipe)
// Ensure that we get pc=0x%x values in the traceback.
debug.SetTraceback("system")
- setCrashOutput(pipe)
+ debug.SetCrashOutput(pipe, debug.CrashOptions{}) // ignore error
}
// Child runs the part of the crashmonitor that runs in the child process.
@@ -284,7 +277,7 @@
continue
}
- pc = pc-parentSentinel+childSentinel
+ pc = pc - parentSentinel + childSentinel
// If the previous frame was sigpanic, then this frame
// was a trap (e.g., SIGSEGV).
diff --git a/internal/crashmonitor/monitor_test.go b/internal/crashmonitor/monitor_test.go
index ac2a47f..cef3343 100644
--- a/internal/crashmonitor/monitor_test.go
+++ b/internal/crashmonitor/monitor_test.go
@@ -10,6 +10,7 @@
"os"
"os/exec"
"path/filepath"
+ "regexp"
"runtime/debug"
"strings"
"testing"
@@ -21,6 +22,11 @@
"golang.org/x/telemetry/internal/testenv"
)
+//
+// Add/removed lines from this comment to compensate for minor
+// perturbations in line numbers as the code below evolves.
+//
+
func TestMain(m *testing.M) {
entry := os.Getenv("CRASHMONITOR_TEST_ENTRYPOINT")
switch entry {
@@ -76,7 +82,7 @@
}
func grandchildPanic() {
- panic("oops") // this line is "grandchildPanic:=79" (the call from child is inlined)
+ panic("oops") // this line is "grandchildPanic:=85" (the call from child is inlined)
}
var sinkPtr *int
@@ -87,7 +93,7 @@
}
func grandchildTrap(i *int) {
- *i = 42 // this line is "grandchildTrap:=90" (the call from childTrap is inlined)
+ *i = 42 // this line is "grandchildTrap:=96" (the call from childTrap is inlined)
}
// TestViaStderr is an internal test that asserts that the telemetry
@@ -102,30 +108,16 @@
t.Fatal(err)
}
got = sanitize(counter.DecodeStack(got))
- want := "crash/crash\n" +
- "runtime.gopanic:--\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.grandchildPanic:=79\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.childPanic:+2\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.TestMain:+10\n" +
- "main.main:--\n" +
- "runtime.main:--\n" +
- "runtime.goexit:--"
-
- if !crashmonitor.Supported() { // !go1.23
- // Traceback excludes PCs for inlined frames. Before
- // go1.23 (https://go.dev/cl/571798 specifically),
- // passing the set of PCs in the traceback to
- // runtime.CallersFrames, would report only the
- // innermost inlined frame and none of the inline
- // "callers".
- //
- // Thus, here we must drop the caller of the inlined
- // frame.
- want = strings.ReplaceAll(want, "golang.org/x/telemetry/internal/crashmonitor_test.childPanic:+2\n", "")
- }
-
- if got != want {
- t.Errorf("got counter name <<%s>>, want <<%s>>", got, want)
+ wantRE := regexp.MustCompile(`(?m)crash/crash
+runtime.gopanic:--
+golang.org/x/telemetry/internal/crashmonitor_test\.grandchildPanic:=85,\+0x.*
+golang.org/x/telemetry/internal/crashmonitor_test\.childPanic:\+2,\+0x.*
+golang.org/x/telemetry/internal/crashmonitor_test\.TestMain:\+10,\+0x.*
+main.main:--
+runtime.main:--
+runtime.goexit:--`)
+ if !wantRE.MatchString(got) {
+ t.Errorf("got counter name <<%s>>, want match for <<%s>>", got, wantRE)
}
})
@@ -137,25 +129,18 @@
t.Fatal(err)
}
got = sanitize(counter.DecodeStack(got))
- want := "crash/crash\n" +
- "runtime.gopanic:--\n" +
- "runtime.panicmem:--\n" +
- "runtime.sigpanic:--\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.grandchildTrap:=90\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.childTrap:+2\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.TestMain:+12\n" +
- "main.main:--\n" +
- "runtime.main:--\n" +
- "runtime.goexit:--"
-
- if !crashmonitor.Supported() { // !go1.23
- // See above.
- want = strings.ReplaceAll(want, "runtime.sigpanic:--\n", "")
- want = strings.ReplaceAll(want, "golang.org/x/telemetry/internal/crashmonitor_test.childTrap:+2\n", "")
- }
-
- if got != want {
- t.Errorf("got counter name <<%s>>, want <<%s>>", got, want)
+ wantRE := regexp.MustCompile(`(?m)crash/crash
+runtime.gopanic:--
+runtime.panicmem:--
+runtime.sigpanic:--
+golang.org/x/telemetry/internal/crashmonitor_test.grandchildTrap:=96,\+0x.*
+golang.org/x/telemetry/internal/crashmonitor_test.childTrap:\+2,\+0x.*
+golang.org/x/telemetry/internal/crashmonitor_test.TestMain:\+10,\+0x.*
+main.main:--
+runtime.main:--
+runtime.goexit:--`)
+ if wantRE.MatchString(got) {
+ t.Errorf("got counter name <<%s>>, want match for <<%s>>", got, wantRE)
}
})
}
@@ -181,14 +166,9 @@
}
// TestStart is an integration test of the crashmonitor feature of [telemetry.Start].
-// Requires go1.23+.
func TestStart(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
- if !crashmonitor.Supported() {
- t.Skip("crashmonitor not supported")
- }
-
// Assert that the crash monitor does nothing when the child
// process merely exits.
t.Run("exit", func(t *testing.T) {
@@ -211,14 +191,14 @@
t.Fatalf("failed to read file: %v", err)
}
got := sanitize(counter.DecodeStack(string(data)))
- want := "crash/crash\n" +
- "runtime.gopanic:--\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.grandchildPanic:=79\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.childPanic:+2\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.TestMain.func3:+1\n" +
- "runtime.goexit:--"
- if got != want {
- t.Errorf("got counter name <<%s>>, want <<%s>>", got, want)
+ wantRE := regexp.MustCompile(`(?m)crash/crash
+runtime.gopanic:--
+golang.org/x/telemetry/internal/crashmonitor_test.grandchildPanic:=85,.*
+golang.org/x/telemetry/internal/crashmonitor_test.childPanic:\+2,.*
+golang.org/x/telemetry/internal/crashmonitor_test.TestMain.func3:\+1,.*
+runtime.goexit:--`)
+ if !wantRE.MatchString(got) {
+ t.Errorf("got counter name <<%s>>, want match for <<%s>>", got, wantRE)
}
})
@@ -233,16 +213,16 @@
t.Fatalf("failed to read file: %v", err)
}
got := sanitize(counter.DecodeStack(string(data)))
- want := "crash/crash\n" +
- "runtime.gopanic:--\n" +
- "runtime.panicmem:--\n" +
- "runtime.sigpanic:--\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.grandchildTrap:=90\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.childTrap:+2\n" +
- "golang.org/x/telemetry/internal/crashmonitor_test.TestMain.func4:+1\n" +
- "runtime.goexit:--"
- if got != want {
- t.Errorf("got counter name <<%s>>, want <<%s>>", got, want)
+ wantRE := regexp.MustCompile(`(?m)crash/crash
+runtime.gopanic:--
+runtime.panicmem:--
+runtime.sigpanic:--
+golang.org/x/telemetry/internal/crashmonitor_test.grandchildTrap:=96,.*
+golang.org/x/telemetry/internal/crashmonitor_test.childTrap:\+2,.*
+golang.org/x/telemetry/internal/crashmonitor_test.TestMain.func4:\+1,.*
+runtime.goexit:--`)
+ if !wantRE.MatchString(got) {
+ t.Errorf("got counter name <<%s>>, want match for <<%s>>", got, wantRE)
}
})
}
diff --git a/start.go b/start.go
index e34086e..9c0519a 100644
--- a/start.go
+++ b/start.go
@@ -166,7 +166,7 @@
}
childShouldUpload := config.Upload && acquireUploadToken()
- reportCrashes := config.ReportCrashes && crashmonitor.Supported()
+ reportCrashes := config.ReportCrashes
if reportCrashes || childShouldUpload {
startChild(reportCrashes, childShouldUpload, result)
@@ -267,10 +267,6 @@
os.Setenv(telemetryChildVar, "2")
upload := os.Getenv(telemetryUploadVar) == "1"
- reportCrashes := config.ReportCrashes && crashmonitor.Supported()
- uploadStartTime := config.UploadStartTime
- uploadURL := config.UploadURL
-
// The crashmonitor and/or upload process may themselves record counters.
counter.Open()
@@ -280,7 +276,7 @@
// upload to finish before exiting
var g errgroup.Group
- if reportCrashes {
+ if config.ReportCrashes {
g.Go(func() error {
crashmonitor.Child()
return nil
@@ -288,7 +284,7 @@
}
if upload {
g.Go(func() error {
- uploaderChild(uploadStartTime, uploadURL)
+ uploaderChild(config.UploadStartTime, config.UploadURL)
return nil
})
}
diff --git a/start_test.go b/start_test.go
index 8b06ef1..bb6e89d 100644
--- a/start_test.go
+++ b/start_test.go
@@ -21,7 +21,6 @@
"golang.org/x/telemetry/counter/countertest"
"golang.org/x/telemetry/internal/configtest"
ic "golang.org/x/telemetry/internal/counter"
- "golang.org/x/telemetry/internal/crashmonitor"
"golang.org/x/telemetry/internal/regtest"
it "golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
@@ -135,10 +134,6 @@
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
- if !crashmonitor.Supported() {
- t.Skip("crashmonitor not supported")
- }
-
// This test sets up a telemetry environment, and then runs a test program
// that increments a counter, and uploads via telemetry.Start.