Skip to content

Commit 072eb40

Browse files
authored
Merge pull request #1 from pyroscope-io/feature/nettrace
NetTrace stream decoder
2 parents f676a2f + d657f28 commit 072eb40

File tree

18 files changed

+1935
-67
lines changed

18 files changed

+1935
-67
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# .Net diagnostics
22

3-
*Work In Progress*
4-
53
The package provides means for .Net runtime diagnostics implemented in Golang:
64
- [Diagnostics IPC Protocol](https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md#transport) client.
75
- [NetTrace](https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md) decoder.
86

9-
### Diagnostics IPC Protocol Client
7+
### Diagnostic IPC Client
8+
9+
```
10+
# go get github.com/pyroscope-io/dotnetdiag
11+
```
1012

1113
Supported .Net versions:
1214
- .Net 5.0
@@ -26,4 +28,17 @@ Implemented commands:
2628
- [ ] ProcessInfo
2729
- [ ] ResumeRuntime
2830

29-
See [examples](examples) directory.
31+
### NetTrace decoder
32+
33+
```
34+
# go get github.com/pyroscope-io/dotnetdiag/nettrace
35+
```
36+
37+
Supported format versions: <= 4
38+
39+
The decoder deserializes `NetTrace` binary stream to the object sequence. The package also provides an example stream
40+
handler implementation which processes **Microsoft-DotNETCore-SampleProfiler**
41+
events: [github.com/pyroscope-io/dotnetdiag/nettrace/profiler](github.com/pyroscope-io/dotnetdiag/nettrace/profiler).
42+
43+
See [examples](examples) directory.
44+

client.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,5 @@ func (s *Session) Read(b []byte) (int, error) {
116116
}
117117

118118
func (s *Session) Close() error {
119-
// TODO: close session conn?
120119
return s.c.StopTracing(s.ID)
121120
}

client_unix.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
package dotnetdiag
44

5-
import "net"
5+
import (
6+
"fmt"
7+
"net"
8+
"os"
9+
"path/filepath"
10+
"sort"
11+
)
612

713
func dial(addr string) (net.Conn, error) {
814
ua := &net.UnixAddr{
@@ -15,3 +21,12 @@ func dial(addr string) (net.Conn, error) {
1521
}
1622
return conn, nil
1723
}
24+
25+
func DefaultServerAddress(pid int) string {
26+
paths, err := filepath.Glob(fmt.Sprintf("%s/dotnet-diagnostic-%d-*-socket", os.TempDir(), pid))
27+
if err != nil || len(paths) == 0 {
28+
return ""
29+
}
30+
sort.Slice(paths, func(i, j int) bool { return paths[i] > paths[j] })
31+
return paths[0]
32+
}

dotnetdiag.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ var (
1919
// DOTNET_IPC_V1 magic header.
2020
var magic = [...]byte{0x44, 0x4F, 0x54, 0x4E, 0x45, 0x54, 0x5f, 0x49, 0x50, 0x43, 0x5F, 0x56, 0x31, 0x00}
2121

22-
// Header ...
23-
// size = 14 + 2 + 1 + 1 + 2 = 20 bytes
24-
// struct IpcHeader
25-
// {
26-
// uint8_t[14] magic = "DOTNET_IPC_V1";
27-
// uint16_t size; // size of packet = size of header + payload
28-
// uint8_t command_set; // combined with command_id is the Command to invoke
29-
// uint8_t command_id; // combined with command_set is the Command to invoke
30-
// uint16_t reserved; // for potential future use
31-
// };
3222
type Header struct {
3323
Magic [14]uint8
3424
Size uint16
@@ -39,18 +29,6 @@ type Header struct {
3929

4030
const headerSize = 20
4131

42-
// CommandSet ...
43-
// enum class CommandSet : uint8_t
44-
// {
45-
// // reserved = 0x00,
46-
// Dump = 0x01,
47-
// EventPipe = 0x02,
48-
// Profiler = 0x03,
49-
// Process = 0x04,
50-
// // future
51-
//
52-
// Server = 0xFF,
53-
// };
5432
const (
5533
_ = iota
5634
CommandSetDump
@@ -61,13 +39,6 @@ const (
6139
CommandSetServer = 0xFF
6240
)
6341

64-
// enum class EventPipeCommandId : uint8_t
65-
// {
66-
// // reserved = 0x00,
67-
// StopTracing = 0x01, // stop a given session
68-
// CollectTracing = 0x02, // create/start a given session
69-
// CollectTracing2 = 0x03, // create/start a given session with/without rundown
70-
// }
7142
const (
7243
_ = iota
7344
EventPipeStopTracing

examples/tracing/README.md

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
# Collect Tracing
22

3-
The example demonstrates how the package may be used to collect NetTrace stream data using Diagnostics IPC:
4-
the program streams traces produced with `Microsoft-DotNETCore-SampleProfiler` provider to a file.
3+
The example demonstrates how the package may be used to collect and process `NetTrace` stream data using Diagnostics IPC:
4+
the program processes events produced with **Microsoft-DotNETCore-SampleProfiler** provider and creates a sampled profile,
5+
rendered as a call tree.
56

67
1. Run dotnet application.
78
2. Find its PID, e.g.:
89
```
910
# dotnet-trace ps
1011
```
1112

12-
3. Find Diagnostics IPC socket/pipe created by the application, e.g.:
13-
- Linux/MacOS:
14-
```
15-
# lsof -p {PID} | grep dotnet-diagnostic
16-
```
17-
18-
4. Run the example program:
19-
```
20-
# collect -s {absolute-path-to-socket} -o {path-to-nettrace-output-file}
21-
```
22-
23-
5. (Optional) Verify output file:
13+
3. Build and run the example program:
2414
```
25-
# dotnet-trace convert --format speedscope -o {path-to-output-file} {path-to-nettrace-file}
15+
# go run ./examples/tracing -p {pid}
2616
```

examples/tracing/collect.go

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,30 @@ package main
22

33
import (
44
"flag"
5+
"fmt"
56
"io"
67
"log"
78
"os"
89
"os/signal"
10+
"strconv"
11+
"strings"
912

1013
"github.com/pyroscope-io/dotnetdiag"
14+
"github.com/pyroscope-io/dotnetdiag/nettrace"
15+
"github.com/pyroscope-io/dotnetdiag/nettrace/profiler"
1116
)
1217

1318
func main() {
14-
var (
15-
socketFilePath string
16-
outputFilePath = "my-traces.nettrace"
17-
)
18-
19-
flag.StringVar(&socketFilePath, "s", "", "Path to Diagnostic IPC socket")
20-
flag.StringVar(&outputFilePath, "o", "my-traces.nettrace", "Output file.")
19+
var ps string
20+
flag.StringVar(&ps, "p", "", "Target process ID")
2121
flag.Parse()
2222

23-
if socketFilePath == "" {
24-
log.Fatalln("Diagnostic IPC socket path is required.")
25-
}
26-
27-
file, err := os.Create(outputFilePath)
23+
pid, err := strconv.Atoi(ps)
2824
if err != nil {
29-
log.Fatalln(err)
25+
log.Fatalln("Invalid PID:", err)
3026
}
31-
defer file.Close()
3227

33-
c := dotnetdiag.NewClient(socketFilePath)
28+
c := dotnetdiag.NewClient(dotnetdiag.DefaultServerAddress(pid))
3429
ctc := dotnetdiag.CollectTracingConfig{
3530
CircularBufferSizeMB: 10,
3631
Providers: []dotnetdiag.ProviderConfig{
@@ -57,9 +52,45 @@ func main() {
5752
}
5853
}()
5954

60-
if _, err = io.Copy(file, sess); err != nil {
55+
// Process the stream with the sample profiler.
56+
stream := nettrace.NewStream(sess)
57+
trace, err := stream.Open()
58+
if err != nil {
59+
_ = sess.Close()
6160
log.Fatalln(err)
6261
}
6362

64-
log.Println("Done")
63+
p := profiler.NewSampleProfiler(trace)
64+
stream.EventHandler = p.EventHandler
65+
stream.MetadataHandler = p.MetadataHandler
66+
stream.StackBlockHandler = p.StackBlockHandler
67+
stream.SequencePointBlockHandler = p.SequencePointBlockHandler
68+
69+
log.Println("Collecting trace log")
70+
for {
71+
switch err = stream.Next(); err {
72+
default:
73+
log.Fatalln(err)
74+
case nil:
75+
continue
76+
case io.EOF:
77+
p.Walk(treePrinter(os.Stdout))
78+
log.Println("Done")
79+
return
80+
}
81+
}
82+
}
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()
6596
}

0 commit comments

Comments
 (0)