From 58706c1d69c5c8399019fed891ff2a3034bf9982 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 31 Mar 2021 12:13:26 -0400 Subject: [PATCH] support multiple initial packages; show dominance using bold in path; cleanups --- code.js | 62 +++++++++++++++++++----------------------- dom.go | 34 ++++++++--------------- index.html | 36 ++++++++++++------------ spaghetti.go | 77 ++++++++++++++++++++++++++++++---------------------- style.css | 1 + 5 files changed, 102 insertions(+), 108 deletions(-) diff --git a/code.js b/code.js index ebba234..c3eb126 100644 --- a/code.js +++ b/code.js @@ -5,18 +5,18 @@ var broken = null // array of 2-arrays of int, the node ids of broken edges. function onLoad() { // Grab data from server: package graph, "directory" tree, broken edges. - var data = null - jQuery.ajax({ - url: "/data", - async: false, - success: function(json) {data = json}, - }) + // TODO(adonovan): opt: put JSON data in the /index.html page? + // There's no need for a second HTTP request. + jQuery.ajax({url: "/data", success: onData}) +} +// onData is called shortly after page load with the result of the /data request. +function onData(data) { // Save array of Package objects. packages = data.Packages - // Show initial (root) packages. - $('#roots').text(data.Roots.map(i => packages[i].PkgPath).join("\n")) + // Show initial packages. + $('#initial').text(data.Initial.map(i => packages[i].PkgPath).join("\n")) // Show broken edges. broken = data.Broken @@ -58,65 +58,59 @@ function onLoad() { } }) - // Search the tree when the user types in the search box. - $("#search").keyup(function () { - var searchString = $(this).val(); - $('#tree').jstree('search', searchString); - }); - // Show package info when a node is clicked. $('#tree').on("changed.jstree", function (e, data) { if (data.node) { selectPkg(data.node.original) } }) + + // Search the tree when the user types in the search box. + $("#search").keyup(function () { + var searchString = $(this).val(); + $('#tree').jstree('search', searchString); + }); } // selectPkg shows package info (if any) about the clicked node. function selectPkg(json) { - if (json.Package == null) { - // Non-package "directory" node: grey out the fields. + if (json.Package < 0) { + // Non-package "directory" node: clear the fields. $('#json').text("") - $('#pkgname').text("N/A") + $('#pkgname').text("none") $('#doc').text("") $('#imports').text("") - $('#dom').text("") $('#path').text("") return } // A package node was selected. - var pkg = packages[json.Package] + var pkg = packages[json.Package] + + // Show JSON, for debugging. $('#json').html("" + JSON.stringify(json) + "") + + // Show selected package. $('#pkgname').text(pkg.PkgPath) + + // Set link to package documentation. $('#doc').html("doc") // TODO(adonovan): display imports as a set of links, // with as ImportPath text and "select dir tree node" as action. - if (json.Imports != null) { // TODO(adonovan): how can this be null? - $('#imports').text(json.Imports.join(" ")) - } - - // Show dominator tree. - var html = "" - var doms = [].concat(json.Dominators).reverse() - for (var i in doms) { - html += (i > 0 ? " ⟶ " : "") + "" + doms[i] + "" - } - $('#dom').html(html) + $('#imports').text(json.Imports.join(" ")) // Show "break edges" buttons. var html = "" var path = [].concat(json.Path).reverse() // from root to selected package for (var i in path) { var p = packages[path[i]] - if (i == 0) { // root - html += "" + p.PkgPath + "
" - } else { + if (i >= 0) { html += " " + " " - + "⟶ " + p.PkgPath + "
" + + "⟶ " } + html += "" + p.PkgPath + "
" } $('#path').html(html) } diff --git a/dom.go b/dom.go index 517f554..b5d56c2 100644 --- a/dom.go +++ b/dom.go @@ -51,8 +51,7 @@ package main // to avoid the need for buckets of size > 1. // Idom returns the block that immediately dominates b: -// its parent in the dominator tree, if any. -// Root nodes have no parent. +// its parent in the dominator tree, if any. The root node has no parent. func (b *node) Idom() *node { return b.dom.idom } // Dominees returns the list of blocks that b immediately dominates: @@ -99,7 +98,6 @@ func (lt *ltState) dfs(v *node, i int32, preorder []*node) int32 { // eval implements the EVAL part of the LT algorithm. func (lt *ltState) eval(v *node) *node { - u := v for ; lt.ancestor[v.dom.index] != nil; v = lt.ancestor[v.dom.index] { if lt.sdom[v.dom.index].dom.pre < lt.sdom[u.dom.index].dom.pre { @@ -114,8 +112,8 @@ func (lt *ltState) link(v, w *node) { lt.ancestor[w.dom.index] = v } -// buildDomTree computes the dominator tree of f using the LT algorithm, -// starting from the roots indicated by node.isroot. +// buildDomTree computes the dominator tree of f using the LT algorithm. +// The first node is the distinguished root node. func buildDomTree(nodes []*node) { // The step numbers refer to the original LT paper; the // reordering is due to Georgiadis. @@ -125,12 +123,15 @@ func buildDomTree(nodes []*node) { b.dom = domInfo{index: -1} } + root := nodes[0] + // The original (ssa) implementation had the precondition // that all nodes are reachable, but because of Spaghetti's // "broken edges", some nodes may be unreachable. // We filter them out now with another graph traversal. // The domInfo.index numbering is relative to this ordering. // See other "reachable hack" comments for related parts. + // We should combine this into step 1. var reachable []*node var visit func(n *node) visit = func(n *node) { @@ -142,11 +143,7 @@ func buildDomTree(nodes []*node) { } } } - for _, n := range nodes { - if n.isroot { - visit(n) - } - } + visit(root) nodes = reachable n := len(nodes) @@ -161,12 +158,7 @@ func buildDomTree(nodes []*node) { // Step 1. Number vertices by depth-first preorder. preorder := space[3*n : 4*n] - var prenum int32 - for _, w := range nodes { - if w.isroot { - prenum = lt.dfs(w, prenum, preorder) - } - } + lt.dfs(root, 0, preorder) buckets := space[4*n : 5*n] copy(buckets, preorder) @@ -215,7 +207,7 @@ func buildDomTree(nodes []*node) { // Step 4. Explicitly define the immediate dominator of each // node, in preorder. for _, w := range preorder[1:] { - if w.isroot { + if w == root { w.dom.idom = nil } else { if w.dom.idom != lt.sdom[w.dom.index] { @@ -226,12 +218,8 @@ func buildDomTree(nodes []*node) { } } - var pre, post int32 - for _, w := range nodes { - if w.isroot { - pre, post = numberDomTree(w, pre, post) - } - } + // Number all nodes to enable O(1) dominance queries. + numberDomTree(root, 0, 0) } // numberDomTree sets the pre- and post-order numbers of a depth-first diff --git a/index.html b/index.html index aca2584..b9271e8 100644 --- a/index.html +++ b/index.html @@ -12,26 +12,29 @@

