Skip to content

Commit

Permalink
add ntrace for network visualising
Browse files Browse the repository at this point in the history
  • Loading branch information
cmwaters committed Apr 3, 2024
1 parent ed91647 commit 382e879
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.1
require (
cosmossdk.io/errors v1.0.1
cosmossdk.io/math v1.3.0
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af
github.com/celestiaorg/blobstream-contracts/v3 v3.1.0
github.com/celestiaorg/go-square v1.0.1
github.com/celestiaorg/go-square/merkle v0.0.0-20240117232118-fd78256df076
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I=
github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
Expand Down
32 changes: 32 additions & 0 deletions tools/ntrace/example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions tools/ntrace/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"fmt"
"log"
"time"

trace "github.com/celestiaorg/celestia-app/tools/ntrace/pkg"
)

func main() {
if err := trace.Generate("output", trainingData()); err != nil {
log.Fatalf("failed to trace: %v", err)
}
fmt.Println("done")
}

func trainingData() trace.Messages {
return []trace.Message{
{
ID: "vote1",
Node: "node1",
Type: "vote",
Timestamp: timeStamp(1 * time.Second),
},
{
ID: "vote1",
Node: "node2",
Type: "vote",
Timestamp: timeStamp(3 * time.Second),
},
{
ID: "vote2",
Node: "node3",
Type: "vote",
Timestamp: timeStamp(4 * time.Second),
},
{
ID: "vote2",
Node: "node1",
Type: "vote",
Timestamp: timeStamp(6 * time.Second),
},
{
ID: "block1",
Node: "node1",
Type: "block",
Timestamp: timeStamp(2 * time.Second),
},
{
ID: "block1",
Node: "node2",
Type: "block",
Timestamp: timeStamp(8 * time.Second),
},
}
}

func timeStamp(delta time.Duration) time.Time {
return time.Unix(16711234400, 0).Add(delta)
}
216 changes: 216 additions & 0 deletions tools/ntrace/pkg/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package trace

import (
"fmt"
"os"
"time"

svg "github.com/ajstarks/svgo"
)

type Point struct {
Node string
Timestamps time.Time
}

type Line struct {
To, From Point
Type string
}

type Lines []Line

func (l Lines) UniqueNodesCount() int {
nodeSet := make(map[string]struct{})
for _, line := range l {
nodeSet[line.To.Node] = struct{}{}
nodeSet[line.From.Node] = struct{}{}
}
return len(nodeSet)
}

func (l Lines) LongestNodeSize() int {
maxLength := 0
for _, line := range l {
toLength := len(line.To.Node)
fromLength := len(line.From.Node)
if toLength > maxLength {
maxLength = toLength
}
if fromLength > maxLength {
maxLength = fromLength
}
}
return maxLength
}

func (l Lines) TimeRange() (time.Time, time.Time) {
if len(l) == 0 {
return time.Time{}, time.Time{} // Return zero times if no lines exist
}
first := l[0].From.Timestamps // Initialize with the first line's from timestamp
last := l[0].To.Timestamps // Initialize with the first line's to timestamp

for _, line := range l {
if line.From.Timestamps.Before(first) {
first = line.From.Timestamps
}
if line.To.Timestamps.After(last) {
last = line.To.Timestamps
}
}
return first, last
}

func (l Lines) ColourMap() map[string]string {
colourMap := make(map[string]string)
colorIndex := 0

for _, line := range l {
if _, exists := colourMap[line.Type]; !exists {
colourMap[line.Type] = colours[colorIndex%len(colours)]
colorIndex++
}
}
return colourMap
}

type Message struct {
ID string // must be unique
Node string
Type string
Timestamp time.Time
}

func (m Message) IsBefore(other Message) bool {
return m.Timestamp.Before(other.Timestamp)
}

func (m Message) ToPoint() Point {
return Point{
Node: m.Node,
Timestamps: m.Timestamp,
}
}

type Messages []Message

func (m Messages) ToLines() (Lines, error) {
byID := make(map[string][]Message)
for _, message := range m {
byID[message.ID] = append(byID[message.ID], message)
}
lines := make([]Line, 0, len(byID))

for _, messageSet := range byID {
if len(messageSet) != 2 {
return nil, fmt.Errorf("message set %v has %d messages, expected 2 per ID", messageSet, len(messageSet))
}

if messageSet[0].Type != messageSet[1].Type {
return nil, fmt.Errorf("message set %v has different types, expected same type", messageSet)
}

var from, to Message
if messageSet[0].IsBefore(messageSet[1]) {
from = messageSet[0]
to = messageSet[1]
} else {
from = messageSet[1]
to = messageSet[0]
}

lines = append(lines, Line{
To: to.ToPoint(),
From: from.ToPoint(),
Type: messageSet[0].Type,
})
}
return lines, nil
}

