Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add initial profiling support #626

Merged
merged 57 commits into from Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
90ce800
wip: profiling
vaind Apr 14, 2023
bb5f4b7
wip profiling integration
vaind Apr 17, 2023
257500a
fix serialization issues
vaind Apr 17, 2023
cc70fdc
fix envelope item type
vaind Apr 17, 2023
47c6596
minor issues
vaind Apr 17, 2023
8040cd7
always send Frame.InApp or it's implied to be true by the server
vaind Apr 17, 2023
d458f3c
Merge branch 'master' into feat/profiling
vaind May 12, 2023
e65ea0b
add stack trace parser
vaind May 13, 2023
2771ebd
update profiler to use runtime.Stacks()
vaind May 14, 2023
5fe2611
update profiler tests
vaind May 15, 2023
b2b84c8
Merge branch 'master' into feat/profiling
vaind May 15, 2023
90b2adb
profiler refactoring and tests
vaind May 15, 2023
b838c0c
update stacktrace test
vaind May 15, 2023
b44b851
reset testProfilerPanic after use
vaind May 15, 2023
63d096d
improve profiler tests
vaind May 16, 2023
b24cea2
replace profiler integration with an option, add example
vaind May 16, 2023
4e019b8
refactorings
vaind May 17, 2023
557d239
refactoring & cleanups
vaind May 17, 2023
ce44d6b
cleanup
vaind May 17, 2023
8417a7b
update tests
vaind May 17, 2023
2f779b1
linter issues
vaind May 23, 2023
2ad4f0e
remove unnecessary int conversion
vaind May 23, 2023
e8cb25f
Update interfaces.go
vaind May 24, 2023
0b382d7
Update internal/traceparser/parser.go
vaind May 24, 2023
c5be183
review changes & refactorings
vaind May 24, 2023
1a6a6bf
Merge branch 'master' into feat/profiling
vaind May 24, 2023
9d077e0
linter issue
vaind May 24, 2023
fda50bf
fix Ticker accuracy on windows
vaind May 24, 2023
f052b8a
linter issue
vaind May 24, 2023
15ddd0a
fix flaky tests
vaind May 24, 2023
d40f02b
more test deflaking
vaind May 24, 2023
b646c20
deflaking tests
vaind May 25, 2023
514e541
more deflaking
vaind May 25, 2023
70ce655
fix test race
vaind May 25, 2023
67dda7b
linter issue
vaind May 25, 2023
7bb2653
use transaction start time for profile start time
vaind May 25, 2023
32d62ed
set ActiveThreadID
vaind May 25, 2023
26e991f
improve profiler test coverage
vaind May 29, 2023
7691c2e
add profiler overhead benchmark as a test case
vaind May 29, 2023
3b01262
use custom ticker to avoid flaky tests
vaind May 29, 2023
5bed825
fix profiler Finish() call order in relation to transaction.EndTime
vaind May 29, 2023
01987d6
fix tests in CI
vaind May 29, 2023
dc21ee9
don't run httptransport example as a test case
vaind May 29, 2023
0e14457
fix testProfilerPanic data races
vaind May 29, 2023
6882ff7
more test deflaking...
vaind May 29, 2023
cdecdf2
tune overhead test in CI
vaind May 30, 2023
c3ce952
skip overhead test in CI
vaind May 30, 2023
c488061
Update profiler.go
vaind Jun 2, 2023
e5f1467
Update profiler_test.go
vaind Jun 2, 2023
e8495a8
update changelog
vaind Jun 2, 2023
54d52ac
test: profiling envelope composition
vaind Jun 2, 2023
eb1ab3d
fix frames order in profiler stacktrace
vaind Jun 4, 2023
e678dd1
remove temp print
vaind Jun 5, 2023
985dc3e
Merge branch 'master' into feat/profiling
vaind Jun 5, 2023
1298104
Update client.go
vaind Jun 12, 2023
3bb17c9
review changes
vaind Jun 12, 2023
8bdd063
Merge branch 'master' into feat/profiling
vaind Jun 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog

## Unrelesed

### Features

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

## 0.21.0

