summaryrefslogtreecommitdiff
path: root/chapter25/src/main/java
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-21 16:26:15 -0700
committerCliff Click <cliffc@acm.org>2026-09-21 16:26:15 -0700
commitd2ffb24d0a3181d107f80df76570c04e010833ed (patch)
treec78b75b2dcca46a4f7513e27fa9f20e43bab787c /chapter25/src/main/java
parent40b33cb6f8b8dc4d8551c3b522e7c5682a44540a (diff)
downloadSimple-d2ffb24d0a3181d107f80df76570c04e010833ed.tar.gz
Simple-d2ffb24d0a3181d107f80df76570c04e010833ed.zip
move the graph visualizer into shared code
Diffstat (limited to 'chapter25/src/main/java')
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java76
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java96
2 files changed, 122 insertions, 50 deletions
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java
index 204b2642..156f1fe9 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java
@@ -7,7 +7,11 @@ import com.seaofnodes.simple.util.SB;
import com.seaofnodes.simple.util.Utils;
import java.io.IOException;
import java.nio.file.Paths;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.net.URI;
import java.util.Collection;
+import java.util.Arrays;
import java.util.Stack;
import static com.seaofnodes.simple.codegen.CodeGen.CODE;
@@ -28,7 +32,20 @@ public class JSViewer implements AutoCloseable {
JSViewer() throws Exception {
// Launch server; handshake
- SERVER = new SimpleWebSocket(Paths.get("docs/index.html").toUri(),12345) ;
+ SERVER = new SimpleWebSocket(viewerURI(),12345);
+ }
+
+ // The browser application is shared by all chapters. Search upwards so
+ // chapter, repository-root, and linearized checkouts use the same assets.
+ static URI viewerURI() throws IOException {
+ String url = System.getProperty("simple.graph.url");
+ if( url != null ) return URI.create(url);
+ for( Path dir = Paths.get("").toAbsolutePath(); dir != null; dir = dir.getParent() ) {
+ Path page = dir.resolve("graph/web/index.html");
+ if( Files.isRegularFile(page) ) return page.toUri();
+ }
+ throw new IOException("Cannot find graph/web/index.html; run inside the Simple checkout "
+ + "or set -Dsimple.graph.url=<viewer URL>");
}
void run( ) throws Exception {
@@ -137,10 +154,10 @@ public class JSViewer implements AutoCloseable {
// Just the Nodes first, in a cluster no edges
sb.i().p("subgraph cluster_Nodes {\n").ii(); // Magic "cluster_" in the subgraph name
for( Node n : all ) {
- if( n instanceof ProjNode || n instanceof CProjNode || n instanceof MemMergeNode )
+ if( n instanceof ProjNode || n instanceof CProjNode )
continue; // Do not emit, rolled into MultiNode or Scope cluster already
- sb.i().p(n.uniqueName()).p(" [ ");
- if( n instanceof MultiNode && !(n instanceof StartNode) ) {
+ sb.i().p(nodeName(n)).p(" [ ");
+ if( n instanceof MultiNode ) {
// Make a box with the MultiNode on top, and all the projections on the bottom
sb. p("shape=plaintext label=<\n").ii();
sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n");
@@ -150,10 +167,11 @@ public class JSViewer implements AutoCloseable {
sb.i().p("<TR><TD>\n").ii();
sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").p("\n");
sb.i().p("<TR>");
- n._outputs.sort((x,y) -> x instanceof ProjNode xp && y instanceof ProjNode yp ? (xp._idx - yp._idx) : ((x==null ? 99999 : x._nid) - (y==null ? 99999 : y._nid)));
+ Node[] outputs = n._outputs.asAry();
+ Arrays.sort(outputs,(x,y) -> Integer.compare(idx(x),idx(y)));
boolean empty_row=true;
- for( Node use : n._outputs ) {
- int idx = idx(n);
+ for( Node use : outputs ) {
+ int idx = idx(use);
if( idx != -1 ) {
cell(sb,use.glabel(),use,"p"+idx);
empty_row=false;
@@ -179,9 +197,9 @@ public class JSViewer implements AutoCloseable {
for (Node n : all) {
if (n instanceof RegionNode region) {
sb.i().p("{ rank=same; ");
- sb.p(region.uniqueName()).p(";");
+ sb.p(nodeName(region)).p(";");
for (Node phi : region._outputs)
- if (phi instanceof PhiNode) sb.p(phi.uniqueName()).p(";");
+ if (phi instanceof PhiNode) sb.p(nodeName(phi)).p(";");
sb.p("}\n");
}
}
@@ -278,10 +296,10 @@ public class JSViewer implements AutoCloseable {
// Called with an open div
private static SB colorcell(SB sb, String text, Node n, boolean dark) {
if( dark ) sb.p("<font color=\"white\">");
- sb.p(text);
+ sb.p(htmlText(text));
if( n._type!=null ) {
sb.p("<br /><font point-size=\"10\">");
- n._type.gprint(sb);
+ sb.p(htmlText(n._type.gprint()));
sb.p("</font>");
}
if( dark ) sb.p("</font>");
@@ -289,7 +307,16 @@ public class JSViewer implements AutoCloseable {
}
- private static String makeScopeName(ScopeNode sn, int level) { return sn.uniqueName() + "_" + level; }
+ // DOT identity must not depend on labels (module/function names contain punctuation).
+ private static String nodeName(Node n) { return "n" + n._nid; }
+
+ // glabel/gprint already use entities such as &lt; and &rarr;.
+ private static String htmlText(String text) {
+ return text.replaceAll("&(?!#[0-9]+;|#x[0-9a-fA-F]+;|[a-zA-Z]+;)", "&amp;")
+ .replace("<", "&lt;").replace(">", "&gt;");
+ }
+
+ private static String makeScopeName(ScopeNode sn, int level) { return nodeName(sn) + "_" + level; }
private static String makePortName(String scopeName, String varName) { return scopeName + "_" + varName; }
// Walk the node edges
@@ -305,8 +332,7 @@ public class JSViewer implements AutoCloseable {
n instanceof ProjNode ||
n instanceof CProjNode ||
// ScopeNodes are done separately
- n instanceof ScopeNode ||
- n instanceof MemMergeNode
+ n instanceof ScopeNode
)
continue;
if( n.isDead() )
@@ -315,20 +341,20 @@ public class JSViewer implements AutoCloseable {
for( int i=0; i<n.nIns(); i++ ) {
Node def = n.in(i);
if( def==null ) continue;
- sb.i().p(n.uniqueName()).p(" -> ");
+ sb.i().p(nodeName(n)).p(" -> ");
if( n instanceof PhiNode && def instanceof RegionNode ) {
// Draw a dotted use->def edge from Phi to Region.
- sb.p(def.uniqueName());
+ sb.p(nodeName(def));
sb.p(" [style=dotted taillabel=").p(i).p("];\n");
} else {
// Most edges land here use->def
if( def instanceof CProjNode proj ) {
- String mname = proj.ctrl().uniqueName();
+ String mname = nodeName(proj.ctrl());
sb.p(mname).p(":p").p(proj._idx);
} else if( def instanceof ProjNode proj ) {
- String mname = proj.in(0).uniqueName();
+ String mname = nodeName(proj.in(0));
sb.p(mname).p(":p").p(proj._idx);
- } else sb.p(def.uniqueName());
+ } else sb.p(nodeName(def));
// Number edges, so we can see how they track
sb.p("[taillabel=").p(i);
// Color the edge
@@ -342,11 +368,13 @@ public class JSViewer implements AutoCloseable {
// Bonus edge if hooked by parser
if( (n.iskeep() || n.isUnused()) && scopeName != null ) {
- sb.i().p(scopeName).p(" -> ").p(n.uniqueName()).p(" [ style=dashed color=grey];\n");
+ sb.i().p(scopeName).p(" -> ").p(nodeName(n)).p(" [ style=dashed color=grey];\n");
}
// Force functions above return
- if( n instanceof ReturnNode ret && !ret.fun().isDead() )
- sb.i().p(ret.ctrl().uniqueName()).p(" -> ").p(ret.fun().uniqueName()).p("[style=invis]\n");
+ if( n instanceof ReturnNode ret && !ret.fun().isDead() ) {
+ defPort(sb.i(),ret.ctrl()).p(" -> ");
+ defPort(sb,ret.fun()).p("[style=invis]\n");
+ }
}
}
@@ -397,7 +425,7 @@ public class JSViewer implements AutoCloseable {
private static SB defPort(SB sb, Node def) {
int idx = idx(def);
return idx != -1
- ? sb.p(def.in(0).uniqueName()).p(":p").p(idx)
- : sb.p(def.uniqueName());
+ ? sb.p(nodeName(def.in(0))).p(":p").p(idx)
+ : sb.p(nodeName(def));
}
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java
index 4077533d..d7fa8bf2 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java
@@ -9,6 +9,10 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
+import java.net.InetSocketAddress;
+import java.nio.file.Path;
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.SimpleFileServer;
// Why-Oh-Why is this needed?
@@ -20,34 +24,66 @@ import java.util.Base64;
class SimpleWebSocket extends ServerSocket {
final InputStream _in;
final OutputStream _out;
+ private HttpServer _http;
+ private Socket _client;
SimpleWebSocket(URI uri, int port) throws IOException, NoSuchAlgorithmException {
super(port);
- // Launch client
- java.awt.Desktop.getDesktop().browse(uri);
- // Look for client; get in/out streams
- Socket sock = accept();
- _in = sock. getInputStream();
- _out = sock.getOutputStream();
+ try {
+ // Use an ordinary web URL; desktop dispatch of file: URLs can silently
+ // raise the browser without opening the page.
+ if( "file".equals(uri.getScheme()) ) {
+ Path page = Path.of(uri);
+ _http = SimpleFileServer.createFileServer(
+ new InetSocketAddress("127.0.0.1",0), page.getParent(),
+ SimpleFileServer.OutputLevel.NONE);
+ _http.start();
+ uri = URI.create("http://127.0.0.1:" + _http.getAddress().getPort()
+ + "/" + page.getFileName());
+ }
+ // Print the address even if the desktop silently fails to open a tab.
+ System.out.println("Graph viewer: " + uri);
+ if( Boolean.parseBoolean(System.getProperty("simple.graph.open", "true")) )
+ java.awt.Desktop.getDesktop().browse(uri);
+ // Look for client; get in/out streams
+ Socket sock = accept();
+ _client = sock;
+ _in = sock. getInputStream();
+ _out = sock.getOutputStream();
- // Establish web socket with handshake; easier with a buffered reader
- BufferedReader br = new BufferedReader(new InputStreamReader(_in));
- String key = null;
- while( br.ready() ) {
- String line = br.readLine();
- if( line.startsWith("Sec-WebSocket-Key: ") )
- key = line.substring(19);
+ // Establish web socket with handshake; easier with a buffered reader
+ BufferedReader br = new BufferedReader(new InputStreamReader(_in, StandardCharsets.US_ASCII));
+ String key = null;
+ // TCP can deliver the request in several chunks. ready() is not an
+ // end-of-headers test; wait for the terminating empty line.
+ String line;
+ while( (line = br.readLine()) != null && !line.isEmpty() ) {
+ if( line.startsWith("Sec-WebSocket-Key: ") )
+ key = line.substring(19);
+ }
+ if( key==null || line==null ) {
+ sock.close();
+ super.close();
+ throw new IOException("Incomplete WebSocket upgrade request");
+ }
+ // Magic handshake
+ byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
+ + "Connection: Upgrade\r\n"
+ + "Upgrade: websocket\r\n"
+ + "Sec-WebSocket-Accept: "
+ + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 )))
+ + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 );
+ _out.write(response,0,response.length);
+ _out.flush();
+ } catch( IOException | NoSuchAlgorithmException | RuntimeException | Error failure ) {
+ if( _http != null ) _http.stop(0);
+ if( _client != null ) {
+ try { _client.close(); } catch( IOException ignored ) {}
+ }
+ try { super.close(); } catch( IOException ignored ) {}
+ throw failure;
}
- assert key!=null;
- // Magic handshake
- byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
- + "Connection: Upgrade\r\n"
- + "Upgrade: websocket\r\n"
- + "Sec-WebSocket-Accept: "
- + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 )))
- + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 );
- _out.write(response,0,response.length);
}
final byte[] _ins = new byte[1024];
@@ -140,10 +176,18 @@ class SimpleWebSocket extends ServerSocket {
}
@Override public void close() throws IOException {
- if( _out != null ) {
- _out.write(0b10001000);
- _out.flush();
+ try {
+ if( _out != null ) {
+ _out.write(new byte[]{(byte)0x88, 0}); // Complete, empty close frame
+ _out.flush();
+ }
+ } finally {
+ if( _http != null ) _http.stop(0);
+ try {
+ if( _client != null ) _client.close();
+ } finally {
+ super.close();
+ }
}
- super.close();
}
}