Spaghetti

- This tool displays the complete dependencies of these root packages: + This tool displays the complete dependencies of these initial packages:

-
...
+
...

Click on a package in the tree view to display information about it, including a path by which it is reached from one of the - root packages. Use the break button to remove an + initial packages. Use the break button to remove an edge from the graph, so that you can assess what edges need to be broken to remove an unwanted dependency.

Packages

- This tree shows - all dependencies of the root packages, grouped hierarchically - by import path and containing module.
Each package has a - numeric weight, computed using network flow: this is the size - of the graph rooted at the node, divided by the node's - in-degree. Click a package to show more information about - it.
+ + This tree shows all dependencies of the initial packages, + grouped hierarchically by import path and containing + module.
+ + Each package has a numeric weight, computed using network + flow: this is the size of the graph rooted at the node, + divided by the node's in-degree.
+ + Click a package to show more information aboutit.

@@ -42,7 +45,7 @@

Packages

tree
-

Selected package: N/A

+

Selected package: none

[...]
@@ -53,14 +56,6 @@

Selected package: N/A

...

-

- Node x dominates - node y if y cannot be reached without going through x. One way to - break a dependency on a package y is to break a dependency on any - of its dominators x. - - ... -

This section displays an arbitrary path from one of the initial packages to @@ -70,6 +65,9 @@

Selected package: N/A