The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.21.0.
Expand Down
80 changes: 80 additions & 0 deletions _examples/profiling/main.go
@@ -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
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 applies on top of TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
vaind marked this conversation as resolved.
Show resolved Hide resolved
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.transactionProfile != nil {
event.transactionProfile.UpdateFromEvent(event)
}

return event
}

Expand Down
8 changes: 4 additions & 4 deletions client_test.go
Expand Up @@ -79,7 +79,7 @@ func TestCaptureMessageEmptyString(t *testing.T) {
}
got := transport.lastEvent
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmpopts.IgnoreFields(Event{}, "sdkMetaData", "transactionProfile"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
Expand Down Expand Up @@ -286,7 +286,7 @@ func TestCaptureEvent(t *testing.T) {
},
}
got := transport.lastEvent
opts := cmp.Options{cmpopts.IgnoreFields(Event{}, "Release", "sdkMetaData")}
opts := cmp.Options{cmpopts.IgnoreFields(Event{}, "Release", "sdkMetaData", "transactionProfile")}
if diff := cmp.Diff(want, got, opts); diff != "" {
t.Errorf("Event mismatch (-want +got):\n%s", diff)
}
Expand Down Expand Up @@ -314,7 +314,7 @@ func TestCaptureEventNil(t *testing.T) {
}
got := transport.lastEvent
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmpopts.IgnoreFields(Event{}, "sdkMetaData", "transactionProfile"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
Expand Down Expand Up @@ -538,7 +538,7 @@ func TestRecover(t *testing.T) {
}
got := events[0]
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmpopts.IgnoreFields(Event{}, "sdkMetaData", "transactionProfile"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Message: e.Message,
Expand Down
2 changes: 0 additions & 2 deletions example_transportwithhooks_test.go
Expand Up @@ -60,6 +60,4 @@ func Example_transportWithHooks() {
defer sentry.Flush(2 * time.Second)

sentry.CaptureMessage("test")

// Output:
}
2 changes: 1 addition & 1 deletion fasthttp/sentryfasthttp_test.go
Expand Up @@ -207,7 +207,7 @@ func TestIntegration(t *testing.T) {
sentry.Event{},
"Contexts", "EventID", "Extra", "Platform", "Modules",
"Release", "Sdk", "ServerName", "Tags", "Timestamp",
"sdkMetaData",
"sdkMetaData", "transactionProfile",
),
cmpopts.IgnoreMapEntries(func(k string, v string) bool {
// fasthttp changed Content-Length behavior in
Expand Down
2 changes: 1 addition & 1 deletion http/sentryhttp_test.go
Expand Up @@ -210,7 +210,7 @@ func TestIntegration(t *testing.T) {
sentry.Event{},
"Contexts", "EventID", "Extra", "Platform", "Modules",
"Release", "Sdk", "ServerName", "Tags", "Timestamp",
"sdkMetaData",
"sdkMetaData", "transactionProfile",
),
cmpopts.IgnoreFields(
sentry.Request{},
Expand Down
4 changes: 4 additions & 0 deletions interfaces.go
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 @@ -315,6 +317,8 @@ type Event struct {
// The fields below are not part of the final JSON payload.

sdkMetaData SDKMetaData

transactionProfile *profileInfo
vaind marked this conversation as resolved.
Show resolved Hide resolved
}

// SetException appends the unwrapped errors to the event's exception list.
Expand Down
13 changes: 13 additions & 0 deletions internal/traceparser/README.md
@@ -0,0 +1,13 @@
## 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 42332671 25.99 ns/op
BenchmarkStringEqual-20 70265427 17.02 ns/op
BenchmarkEqualPrefix-20 42128026 30.14 ns/op
BenchmarkFullParse-20 738534 1501 ns/op 1358.56 MB/s 1024 B/op 6 allocs/op
BenchmarkSplitOnly-20 2298318 524.6 ns/op 3886.65 MB/s 128 B/op 1 allocs/op
```
168 changes: 168 additions & 0 deletions internal/traceparser/parser.go
@@ -0,0 +1,168 @@
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) FramesReversed() ReverseFrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return ReverseFrameIterator{lines: lines, i: len(lines)}
}

// 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}
}

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

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 elemnt 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
}