go-hep.org/x/hep@v0.38.1/fwk/dflow.go (about)

     1  // Copyright ©2017 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package fwk
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  
    14  	"go-hep.org/x/hep/fwk/utils/tarjan"
    15  	"gonum.org/v1/gonum/graph/encoding"
    16  	"gonum.org/v1/gonum/graph/encoding/dot"
    17  	"gonum.org/v1/gonum/graph/simple"
    18  )
    19  
    20  type node struct {
    21  	in  map[string]reflect.Type
    22  	out map[string]reflect.Type
    23  }
    24  
    25  func newNode() *node {
    26  	return &node{
    27  		in:  make(map[string]reflect.Type),
    28  		out: make(map[string]reflect.Type),
    29  	}
    30  }
    31  
    32  // dflowsvc models and describes the runtime data-flow and (data) dependencies between
    33  // components as declared during configuration.
    34  type dflowsvc struct {
    35  	SvcBase
    36  	nodes map[string]*node
    37  	edges map[string]reflect.Type
    38  
    39  	dotfile string // path to a DOT file where to dump the data dependency graph.
    40  }
    41  
    42  func (svc *dflowsvc) Configure(ctx Context) error {
    43  	return nil
    44  }
    45  
    46  func (svc *dflowsvc) StartSvc(ctx Context) error {
    47  	var err error
    48  
    49  	// sort node-names for reproducibility
    50  	nodenames := make([]string, 0, len(svc.nodes))
    51  	for n := range svc.nodes {
    52  		nodenames = append(nodenames, n)
    53  	}
    54  	sort.Strings(nodenames)
    55  
    56  	// - make sure all input keys of components are available
    57  	//   as output keys of a task
    58  	// - also detect whether a key is labeled as an out-port
    59  	//   by 2 different components
    60  	out := make(map[string]string) // outport-name -> producer-name
    61  	for _, tsk := range nodenames {
    62  		node := svc.nodes[tsk]
    63  		for k := range node.out {
    64  			n, dup := out[k]
    65  			if dup {
    66  				return fmt.Errorf("%s: component [%s] already declared port [%s] as its output (current=%s)",
    67  					svc.Name(), n, k, tsk,
    68  				)
    69  			}
    70  			out[k] = tsk
    71  		}
    72  	}
    73  
    74  	for _, tsk := range nodenames {
    75  		node := svc.nodes[tsk]
    76  		for k := range node.in {
    77  			_, ok := out[k]
    78  			if !ok {
    79  				return fmt.Errorf("%s: component [%s] declared port [%s] as input but NO KNOWN producer",
    80  					svc.Name(), tsk, k,
    81  				)
    82  			}
    83  		}
    84  	}
    85  
    86  	// detect cycles.
    87  	graph := make(map[any][]any)
    88  	for _, n := range nodenames {
    89  		node := svc.nodes[n]
    90  		graph[n] = []any{}
    91  		for in := range node.in {
    92  			for _, o := range nodenames {
    93  				if o == n {
    94  					continue
    95  				}
    96  				onode := svc.nodes[o]
    97  				connected := false
    98  				for out := range onode.out {
    99  					if in == out {
   100  						connected = true
   101  						break
   102  					}
   103  				}
   104  				if connected {
   105  					graph[n] = append(graph[n], o)
   106  				}
   107  			}
   108  		}
   109  	}
   110  
   111  	cycles := tarjan.Connections(graph)
   112  	if len(cycles) > 0 {
   113  		msg := ctx.Msg()
   114  		ncycles := 0
   115  		for _, cycle := range cycles {
   116  			if len(cycle) > 1 {
   117  				ncycles++
   118  				msg.Errorf("cycle detected: %v\n", cycle)
   119  			}
   120  		}
   121  		s := ""
   122  		if ncycles > 1 {
   123  			s = "s"
   124  		}
   125  		if ncycles > 0 {
   126  			return fmt.Errorf("%s: cycle%s detected: %d", svc.Name(), s, ncycles)
   127  		}
   128  	}
   129  
   130  	if svc.dotfile != "" {
   131  		err = svc.dumpGraph()
   132  		if err != nil {
   133  			return err
   134  		}
   135  	}
   136  	return err
   137  }
   138  
   139  func (svc *dflowsvc) StopSvc(ctx Context) error {
   140  	return nil
   141  }
   142  
   143  func (svc *dflowsvc) keys() []string {
   144  	keys := make([]string, 0, len(svc.edges))
   145  	for k := range svc.edges {
   146  		keys = append(keys, k)
   147  	}
   148  	return keys
   149  }
   150  
   151  func (svc *dflowsvc) addInNode(tsk string, name string, t reflect.Type) error {
   152  	node, ok := svc.nodes[tsk]
   153  	if !ok {
   154  		node = newNode()
   155  		svc.nodes[tsk] = node
   156  	}
   157  	_, ok = node.in[name]
   158  	if ok {
   159  		return fmt.Errorf(
   160  			"fwk.DeclInPort: component [%s] already declared in-port with name [%s]",
   161  			tsk,
   162  			name,
   163  		)
   164  	}
   165  
   166  	node.in[name] = t
   167  	edgetyp, dup := svc.edges[name]
   168  	if dup {
   169  		// make sure types match
   170  		if edgetyp != t {
   171  			type elemT struct {
   172  				port string // in/out
   173  				task string // task which defined the port
   174  				typ  reflect.Type
   175  			}
   176  			cont := []elemT{}
   177  			nodenames := make([]string, 0, len(svc.nodes))
   178  			for tskname := range svc.nodes {
   179  				nodenames = append(nodenames, tskname)
   180  			}
   181  			sort.Strings(nodenames)
   182  			for _, tskname := range nodenames {
   183  				node := svc.nodes[tskname]
   184  				for k, in := range node.in {
   185  					if k != name {
   186  						continue
   187  					}
   188  					cont = append(cont,
   189  						elemT{
   190  							port: "in ",
   191  							task: tskname,
   192  							typ:  in,
   193  						},
   194  					)
   195  				}
   196  				for k, out := range node.out {
   197  					if k != name {
   198  						continue
   199  					}
   200  					cont = append(cont,
   201  						elemT{
   202  							port: "out",
   203  							task: tskname,
   204  							typ:  out,
   205  						},
   206  					)
   207  				}
   208  			}
   209  			var o strings.Builder
   210  			fmt.Fprintf(&o, "fwk.DeclInPort: detected type inconsistency for port [%s]:\n", name)
   211  			for _, c := range cont {
   212  				fmt.Fprintf(&o, " component=%q port=%s type=%v\n", c.task, c.port, c.typ)
   213  			}
   214  			return fmt.Errorf("%v", o.String())
   215  		}
   216  	}
   217  
   218  	svc.edges[name] = t
   219  	return nil
   220  }
   221  
   222  func (svc *dflowsvc) addOutNode(tsk string, name string, t reflect.Type) error {
   223  	node, ok := svc.nodes[tsk]
   224  	if !ok {
   225  		node = newNode()
   226  		svc.nodes[tsk] = node
   227  	}
   228  	_, ok = node.out[name]
   229  	if ok {
   230  		return fmt.Errorf(
   231  			"fwk.DeclOutPort: component [%s] already declared out-port with name [%s]",
   232  			tsk,
   233  			name,
   234  		)
   235  	}
   236  
   237  	node.out[name] = t
   238  
   239  	edgetyp, dup := svc.edges[name]
   240  	if dup {
   241  		// edge already exists
   242  		// loop over nodes, find out who already defined that edge
   243  		nodenames := make([]string, 0, len(svc.nodes))
   244  		for tskname := range svc.nodes {
   245  			nodenames = append(nodenames, tskname)
   246  		}
   247  		sort.Strings(nodenames)
   248  		for _, duptsk := range nodenames {
   249  			dupnode := svc.nodes[duptsk]
   250  			if duptsk == tsk {
   251  				continue
   252  			}
   253  			for out := range dupnode.out {
   254  				if out == name {
   255  					return fmt.Errorf(
   256  						"fwk.DeclOutPort: component [%s] already declared out-port with name [%s (type=%v)].\nfwk.DeclOutPort: component [%s] is trying to add a duplicate out-port [%s (type=%v)]",
   257  						duptsk,
   258  						name,
   259  						edgetyp,
   260  						tsk,
   261  						name,
   262  						t,
   263  					)
   264  				}
   265  			}
   266  		}
   267  	}
   268  	svc.edges[name] = t
   269  	return nil
   270  }
   271  
   272  type nodeFlow struct {
   273  	simple.Node
   274  	attrs []encoding.Attribute
   275  }
   276  
   277  func (n *nodeFlow) Attributes() []encoding.Attribute {
   278  	return n.attrs
   279  }
   280  
   281  func (svc *dflowsvc) dumpGraph() error {
   282  	var err error
   283  	gr := simple.NewDirectedGraph()
   284  
   285  	quote := func(s string) string {
   286  		return fmt.Sprintf("%q", s)
   287  	}
   288  
   289  	id := int64(0)
   290  	ids := make(map[string]*nodeFlow, len(svc.edges)+len(svc.nodes))
   291  
   292  	{
   293  		keys := make([]string, 0, len(svc.edges))
   294  		for edge := range svc.edges {
   295  			keys = append(keys, edge)
   296  		}
   297  		sort.Strings(keys)
   298  
   299  		for _, edge := range keys {
   300  			id++
   301  			node := &nodeFlow{
   302  				simple.Node(id),
   303  				[]encoding.Attribute{
   304  					{Key: `"node"`, Value: `"data"`},
   305  					{Key: `"label"`, Value: quote(edge)},
   306  				},
   307  			}
   308  			ids["data-"+edge] = node
   309  			gr.AddNode(node)
   310  		}
   311  
   312  		keys = keys[:0]
   313  		for name := range svc.nodes {
   314  			keys = append(keys, name)
   315  		}
   316  		sort.Strings(keys)
   317  
   318  		for _, name := range keys {
   319  			id++
   320  			node := &nodeFlow{
   321  				simple.Node(id),
   322  				[]encoding.Attribute{
   323  					{Key: `"node"`, Value: `"task"`},
   324  					{Key: `"shape"`, Value: `"component"`},
   325  					{Key: `"label"`, Value: quote(name)},
   326  				},
   327  			}
   328  			ids["task-"+name] = node
   329  			gr.AddNode(node)
   330  		}
   331  	}
   332  
   333  	for name, node := range svc.nodes {
   334  		for in := range node.in {
   335  			from := ids["data-"+in]
   336  			to := ids["task-"+name]
   337  			gr.SetEdge(simple.Edge{
   338  				F: from,
   339  				T: to,
   340  			})
   341  		}
   342  
   343  		for out := range node.out {
   344  			from := ids["task-"+name]
   345  			to := ids["data-"+out]
   346  			gr.SetEdge(simple.Edge{
   347  				F: from,
   348  				T: to,
   349  			})
   350  		}
   351  	}
   352  
   353  	out, err := dot.Marshal(gr, "dataflow", "", "  ")
   354  	if err != nil {
   355  		return fmt.Errorf("could not marshal dataflow to 'dot' format: %w", err)
   356  	}
   357  	out = append(out, '\n')
   358  
   359  	err = os.WriteFile(svc.dotfile, out, 0644)
   360  	if err != nil {
   361  		return fmt.Errorf("could not write dataflow to dot-file %q: %w", svc.dotfile, err)
   362  	}
   363  
   364  	return err
   365  }
   366  
   367  func newDataFlowSvc(typ, name string, mgr App) (Component, error) {
   368  	var err error
   369  	svc := &dflowsvc{
   370  		SvcBase: NewSvc(typ, name, mgr),
   371  		nodes:   make(map[string]*node),
   372  		edges:   make(map[string]reflect.Type),
   373  		dotfile: "", // empty: no dump
   374  	}
   375  
   376  	err = svc.DeclProp("DotFile", &svc.dotfile)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	return svc, err
   382  }
   383  
   384  func init() {
   385  	Register(reflect.TypeOf(dflowsvc{}), newDataFlowSvc)
   386  }