Skip to content

Commit

Permalink
feat: Add initial profiling support (#626)
Browse files Browse the repository at this point in the history
Co-authored-by: Anton Ovchinnikov <anton@tonyo.info>
Co-authored-by: Michi Hoffmann <cleptric@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 12, 2023
1 parent 2aacdfb commit 4a965bc
Show file tree
Hide file tree
Showing 20 changed files with 1,912 additions and 140 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## Unreleased
## Unrelesed

### Features

- Initial alpha support for profiling [#626](https://github.com/getsentry/sentry-go/pull/626)

### Bug fixes

Expand Down
80 changes: 80 additions & 0 deletions _examples/profiling/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// go run main.go
//
// To actually report events to Sentry, set the DSN either by editing the
// appropriate line below or setting the environment variable SENTRY_DSN to
// match the DSN of your Sentry project.
package main

import (
"context"
"fmt"
"log"
"runtime"
"sync"
"time"

"github.com/getsentry/sentry-go"
)

func main() {
err := sentry.Init(sentry.ClientOptions{
// Either set your DSN here or set the SENTRY_DSN environment variable.
Dsn: "",
// Enable printing of SDK debug messages.
// Useful when getting started or trying to figure something out.
Debug: true,
EnableTracing: true,
TracesSampleRate: 1.0,
ProfilesSampleRate: 1.0,
})

// Flush buffered events before the program terminates.
// Set the timeout to the maximum duration the program can afford to wait.
defer sentry.Flush(2 * time.Second)

if err != nil {
log.Fatalf("sentry.Init: %s", err)
}
ctx := context.Background()
tx := sentry.StartTransaction(ctx, "top")

fmt.Println("Finding prime numbers")
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func(num int) {
span := tx.StartChild(fmt.Sprintf("Goroutine %d", num))
defer span.Finish()
for i := 0; i < num; i++ {
_ = findPrimeNumber(50000)
runtime.Gosched() // we need to manually yield this busy loop
}
fmt.Printf("routine %d done\n", num)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("all")
tx.Finish()
}

func findPrimeNumber(n int) int {
count := 0
a := 2
for count < n {
b := 2
prime := true // to check if found a prime
for b*b <= a {
if a%b == 0 {
prime = false
break
}
b++
}
if prime {
count++
}
a++
}
return a - 1
}
9 changes: 9 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ type ClientOptions struct {
TracesSampleRate float64
// Used to customize the sampling of traces, overrides TracesSampleRate.
TracesSampler TracesSampler
// The sample rate for profiling traces in the range [0.0, 1.0].
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
ProfilesSampleRate float64
// List of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped.
Expand Down Expand Up @@ -371,6 +374,7 @@ func (client *Client) AddEventProcessor(processor EventProcessor) {
}

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

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

Expand Down Expand Up @@ -640,6 +645,10 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
}
}

if event.sdkMetaData.transactionProfile != nil {
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
}

return event
}

Expand Down
2 changes: 0 additions & 2 deletions example_transportwithhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,4 @@ func Example_transportWithHooks() {
defer sentry.Flush(2 * time.Second)

sentry.CaptureMessage("test")

// Output:
}
5 changes: 4 additions & 1 deletion interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const transactionType = "transaction"
// eventType is the type of an error event.
const eventType = "event"

const profileType = "profile"

// Level marks the severity of the event.
type Level string

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

// Contains information about how the name of the transaction was determined.
Expand Down
15 changes: 15 additions & 0 deletions internal/traceparser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Benchmark results

```
goos: windows
goarch: amd64
pkg: github.com/getsentry/sentry-go/internal/trace
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
BenchmarkEqualBytes-20 44323621 26.08 ns/op
BenchmarkStringEqual-20 60980257 18.27 ns/op
BenchmarkEqualPrefix-20 41369181 31.12 ns/op
BenchmarkFullParse-20 702012 1507 ns/op 1353.42 MB/s 1024 B/op 6 allocs/op
BenchmarkFramesIterator-20 1229971 969.3 ns/op 896 B/op 5 allocs/op
BenchmarkFramesReversedIterator-20 1271061 944.5 ns/op 896 B/op 5 allocs/op
BenchmarkSplitOnly-20 2250800 534.0 ns/op 3818.23 MB/s 128 B/op 1 allocs/op
```
210 changes: 210 additions & 0 deletions internal/traceparser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package traceparser

import (
"bytes"
"strconv"
)

var blockSeparator = []byte("\n\n")
var lineSeparator = []byte("\n")

// Parses multi-stacktrace text dump produced by runtime.Stack([]byte, all=true).
// The parser prioritizes performance but requires the input to be well-formed in order to return correct data.
// See https://github.com/golang/go/blob/go1.20.4/src/runtime/mprof.go#L1191
func Parse(data []byte) TraceCollection {
var it = TraceCollection{}
if len(data) > 0 {
it.blocks = bytes.Split(data, blockSeparator)
}
return it
}

type TraceCollection struct {
blocks [][]byte
}

func (it TraceCollection) Length() int {
return len(it.blocks)
}

// Returns the stacktrace item at the given index.
func (it *TraceCollection) Item(i int) Trace {
// The first item may have a leading data separator and the last one may have a trailing one.
// Note: Trim() doesn't make a copy for single-character cutset under 0x80. It will just slice the original.
var data []byte
switch {
case i == 0:
data = bytes.TrimLeft(it.blocks[i], "\n")
case i == len(it.blocks)-1:
data = bytes.TrimRight(it.blocks[i], "\n")
default:
data = it.blocks[i]
}

var splitAt = bytes.IndexByte(data, '\n')
if splitAt < 0 {
return Trace{header: data}
}

return Trace{
header: data[:splitAt],
data: data[splitAt+1:],
}
}

// Trace represents a single stacktrace block, identified by a Goroutine ID and a sequence of Frames.
type Trace struct {
header []byte
data []byte
}

var goroutinePrefix = []byte("goroutine ")

// GoID parses the Goroutine ID from the header.
func (t *Trace) GoID() (id uint64) {
if bytes.HasPrefix(t.header, goroutinePrefix) {
var line = t.header[len(goroutinePrefix):]
var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
id, _ = strconv.ParseUint(string(line[:splitAt]), 10, 64)
}
}
return id
}

