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.
-
- ⓘ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; }