Skip to content

Commit 0cbb439

Browse files
committed
feat(stacktrace): optimize parseSymbol with zero-allocation implementation
Replace regex-based symbol parsing with string operations to eliminate per-frame allocations in stack capture. Performance improvements: - parseSymbol: 0 allocs/op (was ~2 allocs/op) - BenchmarkCaptureWithRedaction/depth_10: 38→6 allocs/op (84% reduction) - BenchmarkCaptureWithRedaction/depth_50: 118→6 allocs/op (95% reduction) - Speed: 19.2µs→4.1µs @ depth=10 (79% faster) - Memory: 8966B→5296B @ depth=10 (41% reduction) Allocations are now constant regardless of stack depth. Signed-off-by: Kemal Akkoyun <[email protected]>
1 parent ce3c770 commit 0cbb439

File tree

1 file changed

+60
-18
lines changed

1 file changed

+60
-18
lines changed

internal/stacktrace/stacktrace.go

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package stacktrace
1010

1111
import (
1212
"errors"
13-
"regexp"
1413
"runtime"
1514
"slices"
1615
"strconv"
@@ -179,28 +178,71 @@ func (q *queue[T]) Remove() T {
179178
return item
180179
}
181180

182-
var symbolRegex = regexp.MustCompile(`^(([^(]+/)?([^(/.]+)?)(\.\(([^/)]+)\))?\.([^/()]+)$`)
183-
184-
// parseSymbol parses a symbol name into its package, receiver and function
185-
// ex: github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException
186-
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
187-
// -> receiver: *Event
188-
// -> function: NewException
181+
// parseSymbol parses a symbol name into its package, receiver and function using
182+
// zero-allocation string operations. This is a hot path called once per stack frame.
183+
//
184+
// Handles various Go symbol formats:
185+
// - Simple function: pkg.Function
186+
// - Method with receiver: pkg.(*Type).Method or pkg.(Type).Method
187+
// - Lambda/closure: pkg.Function.func1 or pkg.(*Type).Method.func1
188+
// - Generics: pkg.(*Type[...]).Method or pkg.Function[...]
189+
//
190+
// Examples:
191+
//
192+
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException
193+
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
194+
// -> receiver: *Event
195+
// -> function: NewException
196+
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc.func1
197+
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
198+
// -> receiver: ""
199+
// -> function: TestFunc.func1
189200
func parseSymbol(name string) symbol {
190-
matches := symbolRegex.FindStringSubmatch(name)
191-
if len(matches) != 7 {
192-
log.Error("Failed to parse symbol for stacktrace: %s", name)
193-
return symbol{
194-
Package: "",
195-
Receiver: "",
196-
Function: "",
201+
// Check for receiver first: pkg.(*Type) or pkg.(Type)
202+
// Look for ".(" which marks the start of a receiver
203+
if idx := strings.Index(name, ".("); idx != -1 {
204+
// Find the closing paren of the receiver
205+
receiverEnd := strings.IndexByte(name[idx+2:], ')')
206+
if receiverEnd != -1 {
207+
pkg := name[:idx]
208+
receiver := name[idx+2 : idx+2+receiverEnd]
209+
// Everything after ")." is the function (which may contain dots for lambdas)
210+
fn := name[idx+2+receiverEnd+2:] // +2 for ")."
211+
return symbol{
212+
Package: pkg,
213+
Receiver: receiver,
214+
Function: fn,
215+
}
197216
}
198217
}
199218

219+
// No receiver case: need to find where package ends and function begins
220+
// Package path ends at the last '/' followed by a segment before first '.'
221+
// Examples:
222+
// "pkg.Function" -> pkg: "pkg", fn: "Function"
223+
// "pkg.Function.func1" -> pkg: "pkg", fn: "Function.func1"
224+
// "github.com/org/pkg.Function" -> pkg: "github.com/org/pkg", fn: "Function"
225+
226+
// Find the last slash to identify where the package name starts
227+
lastSlash := strings.LastIndexByte(name, '/')
228+
229+
// Find the first dot after the last slash (or from the beginning if no slash)
230+
searchStart := 0
231+
if lastSlash != -1 {
232+
searchStart = lastSlash + 1
233+
}
234+
235+
firstDotAfterSlash := strings.IndexByte(name[searchStart:], '.')
236+
if firstDotAfterSlash == -1 {
237+
// No dots after last slash, the whole thing is the function name
238+
return symbol{Function: name}
239+
}
240+
241+
// Package ends at this dot, function starts after it
242+
pkgEnd := searchStart + firstDotAfterSlash
200243
return symbol{
201-
Package: matches[1],
202-
Receiver: matches[5],
203-
Function: matches[6],
244+
Package: name[:pkgEnd],
245+
Function: name[pkgEnd+1:], // Everything after the dot, including nested dots for lambdas
204246
}
205247
}
206248

0 commit comments

Comments
 (0)