func Generate(output string, m Messages) error {
lines, err := m.ToLines()
if err != nil {
return err
}
return GenerateFromLines(output, lines)
}

func GenerateFromLines(output string, lines Lines) error {
if len(lines) > 1000 {
return fmt.Errorf("a maximum of 1000 lines are supported, got %d", len(lines))
}
file, err := os.Create(fmt.Sprintf("%s.svg", output))
if err != nil {
return err
}
defer file.Close()

margin := 20
startTime, endTime := lines.TimeRange()
diff := endTime.Sub(startTime)
// interval := diff / 20
numNodes := lines.UniqueNodesCount()
if numNodes > 10 {
return fmt.Errorf("a maximum of 10 nodes are supported, got %d", numNodes)
}
nodeSize := lines.LongestNodeSize()
lineIndent := nodeSize*8 + margin + 10
span := 990 - lineIndent
diffPerPixel := int(diff) / span
windowHeight := numNodes*30 + margin*2
colourMap := lines.ColourMap()

canvas := svg.New(file)
canvas.Start(1000, windowHeight) // Assuming a canvas size, this may need to be adjusted

nodePositions := make(map[string]int)
currentHeight := margin // Starting height for the first node

// Draw horizontal lines for each unique node, label them, and record their Y positions
for _, line := range lines {
if _, exists := nodePositions[line.From.Node]; !exists {
canvas.Line(lineIndent, currentHeight, 990, currentHeight, "stroke:black") // Drawing a line across the canvas
canvas.Text(10, currentHeight+5, line.From.Node, "text-anchor:start;fill:black") // Labeling the line with the node name
nodePositions[line.From.Node] = currentHeight
currentHeight += 30 // Incrementing the height for the next node line
}
if _, exists := nodePositions[line.To.Node]; !exists {
canvas.Line(lineIndent, currentHeight, 990, currentHeight, "stroke:black") // Drawing a line across the canvas
canvas.Text(10, currentHeight+5, line.To.Node, "text-anchor:start;fill:black") // Labeling the line with the node name
nodePositions[line.To.Node] = currentHeight
currentHeight += 30 // Incrementing the height for the next node line
}
}

legendHeight := currentHeight + 20 // Starting height for the legend, 20 pixels below the last node
canvas.Text(10, legendHeight, "Legend:", "text-anchor:start;fill:black")
legendItemHeight := legendHeight + 20 // Starting height for the first legend item
for lineType, colour := range colourMap {
canvas.Line(10, legendItemHeight, 30, legendItemHeight, fmt.Sprintf("stroke:%s", colour)) // Drawing a coloured line for the legend
canvas.Text(35, legendItemHeight+5, lineType, "text-anchor:start;fill:black") // Labeling the line with the line type
legendItemHeight += 20 // Incrementing the height for the next legend item
}

// Draw diagonal lines between nodes based on the timestamps and add a small dot at the start and end of each line
for _, line := range lines {
fromY := nodePositions[line.From.Node]
toY := nodePositions[line.To.Node]
// Assuming timestamp can somehow be converted to an X coordinate, this logic may need to be adjusted
fromX := int(line.From.Timestamps.Sub(startTime))/diffPerPixel + lineIndent // Ensuring the line starts within canvas bounds
toX := int(line.To.Timestamps.Sub(startTime))/diffPerPixel + lineIndent // Ensuring the line ends within canvas bounds
canvas.Line(fromX, fromY, toX, toY, fmt.Sprintf("stroke:%s", colourMap[line.Type])) // Drawing a line from one node to another
canvas.Circle(fromX, fromY, 3, fmt.Sprintf("fill:%s", colourMap[line.Type])) // Adding a small dot at the start of the line
canvas.Circle(toX, toY, 3, fmt.Sprintf("fill:%s", colourMap[line.Type])) // Adding a small dot at the end of the line
}

// Adjust canvas height to fit the legend
canvas.End()
canvas.Start(1000, legendItemHeight+20) // Assuming a canvas size, this may need to be adjusted

canvas.End()
return nil
}

var colours = []string{"red", "blue", "green", "yellow", "purple", "orange", "pink", "teal", "grey", "maroon"}
13 changes: 13 additions & 0 deletions tools/ntrace/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ntrace

A tool for visualizing message passing in a distributed system based on local timestamps of the nodes that are sending and receiving messages.

## Usage

```bash
go run tools/ntrace/main.go
```

It should create a SVG file that looks like this:

![ntrace example](./example.svg)

0 comments on commit 382e879

Please sign in to comment.