Skip to content

Commit 4a965bc

Browse files
vaindtonyocleptric
authored
feat: Add initial profiling support (#626)
Co-authored-by: Anton Ovchinnikov <[email protected]> Co-authored-by: Michi Hoffmann <[email protected]>
1 parent 2aacdfb commit 4a965bc

20 files changed

+1912
-140
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

3-
## Unreleased
3+
## Unrelesed
4+
5+
### Features
6+
7+
- Initial alpha support for profiling [#626](https://github.com/getsentry/sentry-go/pull/626)
48

59
### Bug fixes
610

_examples/profiling/main.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// go run main.go
2+
//
3+
// To actually report events to Sentry, set the DSN either by editing the
4+
// appropriate line below or setting the environment variable SENTRY_DSN to
5+
// match the DSN of your Sentry project.
6+
package main
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"log"
12+
"runtime"
13+
"sync"
14+
"time"
15+
16+
"github.com/getsentry/sentry-go"
17+
)
18+
19+
func main() {
20+
err := sentry.Init(sentry.ClientOptions{
21+
// Either set your DSN here or set the SENTRY_DSN environment variable.
22+
Dsn: "",
23+
// Enable printing of SDK debug messages.
24+
// Useful when getting started or trying to figure something out.
25+
Debug: true,
26+
EnableTracing: true,
27+
TracesSampleRate: 1.0,
28+
ProfilesSampleRate: 1.0,
29+
})
30+
31+
// Flush buffered events before the program terminates.
32+
// Set the timeout to the maximum duration the program can afford to wait.
33+
defer sentry.Flush(2 * time.Second)
34+
35+
if err != nil {
36+
log.Fatalf("sentry.Init: %s", err)
37+
}
38+
ctx := context.Background()
39+
tx := sentry.StartTransaction(ctx, "top")
40+
41+
fmt.Println("Finding prime numbers")
42+
var wg sync.WaitGroup
43+
wg.Add(10)
44+
for i := 0; i < 10; i++ {
45+
go func(num int) {
46+
span := tx.StartChild(fmt.Sprintf("Goroutine %d", num))
47+
defer span.Finish()
48+
for i := 0; i < num; i++ {
49+
_ = findPrimeNumber(50000)
50+
runtime.Gosched() // we need to manually yield this busy loop
51+
}
52+
fmt.Printf("routine %d done\n", num)
53+
wg.Done()
54+
}(i)
55+
}
56+
wg.Wait()
57+
fmt.Println("all")
58+
tx.Finish()
59+
}
60+
61+
func findPrimeNumber(n int) int {
62+
count := 0
63+
a := 2
64+
for count < n {
65+
b := 2
66+
prime := true // to check if found a prime
67+
for b*b <= a {
68+
if a%b == 0 {
69+
prime = false
70+
break
71+
}
72+
b++
73+
}
74+
if prime {
75+
count++
76+
}
77+
a++
78+
}
79+
return a - 1
80+
}

client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ type ClientOptions struct {
130130
TracesSampleRate float64
131131
// Used to customize the sampling of traces, overrides TracesSampleRate.
132132
TracesSampler TracesSampler
133+
// The sample rate for profiling traces in the range [0.0, 1.0].
134+
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
135+
ProfilesSampleRate float64
133136
// List of regexp strings that will be used to match against event's message
134137
// and if applicable, caught errors type and value.
135138
// If the match is found, then a whole event will be dropped.
@@ -371,6 +374,7 @@ func (client *Client) AddEventProcessor(processor EventProcessor) {
371374
}
372375

373376
// Options return ClientOptions for the current Client.
377+
// TODO don't access this internally to avoid creating a copy each time.
374378
func (client Client) Options() ClientOptions {
375379
return client.options
376380
}
@@ -573,6 +577,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
573577

574578
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
575579
if event.EventID == "" {
580+
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
576581
event.EventID = EventID(uuid())
577582
}
578583

@@ -640,6 +645,10 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
640645
}
641646
}
642647

648+
if event.sdkMetaData.transactionProfile != nil {
649+
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
650+
}
651+
643652
return event
644653
}
645654

example_transportwithhooks_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,4 @@ func Example_transportWithHooks() {
6060
defer sentry.Flush(2 * time.Second)
6161

6262
sentry.CaptureMessage("test")
63-
64-
// Output:
6563
}

interfaces.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const transactionType = "transaction"
2020
// eventType is the type of an error event.
2121
const eventType = "event"
2222

