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

Adds eBPF spy + pyroscope connect #101

Merged
merged 3 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pkged.go
/scripts/packages/
.eslintcache
*.pyc
*.internal
.idea
.vscode
vendor/
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ RUN apk add --no-cache make

WORKDIR /opt/pyroscope

COPY package.json yarn.lock babel.config.js .eslintrc .eslintignore Makefile ./
COPY scripts ./scripts
COPY webapp ./webapp
COPY package.json yarn.lock babel.config.js .eslintrc .eslintignore Makefile ./

ARG EXTRA_METADATA=""
RUN EXTRA_METADATA=$EXTRA_METADATA make assets-release
Expand Down Expand Up @@ -76,7 +76,7 @@ COPY scripts ./scripts
COPY go.mod go.sum pyroscope.go ./
COPY Makefile ./

RUN EMBEDDED_ASSETS_DEPS="" EXTRA_LDFLAGS="-linkmode external -extldflags \"-static\"" make build-release
RUN EMBEDDED_ASSETS_DEPS="" EXTRA_LDFLAGS="-linkmode external -extldflags '-static'" make build-release


# __ _ _ _
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
GOBUILD=go build -trimpath

ENABLED_SPIES ?= "rbspy,pyspy"
ifeq ("$(shell go env GOARCH || true)", "arm64")
# this makes it work better on M1 machines
GODEBUG=asyncpreemptoff=1
endif

ifeq ("$(shell go env GOOS || true)", "linux")
ENABLED_SPIES ?= "ebpfspy,rbspy,pyspy"
else
ENABLED_SPIES ?= "rbspy,pyspy"
endif

EMBEDDED_ASSETS ?= ""
EMBEDDED_ASSETS_DEPS ?= "assets-release"
EXTRA_LDFLAGS ?= ""
Expand Down
4 changes: 2 additions & 2 deletions pkg/agent/debugspy/debugspy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ func (s *DebugSpy) Stop() error {
}

