Skip to content

Commit c820e2f

Browse files
RSDK-6470 Expose resource graph from debug endpoint (viamrobotics#3492)
Co-authored-by: Dan Gottlieb <[email protected]>
1 parent 254120d commit c820e2f

9 files changed

+124
-6
lines changed

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/go-gl/mathgl v1.0.0
3737
github.com/go-gnss/rtcm v0.0.3
3838
github.com/go-nlopt/nlopt v0.0.0-20230219125344-443d3362dcb5
39+
github.com/goccy/go-graphviz v0.1.2
3940
github.com/golang-jwt/jwt/v4 v4.5.0
4041
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
4142
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551
@@ -86,7 +87,7 @@ require (
8687
go.viam.com/test v1.1.1-0.20220913152726-5da9916c08a2
8788
go.viam.com/utils v0.1.59
8889
goji.io v2.0.2+incompatible
89-
golang.org/x/image v0.12.0
90+
golang.org/x/image v0.14.0
9091
golang.org/x/sys v0.13.0
9192
golang.org/x/term v0.13.0
9293
golang.org/x/time v0.3.0
@@ -366,7 +367,7 @@ require (
366367
golang.org/x/net v0.17.0 // indirect
367368
golang.org/x/oauth2 v0.10.0 // indirect
368369
golang.org/x/sync v0.3.0 // indirect
369-
golang.org/x/text v0.13.0 // indirect
370+
golang.org/x/text v0.14.0 // indirect
370371
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
371372
google.golang.org/api v0.126.0 // indirect
372373
google.golang.org/appengine v1.6.7 // indirect

go.sum

+7-4
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
248248
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
249249
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
250250
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
251+
github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA=
251252
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
252253
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
253254
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -468,6 +469,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
468469
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
469470
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
470471
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
472+
github.com/goccy/go-graphviz v0.1.2 h1:sWSJ6w13BCm/ZOUTHDVrdvbsxqN8yyzaFcHrH/hQ9Yg=
473+
github.com/goccy/go-graphviz v0.1.2/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI=
471474
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
472475
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
473476
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -1548,8 +1551,8 @@ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+o
15481551
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
15491552
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
15501553
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
1551-
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
1552-
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
1554+
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
1555+
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
15531556
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
15541557
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
15551558
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1814,8 +1817,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
18141817
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
18151818
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
18161819
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
1817-
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
1818-
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
1820+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
1821+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
18191822
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
18201823
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
18211824
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

resource/resource_graph.go

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resource
22

33
import (
4+
"fmt"
45
"strings"
56
"sync"
67
"sync/atomic"
@@ -118,6 +119,40 @@ func (g *Graph) Clone() *Graph {
118119
return g.clone()
119120
}
120121

122+
// ExportDot exports the resource graph as a DOT representation for visualization.
123+
// DOT reference: https://graphviz.org/doc/info/lang.html
124+
func (g *Graph) ExportDot() (string, error) {
125+
g.mu.Lock()
126+
defer g.mu.Unlock()
127+
128+
sb := strings.Builder{}
129+
130+
_, err := sb.WriteString("digraph {\n\tgraph [ratio=\"compress\" size=\"15,15\"]\n")
131+
if err != nil {
132+
return "", err
133+
}
134+
135+
for node := range g.nodes {
136+
line := fmt.Sprintf("\t%s;\n", node.Name)
137+
if _, err := sb.WriteString(line); err != nil {
138+
return "", err
139+
}
140+
}
141+
142+
for node, children := range g.children {
143+
for child := range children {
144+
line := fmt.Sprintf("\t%s -> %s;\n", child.Name, node.Name)
145+
if _, err := sb.WriteString(line); err != nil {
146+
return "", err
147+
}
148+
}
149+
}
150+
if _, err := sb.WriteString("}\n"); err != nil {
151+
return "", err
152+
}
153+
return sb.String(), nil
154+
}
155+
121156
func (g *Graph) clone() *Graph {
122157
return &Graph{
123158
children: copyNodeMap(g.children),

robot/impl/local_robot.go

+7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ type localRobot struct {
6767
frameSvc framesystem.Service
6868
}
6969

70+
// ExportResourcesAsDot exports the resource graph as a DOT representation for
71+
// visualization.
72+
// DOT reference: https://graphviz.org/doc/info/lang.html
73+
func (r *localRobot) ExportResourcesAsDot() (string, error) {
74+
return r.manager.ExportDot()
75+
}
76+
7077
// RemoteByName returns a remote robot by name. If it does not exist
7178
// nil is returned.
7279
func (r *localRobot) RemoteByName(name string) (robot.Robot, bool) {

robot/impl/resource_manager.go

+6
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ func fromRemoteNameToRemoteNodeName(name string) resource.Name {
8888
return resource.NewName(client.RemoteAPI, name)
8989
}
9090

91+
// ExportDot exports the resource graph as a DOT representation for visualization.
92+
// DOT reference: https://graphviz.org/doc/info/lang.html
93+
func (manager *resourceManager) ExportDot() (string, error) {
94+
return manager.resources.ExportDot()
95+
}
96+
9197
func (manager *resourceManager) startModuleManager(
9298
ctx context.Context,
9399
parentAddr string,

robot/robot.go

+5
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ type LocalRobot interface {
109109

110110
// ModuleAddress returns the address (path) of the unix socket modules use to contact the parent.
111111
ModuleAddress() (string, error)
112+
113+
// ExportResourcesAsDot exports the resource graph as a DOT representation for
114+
// visualization.
115+
// DOT reference: https://graphviz.org/doc/info/lang.html
116+
ExportResourcesAsDot() (string, error)
112117
}
113118

114119
// A RemoteRobot is a Robot that was created through a connection.

robot/web/web.go

+5
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,11 @@ func (svc *webService) initMux(options weboptions.Options) (*goji.Mux, error) {
891891
mux.HandleFunc(pat.New("/debug/pprof/trace"), pprof.Trace)
892892
}
893893

894+
// serve resource graph visualization
895+
// TODO: hide behind option
896+
// TODO: accept params to display different formats
897+
mux.HandleFunc(pat.New("/debug/graph"), svc.handleVisualizeResourceGraph)
898+
894899
prefix := "/viam"
895900
addPrefix := func(h http.Handler) http.Handler {
896901
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

robot/web/web_c.go

+52
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
package web
44

55
import (
6+
"bytes"
67
"context"
8+
"image/jpeg"
79
"math"
10+
"net/http"
811
"runtime"
912
"sync"
1013
"time"
1114

15+
"github.com/goccy/go-graphviz"
1216
"github.com/pkg/errors"
1317
streampb "go.viam.com/api/stream/v1"
1418
"go.viam.com/utils"
@@ -348,3 +352,51 @@ func (svc *webService) initStreamServer(ctx context.Context, options *weboptions
348352
}
349353
return nil
350354
}
355+
356+
func (svc *webService) handleVisualizeResourceGraph(w http.ResponseWriter, r *http.Request) {
357+
localRobot, isLocal := svc.r.(robot.LocalRobot)
358+
if !isLocal {
359+
return
360+
}
361+
dot, err := localRobot.ExportResourcesAsDot()
362+
if err != nil {
363+
return
364+
}
365+
layout := r.URL.Query().Get("layout")
366+
if layout == "text" {
367+
//nolint
368+
w.Write([]byte(dot))
369+
return
370+
}
371+
372+
gv := graphviz.New()
373+
defer func() {
374+
closeErr := gv.Close()
375+
if closeErr != nil {
376+
svc.r.Logger().Warn("failed to close graph visualizer")
377+
}
378+
}()
379+
380+
graph, err := graphviz.ParseBytes([]byte(dot))
381+
if err != nil {
382+
return
383+
}
384+
if layout != "" {
385+
gv.SetLayout(graphviz.Layout(layout))
386+
}
387+
img, err := gv.RenderImage(graph)
388+
if err != nil {
389+
//nolint
390+
w.Write([]byte(err.Error()))
391+
return
392+
}
393+
buf := new(bytes.Buffer)
394+
err = jpeg.Encode(buf, img, nil)
395+
if err != nil {
396+
return
397+
}
398+
_, err = w.Write(buf.Bytes())
399+
if err != nil {
400+
return
401+
}
402+
}

robot/web/web_notc.go

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package web
44

55
import (
66
"context"
7+
"net/http"
78
"sync"
89

910
"go.viam.com/rdk/logging"
@@ -69,3 +70,6 @@ func (svc *webService) initStreamServer(ctx context.Context, options *weboptions
6970

7071
// stub for missing gostream
7172
type options struct{}
73+
74+
// stub for missing graphviz
75+
func (svc *webService) handleVisualizeResourceGraph(w http.ResponseWriter, r *http.Request) {}

0 commit comments

Comments
 (0)