23+
const profileType = "profile"
24+
2325
// Level marks the severity of the event.
2426
type Level string
2527

@@ -237,7 +239,8 @@ type Exception struct {
237239
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
238240
// but which shouldn't get send to Sentry.
239241
type SDKMetaData struct {
240-
dsc DynamicSamplingContext
242+
dsc DynamicSamplingContext
243+
transactionProfile *profileInfo
241244
}
242245

243246
// Contains information about how the name of the transaction was determined.

internal/traceparser/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Benchmark results
2+
3+
```
4+
goos: windows
5+
goarch: amd64
6+
pkg: github.com/getsentry/sentry-go/internal/trace
7+
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
8+
BenchmarkEqualBytes-20 44323621 26.08 ns/op
9+
BenchmarkStringEqual-20 60980257 18.27 ns/op
10+
BenchmarkEqualPrefix-20 41369181 31.12 ns/op
11+
BenchmarkFullParse-20 702012 1507 ns/op 1353.42 MB/s 1024 B/op 6 allocs/op
12+
BenchmarkFramesIterator-20 1229971 969.3 ns/op 896 B/op 5 allocs/op
13+
BenchmarkFramesReversedIterator-20 1271061 944.5 ns/op 896 B/op 5 allocs/op
14+
BenchmarkSplitOnly-20 2250800 534.0 ns/op 3818.23 MB/s 128 B/op 1 allocs/op
15+
```

internal/traceparser/parser.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package traceparser
2+
3+
import (
4+
"bytes"
5+
"strconv"
6+
)
7+
8+
var blockSeparator = []byte("\n\n")
9+
var lineSeparator = []byte("\n")
10+
11+
// Parses multi-stacktrace text dump produced by runtime.Stack([]byte, all=true).
12+
// The parser prioritizes performance but requires the input to be well-formed in order to return correct data.
13+
// See https://github.com/golang/go/blob/go1.20.4/src/runtime/mprof.go#L1191
14+
func Parse(data []byte) TraceCollection {
15+
var it = TraceCollection{}
16+
if len(data) > 0 {
17+
it.blocks = bytes.Split(data, blockSeparator)
18+
}
19+
return it
20+
}
21+
22+
type TraceCollection struct {
23+
blocks [][]byte
24+
}
25+
26+
func (it TraceCollection) Length() int {
27+
return len(it.blocks)
28+
}
29+
30+
// Returns the stacktrace item at the given index.
31+
func (it *TraceCollection) Item(i int) Trace {
32+
// The first item may have a leading data separator and the last one may have a trailing one.
33+
// Note: Trim() doesn't make a copy for single-character cutset under 0x80. It will just slice the original.
34+
var data []byte
35+
switch {
36+
case i == 0:
37+
data = bytes.TrimLeft(it.blocks[i], "\n")
38+
case i == len(it.blocks)-1:
39+
data = bytes.TrimRight(it.blocks[i], "\n")
40+
default:
41+
data = it.blocks[i]
42+
}
43+
44+
var splitAt = bytes.IndexByte(data, '\n')
45+
if splitAt < 0 {
46+
return Trace{header: data}
47+
}
48+
49+
return Trace{
50+
header: data[:splitAt],
51+
data: data[splitAt+1:],
52+
}
53+
}
54+
55+
// Trace represents a single stacktrace block, identified by a Goroutine ID and a sequence of Frames.
56+
type Trace struct {
57+
header []byte
58+
data []byte
59+
}
60+
61+
var goroutinePrefix = []byte("goroutine ")
62+
63+
// GoID parses the Goroutine ID from the header.
64+
func (t *Trace) GoID() (id uint64) {
65+
if bytes.HasPrefix(t.header, goroutinePrefix) {
66+
var line = t.header[len(goroutinePrefix):]
67+
var splitAt = bytes.IndexByte(line, ' ')
68+
if splitAt >= 0 {
69+
id, _ = strconv.ParseUint(string(line[:splitAt]), 10, 64)
70+
}
71+
}
72+
return id
73+
}
74+
75+
// UniqueIdentifier can be used as a map key to identify the trace.
76+
func (t *Trace) UniqueIdentifier() []byte {
77+
return t.data
78+
}
79+
80+
func (t *Trace) Frames() FrameIterator {
81+
var lines = bytes.Split(t.data, lineSeparator)
82+
return FrameIterator{lines: lines, i: 0, len: len(lines)}
83+
}
84+
85+
func (t *Trace) FramesReversed() ReverseFrameIterator {
86+
var lines = bytes.Split(t.data, lineSeparator)
87+
return ReverseFrameIterator{lines: lines, i: len(lines)}
88+
}
89+
90+
const framesElided = "...additional frames elided..."
91+
92+
// FrameIterator iterates over stack frames.
93+
type FrameIterator struct {
94+
lines [][]byte
95+
i int
96+
len int
97+
}
98+
99+
// Next returns the next frame, or nil if there are none.
100+
func (it *FrameIterator) Next() Frame {
101+
return Frame{it.popLine(), it.popLine()}
102+
}
103+
104+
func (it *FrameIterator) popLine() []byte {
105+
switch {
106+
case it.i >= it.len:
107+
return nil
108+
case string(it.lines[it.i]) == framesElided:
109+
it.i++
110+
return it.popLine()
111+
default:
112+
it.i++
113+
return it.lines[it.i-1]
114+
}
115+
}
116+
117+
// HasNext return true if there are values to be read.
118+
func (it *FrameIterator) HasNext() bool {
119+
return it.i < it.len
120+
}
121+
122+
// LengthUpperBound returns the maximum number of elements this stacks may contain.
123+
// The actual number may be lower because of elided frames. As such, the returned value
124+
// cannot be used to iterate over the frames but may be used to reserve capacity.
125+
func (it *FrameIterator) LengthUpperBound() int {
126+
return it.len / 2
127+
}
128+
129+
// ReverseFrameIterator iterates over stack frames in reverse order.
130+
type ReverseFrameIterator struct {
131+
lines [][]byte
132+
i int
133+
}
134+
135+
// Next returns the next frame, or nil if there are none.
136+
func (it *ReverseFrameIterator) Next() Frame {
137+
var line2 = it.popLine()
138+
return Frame{it.popLine(), line2}
139+
}
140+
141+
func (it *ReverseFrameIterator) popLine() []byte {
142+
it.i--
143+
switch {
144+
case it.i < 0:
145+
return nil
146+
case string(it.lines[it.i]) == framesElided:
147+
return it.popLine()
148+
default:
149+
return it.lines[it.i]
150+
}
151+
}
152+
153+
// HasNext return true if there are values to be read.
154+
func (it *ReverseFrameIterator) HasNext() bool {
155+
return it.i > 1
156+
}
157+
158+
// LengthUpperBound returns the maximum number of elements this stacks may contain.
159+
// The actual number may be lower because of elided frames. As such, the returned value
160+
// cannot be used to iterate over the frames but may be used to reserve capacity.
161+
func (it *ReverseFrameIterator) LengthUpperBound() int {
162+
return len(it.lines) / 2
163+
}
164+
165+
type Frame struct {
166+
line1 []byte
167+
line2 []byte
168+
}
169+
170+
// UniqueIdentifier can be used as a map key to identify the frame.
171+
func (f *Frame) UniqueIdentifier() []byte {
172+
// line2 contains file path, line number and program-counter offset from the beginning of a function
173+
// e.g. C:/Users/name/scoop/apps/go/current/src/testing/testing.go:1906 +0x63a
174+
return f.line2
175+
}
176+
177+
var createdByPrefix = []byte("created by ")
178+
179+
func (f *Frame) Func() []byte {
180+
if bytes.HasPrefix(f.line1, createdByPrefix) {
181+
return f.line1[len(createdByPrefix):]
182+
}
183+
184+
var end = bytes.LastIndexByte(f.line1, '(')
185+
if end >= 0 {
186+
return f.line1[:end]
187+
}
188+
189+
return f.line1
190+
}
191+
192+
func (f *Frame) File() (path []byte, lineNumber int) {
193+
var line = f.line2
194+
if len(line) > 0 && line[0] == '\t' {
195+
line = line[1:]
196+
}
197+
198+
var splitAt = bytes.IndexByte(line, ' ')
199+
if splitAt >= 0 {
200+
line = line[:splitAt]
201+
}
202+
203+
splitAt = bytes.LastIndexByte(line, ':')
204+
if splitAt < 0 {
205+
return line, 0
206+
}
207+
208+
lineNumber, _ = strconv.Atoi(string(line[splitAt+1:]))
209+
return line[:splitAt], lineNumber
210+
}

0 commit comments

Comments
 (0)