package graph
import (
"errors"
"fmt"
"maps"
"os"
"slices"
"sort"
"strings"
"github.com/dominikbraun/graph"
"github.com/dominikbraun/graph/draw"
amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states"
"github.com/pancsta/asyncmachine-go/pkg/telemetry/dbg"
ssdbg "github.com/pancsta/asyncmachine-go/tools/debugger/states"
)
var ErrGraph = errors .New ("graph error" )
func AddErrGraph (
event *am .Event , mach *am .Machine , err error , args ...am .A ,
) am .Result {
if err == nil {
return am .Executed
}
err = fmt .Errorf ("%w: %w" , ErrGraph , err )
return mach .EvAddErrState (event , ss .ErrGraph , err , am .OptArgs (args ))
}
var ss = ssdbg .ServerStates
type Vertex struct {
StateName string
MachId string
}
type Edge = graph .Edge [*Vertex ]
type EdgeData struct {
MachHas *MachineHas
MachConnectedTo bool
MachChildOf bool
MachPipesTo []*MachPipeTo
StateRelation []*StateRelation
}
type MachineHas struct {
Inherited string
Auto bool
Multi bool
}
type StateRelation struct {
RelType am .Relation
}
type MachPipeTo struct {
FromState string
ToState string
MutType am .MutationType
}
type Connection struct {
Edge *EdgeData
Source *Vertex
Target *Vertex
}
func hash(c *Vertex ) string {
if c .StateName != "" {
return c .MachId + ":" + c .StateName
}
return c .MachId
}
type Client struct {
Id string
MsgSchema *dbg .DbgMsgStruct
LatestMsgTx *dbg .DbgMsgTx
LatestTimeSum uint64
LatestMTime am .Time
ConnId string
}
type Graph struct {
Server *am .Machine
Clients map [string ]*Client
G graph .Graph [string , *Vertex ]
Map graph .Graph [string , *Vertex ]
}
func New (server *am .Machine ) (*Graph , error ) {
if !server .Has (ssdbg .ServerStates .Names ()) {
return nil , fmt .Errorf (
"Graph.New: server machine %s does not implement ssdbg.ServerStates" ,
server .Id (),
)
}
g := &Graph {
Server : server ,
G : graph .New (hash , graph .Directed ()),
Map : graph .New (hash ),
Clients : make (map [string ]*Client ),
}
return g , nil
}
func (g *Graph ) Clone () (*Graph , error ) {
c1 , err := g .G .Clone ()
if err != nil {
return nil , err
}
c2 , err := g .Map .Clone ()
if err != nil {
return nil , err
}
g2 := &Graph {
G : c1 ,
Map : c2 ,
Clients : make (map [string ]*Client , len (g .Clients )),
}
for id , c := range g .Clients {
g2 .Clients [id ] = &Client {
Id : id ,
MsgSchema : c .MsgSchema ,
LatestMTime : c .LatestMTime ,
LatestTimeSum : c .LatestTimeSum ,
}
}
return g2 , nil
}
func (g *Graph ) Clear () {
g .Clients = make (map [string ]*Client )
g .G = graph .New (hash , graph .Directed ())
g .Map = graph .New (hash )
}
func (g *Graph ) Connection (source , target string ) (*Connection , error ) {
edge , err := g .G .Edge (source , target )
if err != nil {
return nil , err
}
data := edge .Properties .Data .(*EdgeData )
targetVert , err := g .G .Vertex (target )
if err != nil {
return nil , err
}
sourceVert , err := g .G .Vertex (source )
if err != nil {
return nil , err
}
return &Connection {
Edge : data ,
Source : sourceVert ,
Target : targetVert ,
}, nil
}
func (g *Graph ) ParseMsg (id string , msgTx *dbg .DbgMsgTx ) {
c := g .Clients [id ]
var sum uint64
for _ , v := range msgTx .Clocks {
sum += v
}
index := c .MsgSchema .StatesIndex
if len (msgTx .CalledStates ) > 0 {
msgTx .CalledStatesIdxs = amhelp .StatesToIndexes (index ,
msgTx .CalledStates )
msgTx .MachineID = ""
msgTx .CalledStates = nil
}
if c .LatestMsgTx != nil {
prevTx := c .LatestMsgTx
fakeTx := &am .Transition {
TimeBefore : prevTx .Clocks ,
TimeAfter : msgTx .Clocks ,
}
added , _ , _ := amhelp .GetTransitionStates (fakeTx , index )
isRpcServer := slices .Contains (c .MsgSchema .Tags , "rpc-server" )
if slices .Contains (added , ssrpc .ServerStates .HandshakeDone ) && isRpcServer {
for _ , item := range msgTx .LogEntries {
if !strings .HasPrefix (item .Text , "[add] " ) {
continue
}
line := strings .Split (strings .TrimRight (item .Text , ")\n" ), "(" )
for _ , arg := range strings .Split (line [1 ], " " ) {
a := strings .Split (arg , "=" )
if a [0 ] != "id" {
continue
}
id := a [1 ]
data := graph .EdgeData (&EdgeData {MachConnectedTo : true })
err := g .G .AddEdge (id , c .Id , data )
if err != nil {
g .Server .Log ("waiting for RPC conn %s to show up" , id )
when := g .Server .WhenArgs (ss .InitClient , am .A {"id" : a [1 ]}, nil )
go func () {
<-when
g .Server .Log ("Resuming RPC for %s" , id )
if err := g .G .AddEdge (a [1 ], c .Id , data ); err != nil {
AddErrGraph (nil , g .Server , fmt .Errorf ("ParseMsg: %w" , err ))
return
}
if err = g .Map .AddEdge (a [1 ], c .Id ); err != nil {
AddErrGraph (nil , g .Server , fmt .Errorf ("ParseMsg: %w" , err ))
return
}
}()
} else {
if err = g .Map .AddEdge (a [1 ], c .Id ); err != nil {
AddErrGraph (nil , g .Server , fmt .Errorf ("ParseMsg: %w" , err ))
return
}
}
}
}
}
}
err := g .parseMsgLog (c , msgTx )
if err != nil {
AddErrGraph (nil , g .Server , fmt .Errorf ("parseMsgLog: %w" , err ))
}
c .LatestMsgTx = msgTx
c .LatestMTime = msgTx .Clocks
c .LatestTimeSum = sum
}
func (g *Graph ) RemoveClient (id string ) error {
return nil
}
func (g *Graph ) AddClient (msg *dbg .DbgMsgStruct ) error {
id := msg .ID
c := &Client {
Id : id ,
MsgSchema : msg ,
LatestMTime : make (am .Time , len (msg .States )),
}
g .Clients [id ] = c
var err error
if err = g .addMach (c .Id ); err != nil {
return err
}
if c .MsgSchema .Parent != "" {
data := graph .EdgeData (&EdgeData {MachChildOf : true })
err = g .G .AddEdge (c .Id , c .MsgSchema .Parent , data )
if err != nil {
g .Server .Log ("waiting for parent %s to show up" , c .MsgSchema .Parent )
when := g .Server .WhenArgs (ss .InitClient ,
am .A {"id" : c .MsgSchema .Parent }, nil )
go func () {
<-when
g .Server .Log ("resuming for %s" , c .MsgSchema .Parent )
err = g .G .AddEdge (c .Id , c .MsgSchema .Parent , data )
if err == nil {
_ = g .Map .AddEdge (c .Id , c .MsgSchema .Parent )
}
AddErrGraph (nil , g .Server , fmt .Errorf ("AddClient: %w" , err ))
}()
} else {
_ = g .Map .AddEdge (c .Id , c .MsgSchema .Parent )
}
}
for name , props := range c .MsgSchema .States {
err = g .G .AddVertex (&Vertex {
MachId : id ,
StateName : name ,
})
if err != nil {
return err
}
_ = g .Map .AddVertex (&Vertex {
MachId : id ,
StateName : name ,
})
err = g .G .AddEdge (id , id +":" +name , graph .EdgeData (&EdgeData {
MachHas : &MachineHas {
Auto : props .Auto ,
Multi : props .Multi ,
Inherited : "" ,
},
}))
if err != nil {
return err
}
_ = g .Map .AddEdge (id , id +":" +name )
}
type relation struct {
states am .S
relType am .Relation
}
for name , state := range c .MsgSchema .States {
toAdd := []relation {
{states : state .Require , relType : am .RelationRequire },
{states : state .Add , relType : am .RelationAdd },
{states : state .Remove , relType : am .RelationRemove },
}
for _ , item := range toAdd {
for _ , relState := range item .states {
from := id + ":" + name
to := id + ":" + relState
if edge , err := g .G .Edge (from , to ); err == nil {
data := edge .Properties .Data .(*EdgeData )
data .StateRelation = append (data .StateRelation , &StateRelation {
RelType : item .relType ,
})
err = g .G .UpdateEdge (from , to , graph .EdgeData (data ))
if err != nil {
return err
}
continue
}
err = g .G .AddEdge (from , to , graph .EdgeData (&EdgeData {
StateRelation : []*StateRelation {
{RelType : item .relType },
},
}))
if err != nil {
return err
}
_ = g .Map .AddEdge (from , to )
}
}
}
return nil
}
func (g *Graph ) addMach (id string ) error {
err := g .G .AddVertex (&Vertex {
MachId : id ,
})
if errors .Is (err , graph .ErrVertexAlreadyExists ) {
return nil
}
if err != nil {
return err
}
_ = g .Map .AddVertex (&Vertex {
MachId : id ,
})
return err
}
func (g *Graph ) DumpGv (path string ) error {
file , err := os .Create (path )
if err != nil {
return err
}
return draw .DOT (g .G , file )
}
type MachInspect struct {
Child string
States []string
Conns []string
Pipes map [string ][]string
Time uint64
Schema am .Schema
Tags []string
MTime am .Time
}
func (g *Graph ) Inspect () (map [string ]*MachInspect , error ) {
adjacencyMap , err := g .G .AdjacencyMap ()
if err != nil {
return nil , err
}
inspect := make (map [string ]*MachInspect )
for machId , adjs := range adjacencyMap {
if len (strings .Split (machId , ":" )) == 2 {
continue
}
c , ok := g .Clients [machId ]
if !ok {
continue
}
_, ok = inspect [machId ]
if !ok {
inspect [machId ] = &MachInspect {
Pipes : make (map [string ][]string ),
Time : c .LatestTimeSum ,
Schema : c .MsgSchema .States .Clone (),
Tags : slices .Clone (c .MsgSchema .Tags ),
MTime : slices .Clone (c .LatestMTime ),
States : slices .Clone (c .MsgSchema .StatesIndex ),
}
}
for _ , edge := range adjs {
conn , _ := g .Connection (machId , edge .Target )
if conn .Edge .MachChildOf {
inspect [machId ].Child = edge .Target
}
if conn .Edge .MachConnectedTo {
inspect [machId ].Conns = append (inspect [machId ].Conns , edge .Target )
}
if conn .Edge .MachPipesTo != nil {
for _ , pipe := range conn .Edge .MachPipesTo {
inspect [machId ].Pipes [edge .Target ] = append (
inspect [machId ].Pipes [edge .Target ], fmt .Sprintf (
"[%s] %s -> %s" , pipe .MutType , pipe .FromState , pipe .ToState ,
),
)
}
}
}
}
return inspect , nil
}
func (g *Graph ) parseMsgLog (c *Client , msgTx *dbg .DbgMsgTx ) error {
for _ , entry := range msgTx .PreLogEntries {
err := g .parseMsgReader (c , entry , msgTx )
if err != nil {
return err
}
}
for _ , entry := range msgTx .LogEntries {
err := g .parseMsgReader (c , entry , msgTx )
if err != nil {
return err
}
}
return nil
}
func (g *Graph ) parseMsgReader (
c *Client , log *am .LogEntry , tx *dbg .DbgMsgTx ,
) error {
if strings .HasPrefix (log .Text , "[pipe-in:add] " ) ||
strings .HasPrefix (log .Text , "[pipe-in:remove] " ) ||
strings .HasPrefix (log .Text , "[pipe-out:add] " ) ||
strings .HasPrefix (log .Text , "[pipe-out:remove] " ) {
isAdd := strings .HasPrefix (log .Text , "[pipe-in:add] " ) ||
strings .HasPrefix (log .Text , "[pipe-out:add] " )
isPipeOut := strings .HasPrefix (log .Text , "[pipe-out" )
var msg []string
if isPipeOut && isAdd {
msg = strings .Split (log .Text [len ("[pipe-out:add] " ):], " to " )
} else if !isPipeOut && isAdd {
msg = strings .Split (log .Text [len ("[pipe-in:add] " ):], " from " )
} else if isPipeOut && !isAdd {
msg = strings .Split (log .Text [len ("[pipe-out:remove] " ):], " to " )
} else if !isPipeOut && !isAdd {
msg = strings .Split (log .Text [len ("[pipe-in:remove] " ):], " from " )
}
mut := am .MutationRemove
if isAdd {
mut = am .MutationAdd
}
state := msg [0 ]
otherMach := msg [1 ]
var sourceMachId string
var targetMachId string
if isPipeOut {
sourceMachId = c .Id
targetMachId = otherMach
} else {
sourceMachId = otherMach
targetMachId = c .Id
}
link , linkErr := g .G .Edge (sourceMachId , targetMachId )
var data *EdgeData
if linkErr != nil {
data = &EdgeData {}
err := g .G .AddEdge (sourceMachId , targetMachId , graph .EdgeData (data ))
if err != nil {
if err = g .addMach (otherMach ); err != nil {
return err
}
err = g .G .AddEdge (sourceMachId , targetMachId , graph .EdgeData (data ))
if err != nil {
return err
}
}
_ = g .Map .AddEdge (sourceMachId , targetMachId )
} else {
data = link .Properties .Data .(*EdgeData )
}
found := false
for _ , pipe := range data .MachPipesTo {
if !isPipeOut && pipe .MutType == mut && pipe .ToState == "" {
pipe .ToState = state
found = true
} else if !isPipeOut && pipe .MutType == mut && pipe .ToState == state {
found = true
}
if isPipeOut && pipe .MutType == mut && pipe .FromState == "" {
pipe .FromState = state
found = true
} else if isPipeOut && pipe .MutType == mut && pipe .FromState == state {
found = true
}
if found {
break
}
}
if !found {
pipe := &MachPipeTo {
ToState : state ,
MutType : mut ,
}
if isPipeOut {
pipe = &MachPipeTo {
FromState : state ,
MutType : mut ,
}
}
data .MachPipesTo = append (data .MachPipesTo , pipe )
}
} else if strings .HasPrefix (log .Text , "[pipe:gc] " ) {
l := strings .Split (log .Text , " " )
id := l [1 ]
adjs , err := g .G .AdjacencyMap ()
if err != nil {
return err
}
for _ , edge := range adjs [id ] {
err := g .G .RemoveEdge (id , edge .Target )
if err != nil {
return err
}
_ = g .Map .RemoveEdge (id , edge .Target )
}
preds , err := g .G .PredecessorMap ()
if err != nil {
return err
}
for _ , edge := range preds [id ] {
err := g .G .RemoveEdge (edge .Source , id )
if err != nil {
return err
}
_ = g .Map .RemoveEdge (edge .Source , id )
}
}
return nil
}
func Markdown (inspect map [string ]*MachInspect ) string {
keys := slices .Collect (maps .Keys (inspect ))
sort .Strings (keys )
ret := "# am-vis inspect-dump\n\n"
for _ , machId := range keys {
data := inspect [machId ]
ret += "## " + machId + "\n"
ret += fmt .Sprintf ("Time: t%d\n" , data .Time )
if data .Child != "" {
ret += fmt .Sprintf ("Parent: %s\n\n" , data .Child )
} else {
ret += "\n"
}
if len (data .States ) > 0 {
sort .Strings (data .States )
ret += "### States\n"
ret += fmt .Sprintf ("- %s\n" , strings .Join (data .States , "\n- " ))
ret += "\n"
}
if len (data .Conns ) > 0 {
sort .Strings (data .Conns )
ret += "### RPC\n"
ret += fmt .Sprintf ("- %s\n" , strings .Join (data .Conns , "\n- " ))
ret += "\n"
}
if len (data .Pipes ) > 0 {
ret += "### Pipes\n\n"
keysPipes := slices .Collect (maps .Keys (data .Pipes ))
sort .Strings (keysPipes )
for _ , pipe := range keysPipes {
sort .Strings (data .Pipes [pipe ])
ret += fmt .Sprintf ("#### %s\n" , pipe )
ret += fmt .Sprintf ("- %s\n" , strings .Join (data .Pipes [pipe ], "\n- " ))
ret += "\n"
}
}
ret += "-----\n\n"
}
return ret
}
func Markup (inspect map [string ]*MachInspect ) string {
children := make (map [string ][]string )
var roots []string
for id , data := range inspect {
if data .Child == "" {
roots = append (roots , id )
} else {
children [data .Child ] = append (children [data .Child ], id )
}
}
sort .Strings (roots )
for k := range children {
sort .Strings (children [k ])
}
ret := "<graph>\n"
for _ , id := range roots {
ret += "\n"
ret += writeMachine (inspect , children , id , 1 )
}
ret += "</graph>\n"
return ret
}
func writePipe(target , pipe , pad string ) string {
body := pipe
attrs := fmt .Sprintf (" to=%q" , target )
if strings .HasPrefix (pipe , "[" ) {
if close := strings .Index (pipe , "]" ); close > 1 {
mut := strings .TrimSpace (pipe [1 :close ])
rest := strings .TrimSpace (pipe [close +1 :])
if mut == "remove" {
attrs += fmt .Sprintf (" add=\"0\"" )
} else {
attrs += fmt .Sprintf (" add=\"1\"" )
}
from , to , ok := strings .Cut (rest , " -> " )
if ok {
from = strings .TrimSpace (from )
to = strings .TrimSpace (to )
if from != "" {
attrs += fmt .Sprintf (" as=%q" , from )
}
switch {
case to != "" :
body = to
case from != "" :
body = from
default :
body = rest
}
} else {
body = rest
}
}
}
return fmt .Sprintf ("%s<pipe%s>%s</pipe>\n" , pad , attrs , body )
}
func writeMachine(
inspect map [string ]*MachInspect , children map [string ][]string , id string ,
indent int ,
) string {
data := inspect [id ]
pad := strings .Repeat (" " , indent )
inner := pad + " "
ret := fmt .Sprintf ("%s<machine id=%q time=\"%d\">\n" , pad , id , data .Time )
states := slices .Clone (data .States )
sort .Strings (states )
for _ , s := range states {
ret += writeState (s , data , inner )
}
conns := slices .Clone (data .Conns )
sort .Strings (conns )
for _ , c := range conns {
ret += fmt .Sprintf ("%s<rpc>%s</rpc>\n" , inner , c )
}
pipeKeys := slices .Collect (maps .Keys (data .Pipes ))
sort .Strings (pipeKeys )
for _ , target := range pipeKeys {
pipes := slices .Clone (data .Pipes [target ])
sort .Strings (pipes )
for _ , p := range pipes {
ret += writePipe (target , p , inner )
}
}
tags := slices .Clone (data .Tags )
sort .Strings (tags )
for _ , tag := range tags {
ret += fmt .Sprintf ("%s<tag>%s</tag>\n" , inner , tag )
}
for _ , childId := range children [id ] {
ret += "\n"
ret += writeMachine (inspect , children , childId , indent +1 )
}
ret += pad + "</machine>\n"
return ret
}
func writeState(name string , data *MachInspect , pad string ) string {
state , ok := data .Schema [name ]
if !ok {
return fmt .Sprintf ("%s<state>%s</state>\n" , pad , name )
}
attrs := ""
if idx := slices .Index (data .States , name ); idx >= 0 && idx < len (data .MTime ) {
attrs += fmt .Sprintf (` tick="%d"` , data .MTime [idx ])
if am .IsActiveTick (data .MTime [idx ]) {
attrs += fmt .Sprintf (` active="1"` )
}
}
if state .Auto {
attrs += ` auto="1"`
}
if state .Multi {
attrs += ` multi="1"`
}
hasRelations := len (state .Require ) > 0 || len (state .Add ) > 0 ||
len (state .Remove ) > 0 || len (state .After ) > 0
if !hasRelations {
return fmt .Sprintf ("%s<state%s>%s</state>\n" , pad , attrs , name )
}
ret := fmt .Sprintf ("%s<state%s>\n" , pad , attrs )
ret += fmt .Sprintf ("%s %s\n" , pad , name )
writeRelations := func (tag string , rels am .S ) {
if len (rels ) == 0 {
return
}
items := slices .Clone (rels )
sort .Strings (items )
for _ , rel := range items {
ret += fmt .Sprintf ("%s <%s>%s</%s>\n" , pad , tag , rel , tag )
}
}
writeRelations ("require" , state .Require )
writeRelations ("add" , state .Add )
writeRelations ("remove" , state .Remove )
writeRelations ("after" , state .After )
ret += fmt .Sprintf ("%s</state>\n" , pad )
return ret
}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .