Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gnovm): tool for generating dependency graphs #2635

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

bmilojkovic
Copy link

This is related to issue #2478. For now, this is just an implementation of the graph generation, no CI/CD yet. The tool creates one or more .dot files that can be used to generate an SVG via graphviz. To use the tool, just do the following:

cd gnovm
make build
./build/gno depgraph -o depgraph.dot ../examples

DOT is a graphviz language for describing graphs. It includes layout information as well. For the final step which makes an SVG you need to have graphviz installed.

dot -Tsvg -o depgraph.svg depgraph.dot

The graph below will be produced. I tried several different layout schemes provided by graphviz, and I'm pretty sure that the sheer number of packages and connections is probably always going to make a bit of a mess. This particular layout doesn't clearly show what exactly is being referenced, however it DOES clearly show how often packages are being referenced (blue nodes are p/ and red nodes are r/).

graphs

If you would like to more clearly see requires for a specific package, the tool can also be invoked with the -m flag ("multiple graphs") which produces a hierarchy of directories mimicking the input structure with a .dot file at the bottom for each package. For now you would need to manually convert those to SVG files. Example usage:

./build/gno depgraph -m -o depgraph ../examples

The next steps from here could be:

  • Develop CI that would automatically produce this SVG and include it in a README so we can easily track examples.
  • Add a flag to skip .dot and output directly to SVG (this would require graphviz to be installed ofc)
  • Add some layout and visual customization options? This might be overengineering it. I'm not sure if this is something we realistically need down the line. Maybe we can identify several useful layouts and just support those. For now, the layout is hardcoded.
Contributors' checklist...
  • Added new tests, or not needed, or not feasible
  • Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory
  • Updated the official documentation or not needed
  • No breaking changes were made, or a BREAKING CHANGE: xxx message was included in the description
  • Added references to related issues and PRs
  • Provided any useful hints for running manual tests
  • Added new benchmarks to generated graphs, if any. More info here.

@github-actions github-actions bot added the 📦 🤖 gnovm Issues or PRs gnovm related label Jul 27, 2024
@zivkovicmilos zivkovicmilos self-requested a review July 29, 2024 09:51
Copy link

codecov bot commented Jul 29, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 60.11%. Comparing base (bbcb2f6) to head (5e4bb45).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2635      +/-   ##
==========================================
- Coverage   60.11%   60.11%   -0.01%     
==========================================
  Files         560      560              
  Lines       74918    74918              
==========================================
- Hits        45039    45037       -2     
- Misses      26504    26505       +1     
- Partials     3375     3376       +1     
Flag Coverage Δ
contribs/gnodev 61.40% <ø> (ø)
contribs/gnofaucet 14.46% <ø> (-0.86%) ⬇️
gno.land 64.17% <ø> (ø)
gnovm 64.13% <ø> (ø)
misc/genstd 80.54% <ø> (ø)
misc/logos 19.88% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@thehowl thehowl requested a review from gfanton July 29, 2024 17:31
Copy link
Member

@thehowl thehowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gfanton for good taste in tools and a first review :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put the generating source code in gnovm/pkg/depgraph, so we can hope to reuse it?

I'm also thinking how to have this on the CLI; having it as a subcommand of gno doesn't feel right. I feel like in go this could be go tool xxx; idk.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea i agree, having it in gno tool could actually be a good idea. We could also decide to put this kind of tools inside the contribs folder, at last for now, an move it in the core code when it's mature enough and/or we find out that we use it really often. wdyt ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Copy link
Member

@gfanton gfanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super cool! (check my comments for requested changes)

As Morgan says, I'm not sure about keeping this in the root of gno cli. I think we should either move this to contribs or a tool subcommand. cc @moul, wdyt?