package, removing it from the graph. This may be useful for removing distracting packages that you don't plan to eliminate. + The bold nodes are dominators: nodes that are found on + every path to the selected node. One way to break a dependency + on a package is to break all dependencies on any of its dominators.
diff --git a/spaghetti.go b/spaghetti.go index 9421154..26dd9f9 100644 --- a/spaghetti.go +++ b/spaghetti.go @@ -20,9 +20,8 @@ import ( ) // TODO: -// - select the root nodes initially in the dir tree. +// - select the initial nodes initially in the dir tree. // - need more rigor with IDs. Test on a project with multiple versioned modules. -// - test with multiple module versions // - support gopackages -test option. // - prettier dir tree labels (it's HTML) // - document that server is not concurrency-safe. @@ -44,10 +43,27 @@ func main() { log.Fatal(err) } - // Create nodes in deterministic preorder. + // The dominator computation algorithm needs a single root. + // Synthesize one as needed that imports the initial packages; + // the UI does not expose its existence. + rootpkg := initial[0] + if len(initial) > 1 { + imports := make(map[string]*packages.Package) + for i, pkg := range initial { + imports[fmt.Sprintf("%03d", i)] = pkg + } + rootpkg = &packages.Package{ + ID: "(root)", + Name: "synthetic root package", + PkgPath: "(root)", + Imports: imports, + } + } + + // Create nodes in deterministic preorder, distinguished root first. // Node numbering determines search results, and we want stability. nodes := make(map[string]*node) // map from Package.ID - packages.Visit(initial, func(pkg *packages.Package) bool { + packages.Visit([]*packages.Package{rootpkg}, func(pkg *packages.Package) bool { n := &node{Package: pkg, index: len(allnodes)} if pkg.Module != nil { n.modpath = pkg.Module.Path @@ -61,7 +77,7 @@ func main() { return true }, nil) for _, pkg := range initial { - nodes[pkg.ID].isroot = true + nodes[pkg.ID].initial = true } // Create edges, in arbitrary order. @@ -93,7 +109,7 @@ func main() { // Global server state, modified by HTTP handlers. var ( - allnodes []*node // all package nodes, in packages.Visit order + allnodes []*node // all package nodes, in packages.Visit order (root first) rootdir *dirent // root of module/package "directory" tree broken [][2]*node // broken edges ) @@ -109,7 +125,6 @@ func recompute() { // Record the path to every node from the root. // The path is arbitrary but determined by edge sort order. - // Visit nodes and record the first search path from a root node. var setPath func(n, from *node) setPath = func(n, from *node) { if n.from == nil { @@ -119,11 +134,7 @@ func recompute() { } } } - for _, n := range allnodes { - if n.isroot { - setPath(n, nil) - } - } + setPath(allnodes[0], nil) // Compute dominator tree. buildDomTree(allnodes) @@ -140,14 +151,12 @@ func recompute() { } return n.weight } - for _, n := range allnodes { - weight(n) - } + weight(allnodes[0]) - // Create tree of reachable modules/packages. + // Create tree of reachable modules/packages. Excludes synthetic root, if any. rootdir = new(dirent) for _, n := range allnodes { - if n.isroot || n.from != nil { + if n.initial || n.from != nil { // reachable? // FIXME Use of n.ID here is fishy. getDirent(n.ID, n.modpath, n.modversion).node = n } @@ -157,18 +166,19 @@ func recompute() { //go:embed index.html style.css code.js var content embed.FS +// A node is a vertex in the package dependency graph (a DAG). type node struct { // These fields are immutable. - *packages.Package - index int // in allnodes numbering - isroot bool + *packages.Package // information about the package + index int // in allnodes numbering + initial bool // package was among set of initial roots modpath, modversion string // module, or ("std", "") for standard packages // These fields are recomputed after a graph change. imports, importedBy []*node // graph edges weight int // weight computed by network flow from *node // next link in path from a root node (nil if root) - dom domInfo + dom domInfo // dominator information } func sortNodes(nodes []*node) { @@ -240,33 +250,34 @@ func onData(w http.ResponseWriter, req *http.Request) { // These three fields are used by jsTree ID string `json:"id"` // id of DOM element Parent string `json:"parent"` - Text string `json:"text"` + Text string `json:"text"` // actually HTML Type string `json:"type"` // Any additional fields will be accessible // in the jstree node's .original field. - Package int + Package int // -1 for non-package nodes Imports []string - Dominators []string - Path []int // path from package to a root, inclusive of endpoints + Dominators []int // path through dom tree, from package to root inclusive + Path []int // path through package graph, from package to root inclusive } var payload struct { - Roots []int + Initial []int Tree []treeitem Packages []*packages.Package - Broken [][2]int // (from, to) indices in packages array + Broken [][2]int // (from, to) node indices } // roots and graph nodes (packages) for _, n := range allnodes { - if n.isroot { - payload.Roots = append(payload.Roots, n.index) + if n.initial { + payload.Initial = append(payload.Initial, n.index) } payload.Packages = append(payload.Packages, n.Package) } // broken edges + payload.Broken = [][2]int{} // avoid JSON null for _, edge := range broken { payload.Broken = append(payload.Broken, [2]int{edge[0].index, edge[1].index}) } @@ -282,7 +293,7 @@ func onData(w http.ResponseWriter, req *http.Request) { for _, name := range names { e := children[name] - item := treeitem{ID: e.id(), Text: e.name} + item := treeitem{ID: e.id(), Text: e.name, Package: -1} if e.node != nil { // package node: show flow weight // (This is HTML, not text.) @@ -295,13 +306,15 @@ func onData(w http.ResponseWriter, req *http.Request) { // item.State = { 'opened' : true, 'selected' : true } item.Package = e.node.index + item.Imports = []string{} // avoid JSON null for _, imp := range e.node.imports { item.Imports = append(item.Imports, imp.Package.ID) } for n := e.node; n != nil; n = n.Idom() { - item.Dominators = append(item.Dominators, n.Package.ID) + item.Dominators = append(item.Dominators, n.index) } - for n := e.node; n != nil; n = n.from { + // Don't show the synthetic root node (if any) in the path. + for n := e.node; n != nil && n.ID != "(root)"; n = n.from { item.Path = append(item.Path, n.index) } } diff --git a/style.css b/style.css index 9cdc552..4cfc81d 100644 --- a/style.css +++ b/style.css @@ -1,6 +1,7 @@ body { margin: 1em; font-family: "Minion Pro", serif; } h1, h2, h3, h4, .jstree-default { font-family: Lato; } code, pre { font-family: Consolas, monospace; } +code.dom { font-weight: bold; } button { font-size: 50%; } pre#imports { font-size: 50%; max-height: 3em; overflow: scroll; }