Skip to content

Commit 680c360

Browse files
authored
Merge pull request #2 from pyroscope-io/improved-sampling
Refine sampler performance
2 parents bf1022b + 051c43d commit 680c360

10 files changed

+246
-343
lines changed

examples/tracing/collect.go

+3-18
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package main
22

33
import (
44
"flag"
5-
"fmt"
65
"io"
76
"log"
87
"os"
98
"os/signal"
109
"strconv"
11-
"strings"
1210

1311
"github.com/pyroscope-io/dotnetdiag"
1412
"github.com/pyroscope-io/dotnetdiag/nettrace"
@@ -74,23 +72,10 @@ func main() {
7472
case nil:
7573
continue
7674
case io.EOF:
77-
p.Walk(treePrinter(os.Stdout))
78-
log.Println("Done")
75+
for name, time := range p.Samples() {
76+
log.Println(name, time)
77+
}
7978
return
8079
}
8180
}
8281
}
83-
84-
func treePrinter(w io.Writer) func(profiler.FrameInfo) {
85-
return func(frame profiler.FrameInfo) {
86-
_, _ = fmt.Fprintf(w, "%s(%v) %s\n", padding(frame.Level), frame.SampledTime, frame.Name)
87-
}
88-
}
89-
90-
func padding(x int) string {
91-
var s strings.Builder
92-
for i := 0; i < x; i++ {
93-
s.WriteString("\t")
94-
}
95-
return s.String()
96-
}

nettrace/block.go

+15-9
Original file line numberDiff line numberDiff line change
@@ -210,18 +210,24 @@ func SequencePointBlockFromObject(o Object) (*SequencePointBlock, error) {
210210
return &b, p.Err()
211211
}
212212