// UniqueIdentifier can be used as a map key to identify the trace.
func (t *Trace) UniqueIdentifier() []byte {
return t.data
}

func (t *Trace) Frames() FrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return FrameIterator{lines: lines, i: 0, len: len(lines)}
}

func (t *Trace) FramesReversed() ReverseFrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return ReverseFrameIterator{lines: lines, i: len(lines)}
}

const framesElided = "...additional frames elided..."

// FrameIterator iterates over stack frames.
type FrameIterator struct {
lines [][]byte
i int
len int
}

// Next returns the next frame, or nil if there are none.
func (it *FrameIterator) Next() Frame {
return Frame{it.popLine(), it.popLine()}
}

func (it *FrameIterator) popLine() []byte {
switch {
case it.i >= it.len:
return nil
case string(it.lines[it.i]) == framesElided:
it.i++
return it.popLine()
default:
it.i++
return it.lines[it.i-1]
}
}

// HasNext return true if there are values to be read.
func (it *FrameIterator) HasNext() bool {
return it.i < it.len
}

// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *FrameIterator) LengthUpperBound() int {
return it.len / 2
}

// ReverseFrameIterator iterates over stack frames in reverse order.
type ReverseFrameIterator struct {
lines [][]byte
i int
}

// Next returns the next frame, or nil if there are none.
func (it *ReverseFrameIterator) Next() Frame {
var line2 = it.popLine()
return Frame{it.popLine(), line2}
}

func (it *ReverseFrameIterator) popLine() []byte {
it.i--
switch {
case it.i < 0:
return nil
case string(it.lines[it.i]) == framesElided:
return it.popLine()
default:
return it.lines[it.i]
}
}

// HasNext return true if there are values to be read.
func (it *ReverseFrameIterator) HasNext() bool {
return it.i > 1
}

// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *ReverseFrameIterator) LengthUpperBound() int {
return len(it.lines) / 2
}

type Frame struct {
line1 []byte
line2 []byte
}

// UniqueIdentifier can be used as a map key to identify the frame.
func (f *Frame) UniqueIdentifier() []byte {
// line2 contains file path, line number and program-counter offset from the beginning of a function
// e.g. C:/Users/name/scoop/apps/go/current/src/testing/testing.go:1906 +0x63a
return f.line2
}

var createdByPrefix = []byte("created by ")

func (f *Frame) Func() []byte {
if bytes.HasPrefix(f.line1, createdByPrefix) {
return f.line1[len(createdByPrefix):]
}

var end = bytes.LastIndexByte(f.line1, '(')
if end >= 0 {
return f.line1[:end]
}

return f.line1
}

func (f *Frame) File() (path []byte, lineNumber int) {
var line = f.line2
if len(line) > 0 && line[0] == '\t' {
line = line[1:]
}

var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
line = line[:splitAt]
}

splitAt = bytes.LastIndexByte(line, ':')
if splitAt < 0 {
return line, 0
}

lineNumber, _ = strconv.Atoi(string(line[splitAt+1:]))
return line[:splitAt], lineNumber
}
Loading

0 comments on commit 4a965bc

Please # to comment.