Comment on lines 110 to 111
file.Close()
} else { // useful for testing - makes a separate graph for each found package
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return early here to avoid enclosing the rest of the method in an else statement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea i agree, having it in gno tool could actually be a good idea. We could also decide to put this kind of tools inside the contribs folder, at last for now, an move it in the core code when it's mature enough and/or we find out that we use it really often. wdyt ?

Comment on lines 96 to 102
for _, pkg := range allPkgs {
err := buildGraphData(pkg, allPkgs, make(map[string]bool), make(map[string]bool), &graphData)

if err != nil {
return fmt.Errorf("error in building graph: %w", err)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole purpose of passing the visited map to this method is to keep track of already visited packages. You need to move the declaration of the visited map above the for loop to ensure there will be no duplicates while iterating between packages.

Suggested change
for _, pkg := range allPkgs {
err := buildGraphData(pkg, allPkgs, make(map[string]bool), make(map[string]bool), &graphData)
if err != nil {
return fmt.Errorf("error in building graph: %w", err)
}
}
visited := make(map[string]bool)
for _, pkg := range allPkgs {
err := buildGraphData(pkg, allPkgs, visited, make(map[string]bool), &graphData)
if err != nil {
return fmt.Errorf("error in building graph: %w", err)
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh! Yeah, I was thinking about solving that with a different structure, but this is simpler.

Comment on lines 164 to 199
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error {
if onStack[pkg.Name] {
return fmt.Errorf("cycle detected: %s", pkg.Name)
}
if visited[pkg.Name] {
return nil
}

visited[pkg.Name] = true
onStack[pkg.Name] = true

for _, req := range pkg.Requires {
found := false

for _, candidate := range allPkgs {
if candidate.Name != req {
continue
}
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil {
return err
}
found = true
// this check is wildly inefficient - change this to not work with strings directly
if !strings.Contains(*graphData, "\""+pkg.Name+"\" -> \""+req+"\"\n") {
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n"
}
}
if !found {
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name)
}
}

onStack[pkg.Name] = false

return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to check if the line already exists because you are already ensuring that you visit the package and requirements only once.

Suggested change
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error {
if onStack[pkg.Name] {
return fmt.Errorf("cycle detected: %s", pkg.Name)
}
if visited[pkg.Name] {
return nil
}
visited[pkg.Name] = true
onStack[pkg.Name] = true
for _, req := range pkg.Requires {
found := false
for _, candidate := range allPkgs {
if candidate.Name != req {
continue
}
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil {
return err
}
found = true
// this check is wildly inefficient - change this to not work with strings directly
if !strings.Contains(*graphData, "\""+pkg.Name+"\" -> \""+req+"\"\n") {
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n"
}
}
if !found {
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name)
}
}
onStack[pkg.Name] = false
return nil
}
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error {
if onStack[pkg.Name] {
return fmt.Errorf("cycle detected: %s", pkg.Name)
}
if visited[pkg.Name] {
return nil
}
visited[pkg.Name] = true
onStack[pkg.Name] = true
for _, req := range pkg.Requires {
var candidate gnomod.Pkg
var found bool
for _, candidate = range allPkgs {
if candidate.Name != req {
continue
}
found = true
break
}
if !found {
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name)
}
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil {
return err
}
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n"
}
onStack[pkg.Name] = false
return nil
}

@bmilojkovic bmilojkovic requested a review from a team as a code owner August 4, 2024 10:31
@leohhhn leohhhn linked an issue Aug 4, 2024 that may be closed by this pull request
Comment on lines 74 to 75
nodeData := "" // nodes
graphData := "" // edges
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Using a strings.Builder or bytes.Buffer along with fmt.Fprintf and passing an io.Writer to buildGraphData would have been a cleaner approach than directly writing the output to a string directly.

@moul
Copy link
Member

moul commented Aug 31, 2024

In my opinion, a suitable location for this tool would be in the 'gno mod graph'. It does not appear to import any new dependencies and is closely integrated with gnomod.

fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning")
fs.StringVar(&c.rootDir, "root-dir", rootDir, "clone location of github.com/gnolang/gno (gno tries to guess it)")
fs.StringVar(&c.output, "o", "depgraph", "output (file if single graph, dir if multiple graphs)")
fs.BoolVar(&c.multipleGraphs, "m", false, "make a separate graph for each package")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this flag, you should always generate a single graph.

rootDir := gnoenv.RootDir()
fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning")
fs.StringVar(&c.rootDir, "root-dir", rootDir, "clone location of github.com/gnolang/gno (gno tries to guess it)")
fs.StringVar(&c.output, "o", "depgraph", "output (file if single graph, dir if multiple graphs)")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this flag, generate to stdout.

Comment on lines +29 to +35
// test for big graph

// test for fail on cyclical dependencies

// test for fail on missing dependencies

// test for not duplicating dependencies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need more tests.


func (c *depGraphCfg) RegisterFlags(fs *flag.FlagSet) {
rootDir := gnoenv.RootDir()
fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have a non-graphviz format by default, could be just lines with:

	A -> B
	A -> C
	C -> D

something we can easily read and grep.

then, the graphviz format would be a flag.

if err != nil {
return fmt.Errorf("couldn't open output file: %w", err)
}
graphFileData := fmt.Sprintf("Digraph G {\nrankdir=\"LR\"\nranksep=20\n%s\n%s\n}", nodeData.String(), graphData.String())
Copy link
Member

@moul moul Aug 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is hard to read.

nodeData.WriteString("}\nsubgraph {\nrank=same\n")
for _, pkg := range allPkgs {
if strings.Contains(pkg.Name, "gno.land/r") {
fmt.Fprintf(&nodeData, "\"%s\" [color=\"red\"]\n", pkg.Name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use backticks when you generate things with double quotes.

@Kouteki Kouteki added review/triage-pending PRs opened by external contributors that are waiting for the 1st review and removed review/triage-pending PRs opened by external contributors that are waiting for the 1st review labels Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 🤖 gnovm Issues or PRs gnovm related
Projects
Status: In Progress
Status: In Review
Development

Successfully merging this pull request may close these issues.

tool to display a graph of dependencies
6 participants