From 052c234087b5bc87a1abf5102a7d0913b91e2375 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Sun, 5 Jul 2026 16:28:48 +0200 Subject: [PATCH] perf(stacktrace): defer shortFuncName to kept frames; drop Sprintf in frame.String MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - newStacktrace: compute shortFuncName only for frames that survive filtering. Runtime/oops-internal frames (typically half the captured buffer) no longer pay the string slicing + Replacer allocation. - frame.String: direct concatenation with strconv.Itoa instead of two reflection-based fmt.Sprintf calls; runs once per frame on every stack trace formatting. - getSourceFromFrame: builtin max/min instead of lo.Max/lo.Min, which allocated a []int per call. benchstat (darwin/arm64, Apple M3, -count=10 -cpu=1): │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ New 2.339µ ± 2% 2.202µ ± 4% -5.84% (p=0.000 n=10) Wrap 2.277µ ± 3% 2.122µ ± 3% -6.79% (p=0.000 n=10) geomean 2.307µ 2.162µ -6.31% │ before.txt │ after.txt │ │ B/op │ B/op vs base │ New 2.352Ki ± 0% 2.336Ki ± 0% -0.66% (p=0.000 n=10) Wrap 2.336Ki ± 0% 2.320Ki ± 0% -0.67% (p=0.000 n=10) geomean 2.344Ki 2.328Ki -0.67% │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ New 15.00 ± 0% 13.00 ± 0% -13.33% (p=0.000 n=10) Wrap 14.00 ± 0% 12.00 ± 0% -14.29% (p=0.000 n=10) geomean 14.49 12.49 -13.81% --- sources.go | 6 ++---- stacktrace.go | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/sources.go b/sources.go index ca88fb4..bfbee56 100644 --- a/sources.go +++ b/sources.go @@ -5,8 +5,6 @@ import ( "os" "strings" "sync" - - "github.com/samber/lo" ) /// @@ -155,8 +153,8 @@ func getSourceFromFrame(frame oopsStacktraceFrame) []string { // Calculate the range of lines to extract current := frame.line - 1 // Convert to 0-based index - start := lo.Max([]int{0, current - nbrLinesBefore}) - end := lo.Min([]int{len(lines) - 1, current + nbrLinesAfter}) + start := max(0, current-nbrLinesBefore) + end := min(len(lines)-1, current+nbrLinesAfter) output := []string{} diff --git a/stacktrace.go b/stacktrace.go index bcd6ade..ce2a68b 100644 --- a/stacktrace.go +++ b/stacktrace.go @@ -5,6 +5,7 @@ import ( "reflect" "runtime" "slices" + "strconv" "strings" "github.com/samber/lo" @@ -87,12 +88,11 @@ type oopsStacktraceFrame struct { // "main.go:42 main()" // "handler.go:15 processRequest()" func (frame *oopsStacktraceFrame) String() string { - currentFrame := fmt.Sprintf("%v:%v", frame.file, frame.line) - if frame.function != "" { - currentFrame = fmt.Sprintf("%v:%v %v()", frame.file, frame.line, frame.function) + if frame.function == "" { + return frame.file + ":" + strconv.Itoa(frame.line) } - return currentFrame + return frame.file + ":" + strconv.Itoa(frame.line) + " " + frame.function + "()" } // oopsStacktrace represents a complete stack trace with multiple frames. @@ -228,9 +228,6 @@ func newStacktrace(span string, skip int) *oopsStacktrace { // Clean up the file path by removing Go path prefixes file := removeGoPath(frame.File) - // Extract a short, readable function name - function := shortFuncName(frame.Function) - // Apply frame filtering logic isGoPkg := len(goroot) > 0 && strings.Contains(file, goroot) // skip frames in GOROOT if it's set isOopsPkg := strings.Contains(file, packageName) // skip frames in this package @@ -241,8 +238,8 @@ func newStacktrace(span string, skip int) *oopsStacktrace { if !isGoPkg && (!isOopsPkg || isExamplePkg || isTestPkg) { frames = append(frames, oopsStacktraceFrame{ pc: frame.PC, + function: shortFuncName(frame.Function), file: file, - function: function, line: frame.Line, rawFile: frame.File, rawFunction: frame.Function,