Skip to content

Commit

Permalink
support multiple initial packages; show dominance using bold in path;…
Browse files Browse the repository at this point in the history
… cleanups
  • Loading branch information
adonovan committed Mar 31, 2021
1 parent d60f371 commit 58706c1
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 108 deletions.
62 changes: 28 additions & 34 deletions code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("<code>" + JSON.stringify(json) + "</code>")

// Show selected package.
$('#pkgname').text(pkg.PkgPath)

// Set link to package documentation.
$('#doc').html("<a target='_blank' href='https://pkg.go.dev/" + pkg.PkgPath + "'>doc</a>")

// 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 ? " ⟶ " : "") + "<code>" + doms[i] + "</code>"
}
$('#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 += "<code>" + p.PkgPath + "</code><br/>"
} else {
if (i >= 0) {
html += "<button type='button' onclick='breakedge(" + path[i-1] + ", " + path[i] + ", false)'>break</button> "
+ "<button type='button' onclick='breakedge(" + path[i-1] + ", " + path[i] + ", true)'>break all</button> "
+ "⟶ <code>" + p.PkgPath + "</code><br/>"
+ "⟶ "
}
html += "<code class='" + (json.Dominators.includes(path[i]) ? "dom" : "") + "'>" + p.PkgPath + "</code><br/>"
}
$('#path').html(html)
}
Expand Down
34 changes: 11 additions & 23 deletions dom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -142,11 +143,7 @@ func buildDomTree(nodes []*node) {
}
}
}
for _, n := range nodes {
if n.isroot {
visit(n)
}
}
visit(root)
nodes = reachable

n := len(nodes)
Expand All @@ -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)
Expand Down Expand Up @@ -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] {
Expand All @@ -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
Expand Down
36 changes: 17 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,29 @@
<body onload='onLoad()'>
<h1>Spaghetti</h1>
<p>
This tool displays the complete dependencies of these root packages:
This tool displays the complete dependencies of these initial packages:
</p>
<pre id='roots'>...</pre>
<pre id='initial'>...</pre>
<p>
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 <i>break</i> button to remove an
initial packages. Use the <i>break</i> button to remove an
edge from the graph, so that you can assess what edges need to be
broken to remove an unwanted dependency.
</p>

<h2>Packages</h2>
<p>
<span class="tooltip"><span class="tooltiptext"> This tree shows
all dependencies of the root packages, grouped hierarchically
by import path and containing module. <br/> 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.</span></span>
<span class="tooltip"><span class="tooltiptext">
This tree shows all dependencies of the initial packages,
grouped hierarchically by import path and containing
module. <br/>

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. <br/>

Click a package to show more information aboutit.</span></span>
<form style='display: inline;'>
<label for='search'>Filter:
<input id='search' type='text' name='filter' size='50'/></label>
Expand All @@ -42,7 +45,7 @@ <h2>Packages</h2>
<div style='height: 20em;' id='tree'>tree</div>
</div>

<h2>Selected package: <span id='pkgname'>N/A</span></h2>
<h2>Selected package: <span id='pkgname'>none</span></h2>
<div id='json' style='display: none; overflow: scroll'>...</div> <!-- debugging -->
<p>
[<span id='doc'>...</span>]<br/>
Expand All @@ -53,14 +56,6 @@ <h2>Selected package: <span id='pkgname'>N/A</span></h2>
<label for='imports'>Imports:</label>
<code id='imports'>...</code>
</p>
<p>
<span class="tooltip"><span class="tooltiptext">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.</span></span>
<label for='dom'>Dominators:</label>
<span id='dom'>...</span>
</p>
<p>
<span class="tooltip"><span class="tooltiptext">This section
displays an arbitrary path from one of the initial packages to
Expand All @@ -70,6 +65,9 @@ <h2>Selected package: <span id='pkgname'>N/A</span></h2>
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 <i>dominators</i>: 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.
</span></span>
<label for='path'>Path from initial package:</label>
<div id='path'></div>
Expand Down
Loading

0 comments on commit 58706c1

Please sign in to comment.