// Snapshot calls callback function with stack-trace or error.
func (s *DebugSpy) Snapshot(cb func([]byte, error)) {
func (s *DebugSpy) Snapshot(cb func([]byte, uint64, error)) {
stacktrace := fmt.Sprintf("debug_%d;debug", s.pid)
cb([]byte(stacktrace), nil)
cb([]byte(stacktrace), 1, nil)
}

func init() {
Expand Down
69 changes: 69 additions & 0 deletions pkg/agent/ebpfspy/ebpfspy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// +build ebpfspy

// Package ebpfspy provides integration with Linux eBPF. It calls profile.py from BCC tools:
// https://github.com/iovisor/bcc/blob/master/tools/profile.py
// TODO: At some point we might extract the part that starts another process because it has good potential to be reused by similar profiling tools.
package ebpfspy

import (
"sync"

"github.com/pyroscope-io/pyroscope/pkg/agent/spy"
)

type EbpfSpy struct {
resetMutex sync.Mutex
reset bool
stop bool

profilingSession *session

stopCh chan struct{}
}

func Start(pid int) (spy.Spy, error) {
s := newSession(pid)
err := s.Start()
if err != nil {
return nil, err
}
return &EbpfSpy{
profilingSession: s,
stopCh: make(chan struct{}),
}, nil
}

func (s *EbpfSpy) Stop() error {
s.stop = true
<-s.stopCh
return nil
}

func (s *EbpfSpy) Snapshot(cb func([]byte, uint64, error)) {
s.resetMutex.Lock()
defer s.resetMutex.Unlock()

if !s.reset {
return
}

s.reset = false
s.profilingSession.Reset(func(name []byte, v uint64) {
cb(name, v, nil)
})
if s.stop {
s.stopCh <- struct{}{}
s.profilingSession.Stop()
}
}

func (s *EbpfSpy) Reset() {
s.resetMutex.Lock()
defer s.resetMutex.Unlock()

s.reset = true
}

func init() {
spy.RegisterSpy("ebpfspy", Start)
}
3 changes: 3 additions & 0 deletions pkg/agent/ebpfspy/placeholder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// +build !ebpfspy

package ebpfspy
100 changes: 100 additions & 0 deletions pkg/agent/ebpfspy/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// +build ebpfspy

// Package ebpfspy provides integration with Linux eBPF.
package ebpfspy

import (
"fmt"
"os/exec"
"strconv"
"sync"
"syscall"

"github.com/pyroscope-io/pyroscope/pkg/convert"
"github.com/pyroscope-io/pyroscope/pkg/util/file"
)

type line struct {
name []byte
val int
}

type session struct {
pid int

cmd *exec.Cmd
ch chan line

stopMutex sync.Mutex
stop bool
}

const helpURL = "https://github.com/iovisor/bcc/blob/master/INSTALL.md"

var command = "/usr/share/bcc/tools/profile"

// TODO: make these configurable
var commandArgs = []string{"-F", "100", "-f", "11"}

func newSession(pid int) *session {
return &session{pid: pid}
}

func (s *session) Start() error {
if !file.Exists(command) {
return fmt.Errorf("Could not find profile.py at '%s'. Visit %s for instructions on how to install it", command, helpURL)
}

args := commandArgs
if s.pid != -1 {
args = append(commandArgs, "-p", strconv.Itoa(s.pid))
}

s.cmd = exec.Command(command, args...)
stdout, err := s.cmd.StdoutPipe()
if err != nil {
return err
}

s.ch = make(chan line)

go func() {
convert.ParseGroups(stdout, func(name []byte, val int) {
s.ch <- line{
name: name,
val: val,
}
})
stdout.Close()
close(s.ch)
}()

err = s.cmd.Start()
return err
}

func (s *session) Reset(cb func([]byte, uint64)) error {
s.cmd.Process.Signal(syscall.SIGINT)

for v := range s.ch {
cb(v.name, uint64(v.val))
}
s.cmd.Wait()

s.stopMutex.Lock()
defer s.stopMutex.Unlock()

if s.stop {
return nil
} else {
return s.Start()
}
}

func (s *session) Stop() error {
s.stopMutex.Lock()
defer s.stopMutex.Unlock()

s.stop = true
return nil
}
7 changes: 4 additions & 3 deletions pkg/agent/gospy/gospy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ func (*GoSpy) Stop() error {
}

// Snapshot calls callback function with stack-trace or error.
func (s *GoSpy) Snapshot(cb func([]byte, error)) {
func (s *GoSpy) Snapshot(cb func([]byte, uint64, error)) {
if s.selfFrame == nil {
// Determine the runtime.Frame of this func so we can hide it from our
// profiling output.
rpc := make([]uintptr, 1)
n := runtime.Callers(1, rpc)
if n < 1 {
panic("could not determine selfFrame")
// TODO: log the error
return
}
selfFrame, _ := runtime.CallersFrames(rpc).Next()
s.selfFrame = &selfFrame
Expand All @@ -60,7 +61,7 @@ func (s *GoSpy) Snapshot(cb func([]byte, error)) {
}
}
if !shouldExclude {
cb([]byte(stackStr), nil)
cb([]byte(stackStr), 1, nil)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/agent/pyspy/pyspy.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ func (s *PySpy) Stop() error {
}

// Snapshot calls callback function with stack-trace or error.
func (s *PySpy) Snapshot(cb func([]byte, error)) {
func (s *PySpy) Snapshot(cb func([]byte, uint64, error)) {
r := C.pyspy_snapshot(C.int(s.pid), s.dataPtr, C.int(bufferLength), s.errorPtr, C.int(bufferLength))
if r < 0 {
cb(nil, errors.New(string(s.errorBuf[:-r])))
cb(nil, 0, errors.New(string(s.errorBuf[:-r])))
} else {
cb(s.dataBuf[:r], nil)
cb(s.dataBuf[:r], 1, nil)
}
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/agent/rbspy/rbspy.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ func (s *RbSpy) Stop() error {
}

// Snapshot calls callback function with stack-trace or error.
func (s *RbSpy) Snapshot(cb func([]byte, error)) {
func (s *RbSpy) Snapshot(cb func([]byte, uint64, error)) {
r := C.rbspy_snapshot(C.int(s.pid), s.dataPtr, C.int(bufferLength), s.errorPtr, C.int(bufferLength))
if r < 0 {
cb(nil, errors.New(string(s.errorBuf[:-r])))
cb(nil, 0, errors.New(string(s.errorBuf[:-r])))
} else {
cb(s.dataBuf[:r], nil)
cb(s.dataBuf[:r], 1, nil)
}
}

Expand Down
19 changes: 14 additions & 5 deletions pkg/agent/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
// That's why we do a blank import here and then packages themselves register with the rest of the code.

_ "github.com/pyroscope-io/pyroscope/pkg/agent/debugspy"
_ "github.com/pyroscope-io/pyroscope/pkg/agent/ebpfspy"
_ "github.com/pyroscope-io/pyroscope/pkg/agent/gospy"
_ "github.com/pyroscope-io/pyroscope/pkg/agent/pyspy"
_ "github.com/pyroscope-io/pyroscope/pkg/agent/rbspy"
Expand Down Expand Up @@ -55,19 +56,27 @@ func (ps *ProfileSession) takeSnapshots() {
for {
select {
case <-ticker.C:
if ps.isDueForReset() {
ps.reset()
isdueToReset := ps.isDueForReset()
if isdueToReset {
for _, s := range ps.spies {
if sr, ok := s.(spy.Resettable); ok {
sr.Reset()
}
}
}
for _, spy := range ps.spies {
spy.Snapshot(func(stack []byte, err error) {
for _, s := range ps.spies {
s.Snapshot(func(stack []byte, v uint64, err error) {
if stack != nil && len(stack) > 0 {
ps.trieMutex.Lock()
defer ps.trieMutex.Unlock()

ps.trie.Insert(stack, 1, true)
ps.trie.Insert(stack, v, true)
}
})
}
if isdueToReset {
ps.reset()
}
case <-ps.stopCh:
ticker.Stop()
for _, spy := range ps.spies {
Expand Down
5 changes: 4 additions & 1 deletion pkg/agent/spy/spy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (

type Spy interface {
Stop() error
Snapshot(cb func([]byte, error))
Snapshot(cb func([]byte, uint64, error))
}
type Resettable interface {
Reset()
}

type spyIntitializer func(pid int) (Spy, error)
Expand Down
Loading