213-
func (s Stack) InstructionPointers64() []uint64 {
214-
n := make([]uint64, len(s.Data)/8)
213+
func (s Stack) InstructionPointers(size int32) []uint64 {
214+
var address func([]byte, int) uint64
215+
if size == 8 {
216+
address = address64
217+
} else {
218+
address = address32
219+
}
220+
n := make([]uint64, len(s.Data)/int(size))
215221
for i := 0; i < len(n); i++ {
216-
n[i] = binary.LittleEndian.Uint64(s.Data[i*8 : (i+1)*8])
222+
n[len(n)-1-i] = address(s.Data, i)
217223
}
218224
return n
219225
}
220226

221-
func (s Stack) InstructionPointers32() []uint64 {
222-
n := make([]uint64, len(s.Data)/4)
223-
for i := 0; i < len(n); i++ {
224-
n[i] = uint64(binary.LittleEndian.Uint32(s.Data[i*4 : (i+1)*4]))
225-
}
226-
return n
227+
func address64(b []byte, i int) uint64 {
228+
return binary.LittleEndian.Uint64(b[i*8 : (i+1)*8])
229+
}
230+
231+
func address32(b []byte, i int) uint64 {
232+
return uint64(binary.LittleEndian.Uint32(b[i*4 : (i+1)*4]))
227233
}

nettrace/nettrace_test.go

+30-64
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,36 @@ import (
77
"os"
88
"runtime/debug"
99
"sort"
10-
"strings"
1110
"testing"
1211
"time"
1312

1413
"github.com/pyroscope-io/dotnetdiag/nettrace"
1514
"github.com/pyroscope-io/dotnetdiag/nettrace/profiler"
1615
)
1716

18-
func TestStream(t *testing.T) {
19-
sample, err := os.Open("testdata/golden.nettrace")
17+
func TestNetTraceDecoding(t *testing.T) {
18+
t.Run(".Net 5.0 SampleProfiler Web app", func(t *testing.T) {
19+
requireEqual(t,
20+
"testdata/dotnet-5.0-SampleProfiler-webapp.golden.nettrace",
21+
"testdata/dotnet-5.0-SampleProfiler-webapp.txt")
22+
})
23+
24+
t.Run(".Net 5.0 SampleProfiler Simple single thread app", func(t *testing.T) {
25+
requireEqual(t,
26+
"testdata/dotnet-5.0-SampleProfiler-single-thread.golden.nettrace",
27+
"testdata/dotnet-5.0-SampleProfiler-single-thread.txt")
28+
})
29+
}
30+
31+
func requireEqual(t *testing.T, sample, expected string) {
32+
t.Helper()
33+
34+
s, err := os.Open(sample)
2035
requireNoError(t, err)
21-
expected, err := os.ReadFile("testdata/expected.nettrace")
36+
e, err := os.ReadFile(expected)
2237
requireNoError(t, err)
2338

24-
stream := nettrace.NewStream(sample)
39+
stream := nettrace.NewStream(s)
2540
trace, err := stream.Open()
2641
requireNoError(t, err)
2742

@@ -39,12 +54,10 @@ func TestStream(t *testing.T) {
3954
case nil:
4055
continue
4156
case io.EOF:
42-
r := newRenderer()
43-
p.Walk(r.visitor)
4457
var b bytes.Buffer
45-
r.dumpFlat(&b)
46-
if b.String() != string(expected) {
47-
t.Fatalf("Unexpected output")
58+
dump(&b, p.Samples())
59+
if b.String() != string(e) {
60+
t.Fatal("output mismatch")
4861
}
4962
return
5063
}
@@ -58,60 +71,13 @@ func requireNoError(t *testing.T, err error) {
5871
}
5972
}
6073

61-
type renderer struct {
62-
out map[string]time.Duration
63-
names []string
64-
val time.Duration
65-
prev int
66-
}
67-
68-
func newRenderer() *renderer {
69-
return &renderer{out: make(map[string]time.Duration)}
70-
}
71-
72-
func (r *renderer) visitor(frame profiler.FrameInfo) {
73-
if frame.Level > r.prev || (frame.Level == 0 && r.prev == 0) {
74-
r.names = append(r.names, frame.Name)
75-
} else {
76-
r.complete()
77-
if frame.Level == 0 {
78-
r.names = []string{frame.Name}
79-
} else {
80-
r.names = append(r.names[:frame.Level], frame.Name)
81-
}
82-
}
83-
r.val = frame.SampledTime
84-
r.prev = frame.Level
85-
}
86-
87-
func (r *renderer) complete() {
88-
if len(r.names) > 0 {
89-
r.out[strings.Join(r.names, ";")] += r.val
90-
}
91-
}
92-
93-
func (r *renderer) dumpFlat(w io.Writer) {
94-
r.complete()
95-
s := make([]string, 0, len(r.out))
96-
for k, v := range r.out {
97-
s = append(s, fmt.Sprint(k, " ", v.Nanoseconds()))
98-
}
99-
sort.Strings(s)
100-
for _, x := range s {
101-
_, _ = fmt.Fprintln(w, x)
74+
func dump(w io.Writer, samples map[string]time.Duration) {
75+
names := make([]string, 0, len(samples))
76+
for k := range samples {
77+
names = append(names, k)
10278
}
103-
}
104-
105-
func (r *renderer) dumpTree(w io.Writer) func(profiler.FrameInfo) {
106-
return func(frame profiler.FrameInfo) {
107-
_, _ = fmt.Fprintf(w, "%s(%v) %s\n", padding(frame.Level), frame.SampledTime, frame.Name)
108-
}
109-
}
110-
111-
func padding(x int) string {
112-
var s strings.Builder
113-
for i := 0; i < x; i++ {
114-
s.WriteString("\t")
79+
sort.Strings(names)
80+
for _, n := range names {
81+
_, _ = fmt.Fprintln(w, n, samples[n].Nanoseconds())
11582
}
116-
return s.String()
11783
}

nettrace/profiler/profiler.go

+41-49
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"container/heap"
55
"encoding/binary"
66
"fmt"
7+
"strings"
78
"time"
89

910
"github.com/pyroscope-io/dotnetdiag/nettrace"
@@ -17,40 +18,43 @@ type SampleProfiler struct {
1718
stacks map[int32][]uint64
1819
threads map[int64]*thread
1920

20-
samples samples
21+
events events
22+
samples []sample
23+
}
24+
25+
type sample struct {
26+
stack []uint64
27+
val int64
2128
}
2229

2330
type FrameInfo struct {
24-
ThreadID int64
25-
Level int
2631
SampledTime time.Duration
27-
Addr uint64
2832
Name string
2933
}
3034

31-
type sample struct {
35+
type event struct {
3236
typ clrThreadSampleType
3337
threadID int64
3438
stackID int32
3539
timestamp int64
3640
relativeTime int64
3741
}
3842

39-
type samples []*sample
43+
type events []*event
4044

41-
func (s samples) Len() int { return len(s) }
45+
func (e events) Len() int { return len(e) }
4246

43-
func (s samples) Less(i, j int) bool { return s[i].timestamp < s[j].timestamp }
47+
func (e events) Less(i, j int) bool { return e[i].timestamp < e[j].timestamp }
4448

45-
func (s samples) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
49+
func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
4650

47-
func (s *samples) Push(x interface{}) { *s = append(*s, x.(*sample)) }
51+
func (e *events) Push(x interface{}) { *e = append(*e, x.(*event)) }
4852

49-
func (s *samples) Pop() interface{} {
50-
old := *s
53+
func (e *events) Pop() interface{} {
54+
old := *e
5155
n := len(old)
5256
x := old[n-1]
53-
*s = old[0 : n-1]
57+
*e = old[0 : n-1]
5458
return x
5559
}
5660

@@ -80,28 +84,16 @@ func NewSampleProfiler(trace *nettrace.Trace) *SampleProfiler {
8084
}
8185
}
8286

83-
func (s *SampleProfiler) Walk(fn func(FrameInfo)) {
84-
for tid := range s.threads {
85-
s.WalkThread(tid, fn)
86-
}
87-
}
88-
89-
func (s *SampleProfiler) WalkThread(threadID int64, fn func(FrameInfo)) {
90-
t, ok := s.threads[threadID]
91-
if !ok {
92-
return
87+
func (s *SampleProfiler) Samples() map[string]time.Duration {
88+
samples := make(map[string]time.Duration)
89+
for _, x := range s.samples {
90+
name := make([]string, len(x.stack))
91+
for i := range x.stack {
92+
name[i] = s.sym.resolve(x.stack[i])
93+
}
94+
samples[strings.Join(name, ";")] += time.Duration(x.val * -1)
9395
}
94-
t.walk(func(i int, n *frame) {
95-
addr, _ := s.sym.resolveAddress(n.addr)
96-
name, _ := s.sym.resolveString(addr)
97-
fn(FrameInfo{
98-
ThreadID: threadID,
99-
Addr: n.addr,
100-
Name: name,
101-
SampledTime: time.Duration(n.sampledTime),
102-
Level: i,
103-
})
104-
})
96+
return samples
10597
}
10698

10799
func (s *SampleProfiler) EventHandler(e *nettrace.Blob) error {
@@ -134,35 +126,35 @@ func (s *SampleProfiler) MetadataHandler(md *nettrace.Metadata) error {
134126

135127
func (s *SampleProfiler) StackBlockHandler(sb *nettrace.StackBlock) error {
136128
for _, stack := range sb.Stacks {
137-
s.addStack(stack)
129+
s.stacks[stack.ID] = stack.InstructionPointers(s.trace.PointerSize)
138130
}
139131
return nil
140132
}
141133

142134
func (s *SampleProfiler) SequencePointBlockHandler(*nettrace.SequencePointBlock) error {
143-
for s.samples.Len() != 0 {
144-
x := heap.Pop(&s.samples).(*sample)
145-
s.thread(x.threadID).addSample(x.typ, x.relativeTime, s.stacks[x.stackID])
135+
for s.events.Len() != 0 {
136+
x := heap.Pop(&s.events).(*event)
137+
s.thread(x.threadID).addSample(x.typ, x.relativeTime, x.stackID)
138+
}
139+
for _, t := range s.threads {
140+
for k, v := range t.samples {
141+
s.samples = append(s.samples, sample{
142+
stack: s.stacks[k],
143+
val: v,
144+
})
145+
}
146+
t.samples = make(map[int32]int64)
146147
}
147148
s.stacks = make(map[int32][]uint64)
148149
return nil
149150
}
150151

151-
func (s *SampleProfiler) addStack(x nettrace.Stack) {
152-
if s.trace.PointerSize == 8 {
153-
s.stacks[x.ID] = x.InstructionPointers64()
154-
return
155-
}
156-
s.stacks[x.ID] = x.InstructionPointers32()
157-
return
158-
}
159-
160152
func (s *SampleProfiler) addSample(e *nettrace.Blob) error {
161153
var d clrThreadSampleTraceData
162154
if err := binary.Read(e.Payload, binary.LittleEndian, &d); err != nil {
163155
return err
164156
}
165-
heap.Push(&s.samples, &sample{
157+
heap.Push(&s.events, &event{
166158
typ: d.Type,
167159
threadID: e.Header.ThreadID,
168160
stackID: e.Header.StackID,
@@ -177,7 +169,7 @@ func (s *SampleProfiler) thread(tid int64) *thread {
177169
if ok {
178170
return t
179171
}
180-
t = new(thread)
172+
t = &thread{samples: make(map[int32]int64)}
181173
s.threads[tid] = t
182174
return t
183175
}

0 commit comments

Comments
 (0)