Skip to content

Commit 19fa72c

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.
1 parent 1ed7e54 commit 19fa72c

File tree

2 files changed

+85
-18
lines changed

2 files changed

+85
-18
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016 Datadog, Inc.
5+
6+
package stacktrace
7+
8+
import "testing"
9+
10+
func BenchmarkParseSymbol(b *testing.B) {
11+
testCases := []string{
12+
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc",
13+
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException",
14+
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc.func1",
15+
"os/exec.(*Cmd).Run.func1",
16+
"test.(*Test).Method",
17+
"test.main",
18+
}
19+
20+
b.ResetTimer()
21+
for i := 0; i < b.N; i++ {
22+
for _, tc := range testCases {
23+
_ = parseSymbol(tc)
24+
}
25+
}
26+
}

internal/stacktrace/stacktrace.go

Lines changed: 59 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,70 @@ 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+
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException
192+
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
193+
// -> receiver: *Event
194+
// -> function: NewException
195+
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc.func1
196+
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
197+
// -> receiver: ""
198+
// -> function: TestFunc.func1
189199
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: "",
200+
// Check for receiver first: pkg.(*Type) or pkg.(Type)
201+
// Look for ".(" which marks the start of a receiver
202+
if idx := strings.Index(name, ".("); idx != -1 {
203+
// Find the closing paren of the receiver
204+
receiverEnd := strings.IndexByte(name[idx+2:], ')')
205+
if receiverEnd != -1 {
206+
pkg := name[:idx]
207+
receiver := name[idx+2 : idx+2+receiverEnd]
208+
// Everything after ")." is the function (which may contain dots for lambdas)
209+
fn := name[idx+2+receiverEnd+2:] // +2 for ")."
210+
return symbol{
211+
Package: pkg,
212+
Receiver: receiver,
213+
Function: fn,
214+
}
197215
}
198216
}
199217

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

0 commit comments

Comments
 (0)