diff --git a/misc/dump-timing.py b/misc/dump-timing.py
index c36e44d..e84d8e5 100644
--- a/misc/dump-timing.py
+++ b/misc/dump-timing.py
@@ -7,106 +7,18 @@ import os, sys, time, json, random
streams = sys.argv[1:]
-if not streams:
+if len(streams) != 2:
print("run like: python dump-timing.py tx.json rx.json")
sys.exit(1)
-num_streams = len(streams)
-labels = dict([(num, " "*num + "[%d]" % (num+1) + " "*(num_streams-1-num))
- for num in range(num_streams)])
-abs_timeline = []
-sides = []
+# for now, require sender as first file, receiver as second
+# later, allow use of only one file.
-for side,fn in enumerate(streams):
- with open(fn, "rb") as f:
- for (start, sent, finish, what, details) in json.load(f):
- abs_timeline.append( (start, sent, finish, side, what, details) )
- print("%s is %s" % (labels[side], fn))
- sides.append(os.path.basename(fn))
-
-# relativize all timestamps
-all_times = [e[0] for e in abs_timeline] + [e[2] for e in abs_timeline if e[2]]
-all_times.sort()
-earliest = all_times[0]
-def rel(t):
- if t is None: return None
- return t - earliest
-timeline = [ (rel(start), rel(sent), rel(finish), side, what, details)
- for (start, sent, finish, side, what, details)
- in abs_timeline ]
data = {}
-
-# we pre-calculate the "lane" that each item uses, here in python, rather
-# than leaving that up to the javascript.
-data["lanes"] = ["proc", # 0 gets high-level events and spans: process start,
- # imports, command dispatch, code established, key
- # established, transit connected, process exit
- "API", # 1 gets API call spans (apps usually only wait for
- # one at a time, so they won't overlap): get_code,
- # input_code, get_verifier, get_data, send_data,
- # close
- "wait", # 2 shows waiting-for-human: input code, get
- # permission
- "app", # 3: file-xfer events
- "skt", # 4: websocket message send/receives
- "misc", # 5: anything else
- ]
-data["bounds"] = {"min": rel(all_times[0]), "max": rel(all_times[-1]),
- }
-data["sides"] = sides
-print("started at %s" % time.ctime(earliest))
-print("duration %s seconds" % data["bounds"]["max"])
-items = data["items"] = []
-
-for num, (start, sent, finish, side, what, details) in enumerate(timeline):
- background = False
- if what in ["wormhole",]:
- # background region for wormhole lifetime
- lane = 0
- background = True
- elif what in ["process start", "import", "command dispatch",
- "code established", "key established", "transit connected",
- "exit"]:
- lane = 0
- elif what in ["API get_code", "API input_code", "API set_code",
- "API get_verifier", "API get_data", "API send_data",
- #"API get data", "API send data",
- "API close"]:
- lane = 1
- elif details.get("waiting") in ["user", "crypto"]: # permission or math
- lane = 2
- elif what in ["tx file", "get ack", "rx file", "unpack zip", "send ack"]:
- lane = 3
- elif what in ["websocket"]:
- # connection establishment
- lane = 4
- background = True
- elif (what in ["welcome", "error"] # rendezvous message receives
- or what in ["allocate", "list", "get", "add", "deallocate"]
- # rendezvous message sends
- ):
- lane = 4
- else:
- lane = 5 # unknown
-
- if background:
- continue # disable until I figure out how to draw these better
-
- details_str = ", ".join(["%s=%s" % (name, details[name])
- for name in sorted(details)])
-
- items.append({"side": side,
- "lane": lane,
- "start_time": start,
- "server_sent": sent,
- "finish_time": finish, # maybe None
- "what": what,
- "details": details,
- "details_str": details_str,
- "wiggle": random.randint(0,4),
- })
-
- #if "waiting" in details:
- # viz_className += " wait-%s" % details["waiting"]
+for i,fn in enumerate(streams):
+ name = ["send", "receive"][i]
+ with open(fn, "rb") as f:
+ events = json.load(f)
+ data[name] = {"fn": os.path.basename(fn), "events": events}
from pprint import pprint
pprint(data)
diff --git a/misc/web/timeline.css b/misc/web/timeline.css
index ea90e15..d527f12 100644
--- a/misc/web/timeline.css
+++ b/misc/web/timeline.css
@@ -1,4 +1,57 @@
+line.client_tx {
+ stroke: red;
+ stroke-dasharray: 5,5;
+}
+line.client_rx {
+ stroke: blue;
+ stroke-dasharray: 5,5;
+}
+line.c2c_column {
+ stroke: black;
+ stroke-dasharray: 1,5;
+}
+
+line.c2c {
+ stroke-width: 2.0;
+}
+line.c2c.active {
+ stroke-width: 4.0;
+}
+
+line.y_axis {
+ stroke: gray;
+}
+
+/* putting these in a .css file doesn't work, for some reason I have to add
+the markers as a style= attribute directly. */
+line.circle-arrow-circle {
+ /*marker-start: url(#markerCircle);*/
+ marker-mid: url(#markerArrow);
+ /*marker-end: url(#markerCircle);*/
+}
+
+rect.wait-crypto {
+ stroke: black;
+ fill: #ccc;
+}
+
+rect.wait-user {
+ stroke: #00f;
+ fill: #bbe;
+}
+text.wait-text-user {
+ fill: #00f;
+}
+
+rect.proc-span-import {
+ fill: #fcc;
+}
+
+rect.api {
+ fill: #cfc;
+}
+
rect.bar {
stroke: black;
}
@@ -31,14 +84,6 @@ rect.bar {
fill: #ccf;
}
-rect.wait-user {
- fill: #ccc;
-}
-
-rect.wait-crypto {
- fill: #bbe;
-}
-
.vis-item .vis-item-overflow {
overflow: visible;
}
diff --git a/misc/web/timeline.js b/misc/web/timeline.js
index 0c9978f..f1033c0 100644
--- a/misc/web/timeline.js
+++ b/misc/web/timeline.js
@@ -1,4 +1,4 @@
-var d3, $; // hush
+var d3; // hush
var container = d3.select("#viz");
var data;
@@ -18,24 +18,693 @@ function zoomout() {
globals.redraw();
}
-$.getJSON("data.json", function(d) {
+function is_span(ev, category) {
+ if (ev.category === category && !!ev.stop)
+ return true;
+ return false;
+}
+function is_event(ev, category) {
+ if (ev.category === category && !ev.stop)
+ return true;
+ return false;
+}
+
+const server_message_color = {
+ "welcome": 0, // receive
+
+ "bind": 0, // send
+
+ "allocate": 1, // send
+ "allocated": 1, // receive
+
+ "list": 2, // send
+ "channelids": 2, // receive
+
+ "claim": 3, // send
+ "watch": 4, // send
+
+ "deallocate": 5, // send
+ "deallocated": 5, // receive
+
+ "error": 6, // receive
+
+ //"add": 8, // send (client message)
+ //"message": 8, // receive (client message)
+
+ "ping": 7, // send
+ "pong": 7 // receive
+};
+
+const proc_map = {
+ "command dispatch": "dispatch",
+ "code established": "code-established",
+ "key established": "key-established",
+ "transit connected": "transit-connected",
+ "exit": "exit",
+ "transit connect": "transit-connect",
+ "import": "import"
+ };
+
+const TX_COLUMN = 14;
+const RX_COLUMN = 18;
+const SERVER_COLUMN0 = 20;
+const SERVER_COLUMNS = [20,21,22,23,24,25];
+const NUM_SERVER_COLUMNS = 6;
+const MAX_COLUMN = 45;
+
+function x_offset(offset, side_name) {
+ if (side_name === "send")
+ return offset;
+ return MAX_COLUMN - offset;
+}
+function side_text_anchor(side_name) {
+ if (side_name === "send")
+ return "end";
+ return "start";
+}
+function side_text_dx(side_name) {
+ if (side_name === "send")
+ return "-5px";
+ return "5px";
+}
+
+d3.json("data.json", function(d) {
data = d;
- const LANE_HEIGHT = 30;
- const RECT_HEIGHT = 20;
+ // data is {send,receive}{fn,events}
+ // each event has: {name, start, [stop], [server_rx], [server_tx],
+ // [id], details={} }
- // the Y axis is as follows:
- // * each lane is LANE_HEIGHT tall (e.g. 30px)
- // * the actual rects are RECT_HEIGHT tall (e.g. 20px)
- // * there is a one-lane-tall gap at the top of the chart
- // * there are data.sides.length sides (e.g. one tx, one rx)
- // * there are data.lanes.length lanes (e.g. 6), each with a name
- // * there is a one-lane-tall gap between each side
- // * there is a one-lane-tall gap after the last lane
- // * the horizontal scale markers begin after that gap
- // * the tick marks extend another 6 pixels down
+ // Display all timestamps relative to the sender's startup event. If all
+ // clocks are in sync, this will be the same as first_timestamp, but in
+ // case they aren't, I'd rather use the sender as the reference point.
+ var first = data.send.events[0].start;
+
+ // The X axis is divided up into 50 slots, and then scaled to the screen
+ // later. The left portion represents the "wormhole send" side, the
+ // middle is the rendezvous server, and the right portion is the
+ // "wormhole receive" side.
+ //
+ // 0: time axis, tick marks
+ // 3: sender process events: import, dispatch, exit
+ // 4: sender major application-level events: code/key establishment,
+ // transit-connect
+ // 8: sender stalls: waiting for user, waiting for permission
+ // 10: sender websocket transmits originate from here
+ // 15: sender websocket receives terminate here
+
+ // 20-25: rendezvous-server message lanes
+
+ // 30: receiver websocket receives
+ // 35: receiver websocket transmits
+ // 37: receiver stalls
+ // 41: receiver app-level events
+ // 42: receiver process events
+
+ var first_timestamp = Infinity;
+ var last_timestamp = 0;
+ function prepare_data(e, side_name) {
+ var rel_e = {side_name: side_name, // send or receive
+ name: e.name,
+ start: e.start - first,
+ details: e.details
+ };
+ if (e.stop) rel_e.stop = e.stop - first;
+
+ // sort events into categories, assign X coordinates to some
+ if (proc_map[e.name]) {
+ rel_e.category = "proc";
+ rel_e.x = x_offset(3, side_name);
+ rel_e.text = proc_map[e.name];
+ if (e.name === "import")
+ rel_e.text += " " + e.details.which;
+ }
+ if (e.details.waiting) {
+ rel_e.category = "wait";
+ var off = 8;
+ if (e.details.waiting === "user")
+ off += 0.5;
+ rel_e.x = x_offset(off, side_name);
+ }
+
+ // also, calculate the overall time domain while we're at it
+ [rel_e.start, rel_e.stop].forEach(v => {
+ if (v) {
+ if (v > last_timestamp) last_timestamp = v;
+ if (v < first_timestamp) first_timestamp = v;
+ }
+ });
+ return rel_e;
+ }
+ var events = data.send.events.map(e => prepare_data(e, "send"));
+ events = events.concat(data.receive.events.map(e => prepare_data(e, "receive")));
+
+ /* "Client messages" are ones that go all the way from one client to the
+ other, through the rendezvous channel (and get echoed back to the sender
+ too). We can correlate three websocket messages for each (the send, the
+ local receive, and the remote receive) by comparing their "id" strings.
+
+ Scan for all client messages, to build a list of central columns. For
+ each message, we'll have tx/server_rx/server_tx/rx for the sending side,
+ and server_rx/server_tx/rx for the receiving side. The "add" event
+ contributes tx, the sender's echo contributes and the "message" event
+ contributes server_rx, server_tx, and rx.
+ */
+
+ var side_map = new Map(); // side -> "send"/"receive"
+ var c2c = new Map(); // msgid => {send,receive}{tx,server_rx,server_tx,rx}
+ events.forEach(ev => {
+ var id, phase;
+ if (ev.name === "ws_send") {
+ if (ev.details.type !== "add")
+ return;
+ id = ev.details.id;
+ phase = ev.details.phase;
+ side_map.set(ev.details._side, ev.side_name);
+ } else if (ev.name === "ws_receive") {
+ if (ev.details.message.type !== "message")
+ return;
+ id = ev.details.message.message.id;
+ phase = ev.details.message.message.phase;
+ } else
+ return;
+
+ if (!c2c.has(id)) {
+ c2c.set(id, {phase: phase,
+ side_id: ev.details._side,
+ //tx_side_name: assigned when we see 'add'
+ id: id,
+ arrivals: []
+ //col, server_x: assigned later
+ //server_rx: assigned when we see 'message'
+ });
+ }
+ var cm = c2c.get(id);
+ if (ev.name === "ws_send") { // add
+ cm.tx = ev.start;
+ cm.tx_x = x_offset(TX_COLUMN, ev.side_name);
+ cm.tx_side_name = ev.side_name;
+ } else { // message
+ cm.server_rx = ev.details.message.message.server_rx - first;
+ cm.arrivals.push({server_tx: ev.details.message.server_tx - first,
+ rx: ev.start,
+ rx_x: x_offset(RX_COLUMN, ev.side_name)});
+ }
+ });
+
+ // sort c2c messages by initial sending time
+ var client_messages = Array.from(c2c.values());
+ client_messages.sort( (a,b) => (a.tx - b.tx) );
+
+ // assign columns
+ // TODO: identify overlaps between the c2c messages, share columns
+ // between messages which don't overlap
+
+ client_messages.forEach((cm,index) => {
+ cm.col = index % 6;
+ cm.server_x = 20 + cm.col;
+ });
+
+ console.log("client_messages", client_messages);
+ console.log(side_map);
+ console.log(first_timestamp, last_timestamp);
+
+ /* "Server messages" are ones that stop or originate at the rendezvous
+ server. These are of types other than "add" or "message". Although many
+ of these provoke responses, we do not attempt to correlate these with
+ any other message. For outbound ws_send messages, we know the send
+ timestamp, but not the server receipt timestamp. For inbound ws_receive
+ messages, we know both.
+ */
+ var outbound_sm = new Map();
+ globals.outbound_sm = outbound_sm;
+ events
+ .filter(ev => ev.name === "ws_send")
+ .forEach(ev => {
+ // we don't know the server receipt time, so draw a horizontal
+ // line by setting stop_timestamp=start_timestamp
+ var sm = {side_name: ev.side_name,
+ start_timestamp: ev.start,
+ stop_timestamp: ev.start,
+ start_x: x_offset(TX_COLUMN, ev.side_name),
+ end_x: x_offset(20, ev.side_name),
+ text_x: x_offset(TX_COLUMN, ev.side_name),
+ text_timestamp: ev.start,
+ text_dy: "-5px",
+ type: ev.details.type,
+ tip: ev.details.type,
+ ev: ev
+ };
+ outbound_sm.set(ev.details.id, sm);
+ });
+
+ events
+ .filter(ev => ev.name === "ws_receive")
+ .filter(ev => ev.details.message.type === "ack")
+ .forEach(ev => {
+ var id = ev.details.message.id;
+ var server_tx = ev.details.message.server_tx;
+ var sm = outbound_sm.get(id);
+ sm.stop_timestamp = server_tx - first;
+ });
+
+ var server_messages = [];
+ events
+ .filter(ev => ev.name === "ws_receive")
+ .filter(ev => ev.details.message.type !== "message")
+ .filter(ev => ev.details.message.type !== "ack")
+ .forEach(ev => {
+ var sm = {side_name: ev.side_name,
+ start_timestamp: ev.details.message.server_tx - first,
+ stop_timestamp: ev.start,
+ start_x: x_offset(20, ev.side_name),
+ end_x: x_offset(RX_COLUMN, ev.side_name),
+ text_x: x_offset(RX_COLUMN, ev.side_name),
+ text_timestamp: ev.start,
+ text_dy: "8px",
+ type: ev.details.message.type,
+ tip: ev.details.message.type,
+ ev: ev
+ };
+ server_messages.push(sm);
+ });
+ server_messages = server_messages.concat(
+ Array.from(outbound_sm.values())
+ .filter(sm => sm.type !== "add"));
+ console.log("server_messages", server_messages);
+
+ // TODO: this goes off the edge of the screen, use the viewport instead
+ var container_width = Number(container.style("width").slice(0,-2));
+ var container_height = Number(container.style("height").slice(0,-2));
+ container_height = 700; // no contents, so no height is allocated yet
+ // scale the X axis to the full width of our container
+ var x = d3.scale.linear().domain([0, 50]).range([0, container_width]);
+
+ // scale the Y axis later
+ var y = d3.scale.linear().domain([first_timestamp, last_timestamp])
+ .range([0, container_height]);
+ zoom.y(y);
+ zoom.on("zoom", redraw);
+
+
+ var tip = d3.tip()
+ .attr("class", "d3-tip")
+ .html(function(d) { return "" + d + ""; })
+ .direction("s")
+ ;
+
+ var chart = container.append("svg:svg")
+ .attr("id", "outer_chart")
+ .attr("width", container_width)
+ .attr("height", container_height)
+ .attr("pointer-events", "all")
+ .call(zoom)
+ .call(tip)
+ ;
+
+ var defs = chart.append("svg:defs");
+ defs.append("svg:marker")
+ .attr("id", "markerCircle")
+ .attr("markerWidth", 8)
+ .attr("markerHeight", 8)
+ .attr("refX", 5)
+ .attr("refY", 5)
+ .append("circle")
+ .attr("cx", 5)
+ .attr("cy", 5)
+ .attr("r", 3)
+ .attr("style", "stroke: none; fill: #000000;")
+ ;
+ defs.append("svg:marker")
+ .attr("id", "markerArrow")
+ .attr("markerWidth", 26)
+ .attr("markerHeight", 26)
+ .attr("refX", 26)
+ .attr("refY", 12)
+ .attr("orient", "auto")
+ .attr("markerUnits", "userSpaceOnUse") // don't scale to stroke-width
+ .append("path")
+ .attr("d", "M8,20 L20,12 L8,4")
+ .attr("style", "stroke: #000000; fill: none")
+ ;
+
+ chart.append("svg:line")
+ .attr("x1", x(0.5)).attr("y1", 0)
+ .attr("x2", x(0.5)).attr("y2", container_height)
+ .attr("class", "y_axis")
+ ;
+ chart.append("svg:g")
+ .attr("class", "seconds_g")
+ .attr("transform", "translate("+(x(0.5)+5)+","+(container_height-10)+")")
+ .append("svg:text")
+ .text("seconds")
+ ;
+
+ chart.append("svg:line")
+ .attr("x1", x(TX_COLUMN)).attr("y1", y(first_timestamp))
+ .attr("x2", x(TX_COLUMN)).attr("y2", y(last_timestamp))
+ .attr("class", "client_tx")
+ ;
+ chart.append("svg:text")
+ .attr("x", x(TX_COLUMN)).attr("y", 10)
+ .attr("text-anchor", "middle")
+ .text("sender tx");
+
+ chart.append("svg:line")
+ .attr("x1", x(RX_COLUMN)).attr("y1", y(first_timestamp))
+ .attr("x2", x(RX_COLUMN)).attr("y2", y(last_timestamp))
+ .attr("class", "client_rx")
+ ;
+ chart.append("svg:text")
+ .attr("x", x(RX_COLUMN)).attr("y", 10)
+ .attr("text-anchor", "middle")
+ .text("sender rx");
+
+ chart.selectAll("line.c2c_column").data(SERVER_COLUMNS)
+ .enter().append("svg:line")
+ .attr("class", "c2c_column")
+ .attr("x1", d => x(d)).attr("y1", y(first_timestamp))
+ .attr("x2", d => x(d)).attr("y2", y(last_timestamp))
+ ;
+
+ chart.append("svg:line")
+ .attr("x1", x(MAX_COLUMN-RX_COLUMN)).attr("y1", y(first_timestamp))
+ .attr("x2", x(MAX_COLUMN-RX_COLUMN)).attr("y2", y(last_timestamp))
+ .attr("class", "client_rx")
+ ;
+ chart.append("svg:text")
+ .attr("x", x(MAX_COLUMN-RX_COLUMN)).attr("y", 10)
+ .attr("text-anchor", "middle")
+ .text("receiver rx");
+
+ chart.append("svg:line")
+ .attr("x1", x(MAX_COLUMN-TX_COLUMN)).attr("y1", y(first_timestamp))
+ .attr("x2", x(MAX_COLUMN-TX_COLUMN)).attr("y2", y(last_timestamp))
+ .attr("class", "client_tx")
+ ;
+ chart.append("svg:text")
+ .attr("x", x(MAX_COLUMN-TX_COLUMN)).attr("y", 10)
+ .attr("text-anchor", "middle")
+ .text("receiver tx");
+
+ // produces list of {p_from, p_to, col, add_arrow, tip}
+ function cm_line(cm) {
+ // We draw a bunch of two-point lines
+ var lines = [];
+ function push(p_from, p_to, add_arrow) {
+ lines.push({p_from: p_from, p_to: p_to,
+ col: cm.col, tip: cm.tip,
+ add_arrow: add_arrow});
+ }
+ // the first goes from the sender to the server_rx, if we know it
+ // TODO: tolerate not knowing it
+ var sender_point = [cm.tx_x, cm.tx];
+ var server_rx_point = [cm.server_x, cm.server_rx];
+ push(sender_point, server_rx_point, true);
+
+ // the second goes from the server_rx to the last server_tx
+ var last_server_tx = Math.max.apply(null,
+ cm.arrivals.map(a => a.server_tx));
+ var last_server_tx_point = [cm.server_x, last_server_tx];
+ push(server_rx_point, last_server_tx_point, false);
+
+ cm.arrivals.forEach(ar => {
+ var delivery_tx_point = [cm.server_x, ar.server_tx];
+ var delivery_rx_point = [ar.rx_x, ar.rx];
+ push(delivery_tx_point, delivery_rx_point, true);
+ });
+
+ return lines;
+ }
+
+ var all_cm_lines = [];
+ client_messages.forEach(v => {
+ all_cm_lines = all_cm_lines.concat(cm_line(v));
+ });
+ console.log(all_cm_lines);
+ var cm_colors = d3.scale.category10();
+ chart.selectAll("line.c2c").data(all_cm_lines)
+ .enter()
+ .append("svg:line")
+ .attr("class", "c2c") // circle-arrow-circle")
+ .attr("stroke", ls => cm_colors(ls.col))
+ .attr("style", ls => {
+ if (ls.add_arrow) return "marker-end: url(#markerArrow);";
+ return "";
+ })
+ .on("mouseover", ls => {
+ if (ls.tip)
+ tip.show(ls.tip);
+ chart.selectAll("circle.c2c").filter(d => d.col == ls.col)
+ .attr("r", 10);
+ chart.selectAll("line.c2c")
+ .classed("active", d => d.col == ls.col);
+ })
+ .on("mouseout", ls => {
+ tip.hide(ls);
+ chart.selectAll("circle.c2c")
+ .attr("r", 5);
+ chart.selectAll("line.c2c")
+ .classed("active", false);
+ })
+ ;
+
+ chart.selectAll("g.c2c").data(client_messages)
+ .enter()
+ .append("svg:g")
+ .attr("class", "c2c")
+ .append("svg:text")
+ .attr("class", "c2c")
+ .attr("text-anchor", cm => side_text_anchor(cm.tx_side_name))
+ .attr("dx", cm => side_text_dx(cm.tx_side_name))
+ .attr("dy", "10px")
+ .attr("fill", cm => cm_colors(cm.col))
+ .text(cm => cm.phase);
+
+ function cm_dot(cm) {
+ var dots = [];
+ var color = cm_colors(cm.col);
+ var tip = cm.phase;
+ function push(x,y) {
+ dots.push({x: x, y: y, col: cm.col, color: color, tip: tip});
+ }
+ push(cm.tx_x, cm.tx);
+ cm.arrivals.forEach(ar => push(ar.rx_x, ar.rx));
+ return dots;
+ }
+ var all_cm_dots = [];
+ client_messages.forEach(cm => {
+ all_cm_dots = all_cm_dots.concat(cm_dot(cm));
+ });
+ chart.selectAll("circle.c2c").data(all_cm_dots)
+ .enter()
+ .append("svg:circle")
+ .attr("class", "c2c")
+ .attr("r", 5)
+ .attr("fill", dot => dot.color)
+ .on("mouseover", dot => {
+ if (dot.tip)
+ tip.show(dot.tip);
+ chart.selectAll("circle.c2c").filter(d => d.col == dot.col)
+ .attr("r", 10);
+ chart.selectAll("line.c2c")
+ .classed("active", d => d[2] == dot.col);
+ })
+ .on("mouseout", dot => {
+ tip.hide(dot);
+ chart.selectAll("circle.c2c")
+ .attr("r", 5);
+ chart.selectAll("line.c2c")
+ .classed("active", false);
+ })
+ ;
+
+ // server messages
+ chart.selectAll("line.server-message").data(server_messages)
+ .enter()
+ .append("svg:line")
+ .attr("class", "server-message")
+ .attr("stroke", sm => cm_colors(server_message_color[sm.type] || 0))
+ .attr("style", "marker-end: url(#markerArrow)")
+ .on("mouseover", sm => {
+ if (sm.tip)
+ tip.show(sm.tip);
+ })
+ .on("mouseout", sm => {
+ tip.hide(sm);
+ })
+ ;
+ chart.selectAll("g.server-message").data(server_messages)
+ .enter()
+ .append("svg:g")
+ .attr("class", "server-message")
+ .append("svg:text")
+ .attr("class", "server-message")
+ .attr("text-anchor", sm => side_text_anchor(sm.side_name))
+ .attr("dx", sm => side_text_dx(sm.side_name))
+ .attr("dy", sm => sm.text_dy)
+ .attr("fill", sm => cm_colors(server_message_color[sm.type] || 0))
+ .text(sm => sm.type);
+ // TODO: add dots on the known send/receive time points
+
+ var w = chart.selectAll("g.wait")
+ .data(events.filter(ev => ev.category === "wait"))
+ .enter().append("svg:g")
+ .attr("class", "wait");
+ w.append("svg:rect")
+ .attr("class", ev => "wait wait-"+ev.details.waiting)
+ .attr("width", 10);
+ var wt = chart.selectAll("g.wait-text")
+ .data(events.filter(ev => ev.category === "wait"))
+ .enter().append("svg:g")
+ .attr("class", "wait-text");
+ wt.append("svg:text")
+ .attr("class", ev => "wait-text wait-text-"+ev.details.waiting)
+ .attr("text-anchor", ev => ev.side_name === "send" ? "end" : "start")
+ .attr("dx", ev => ev.side_name === "send" ? "-5px" : "15px")
+ .attr("dy", "5px")
+ .text(v => v.name+" ("+v.details.waiting+")");
+
+ // process-related events
+ var pe = chart.selectAll("g.proc-event")
+ .data(events.filter(ev => is_event(ev, "proc")))
+ .enter().append("svg:g")
+ .attr("class", "proc-event");
+ pe.append("svg:circle")
+ .attr("class", ev => "proc-event proc-event-"+proc_map[ev.name])
+ .attr("cx", ev => ev.side_name === "send" ? "12px" : "-2px")
+ .attr("r", 5)
+ .attr("fill", "red")
+ .attr("width", 10);
+ pe.append("svg:text")
+ .attr("class", ev => "proc-event proc-event-"+proc_map[ev.name])
+ .attr("text-anchor", ev => ev.side_name === "send" ? "start" : "end")
+ .attr("dx", ev => ev.side_name === "send" ? "15px" : "-5px")
+ .attr("dy", "5px")
+ .attr("transform", "rotate(-30)")
+ .text(ev => proc_map[ev.name]);
+
+ // process-related spans
+ var ps = chart.selectAll("g.proc-span")
+ .data(events.filter(ev => is_span(ev, "proc")))
+ .enter().append("svg:g")
+ .attr("class", "proc-span");
+ ps.append("svg:rect")
+ .attr("class", ev => "proc-span proc-span-"+proc_map[ev.name])
+ .attr("width", 10);
+ var pst = chart.selectAll("g.proc-span-text")
+ .data(events.filter(ev => is_span(ev, "proc")))
+ .enter().append("svg:g")
+ .attr("class", "proc-span-text");
+ pst.append("svg:text")
+ .attr("class", ev => "proc-span-text proc-span-text-"+proc_map[ev.name])
+ .attr("text-anchor", ev => ev.side_name === "send" ? "start" : "end")
+ .attr("dx", ev => ev.side_name === "send" ? "15px" : "-5px")
+ .attr("dy", "5px")
+ .text(ev => ev.text);
+
+ function ty(d) { return "translate(0,"+y(d)+")"; }
+
+ function redraw() {
+ chart.selectAll("line.c2c")
+ .attr("x1", ls => x(ls.p_from[0]))
+ .attr("y1", ls => y(ls.p_from[1]))
+ .attr("x2", ls => x(ls.p_to[0]))
+ .attr("y2", ls => y(ls.p_to[1]))
+ ;
+ chart.selectAll("g.c2c")
+ .attr("transform", cm =>
+ "translate("+x(cm.tx_x)+","+y(cm.tx)+")")
+ ;
+ chart.selectAll("circle.c2c")
+ .attr("cx", d => x(d.x))
+ .attr("cy", d => y(d.y))
+ ;
+ chart.selectAll("line.server-message")
+ .attr("x1", sm => x(sm.start_x))
+ .attr("y1", sm => y(sm.start_timestamp))
+ .attr("x2", sm => x(sm.end_x))
+ .attr("y2", sm => y(sm.stop_timestamp));
+ chart.selectAll("g.server-message")
+ .attr("transform", sm => {
+ return "translate("+x(sm.text_x)+","+y(sm.text_timestamp)+")";
+ })
+ ;
+
+
+ chart.selectAll("g.wait")
+ .attr("transform", ev => {
+ return "translate("+x(ev.x)+","+y(ev.start)+")";
+ });
+ chart.selectAll("rect.wait")
+ .attr("height", ev => y(ev.stop)-y(ev.start));
+
+ chart.selectAll("g.wait-text")
+ .attr("transform", ev => {
+ return "translate("+x(ev.x)+","+y((ev.start+ev.stop)/2)+")";
+ });
+
+ chart.selectAll("g.proc-event")
+ .attr("transform", ev => {
+ return "translate("+x(ev.x)+","+y(ev.start)+")";
+ })
+ ;
+
+ chart.selectAll("g.proc-span")
+ .attr("transform", ev => {
+ return "translate("+x(ev.x)+","+y(ev.start)+")";
+ })
+ ;
+ chart.selectAll("rect.proc-span")
+ .attr("height", ev => y(ev.stop)-y(ev.start));
+ chart.selectAll("g.proc-span-text")
+ .attr("transform", ev => {
+ return "translate("+x(ev.x)+","+y((ev.start+ev.stop)/2)+")";
+ });
+
+
+ // vertical scale markers: horizontal tick lines at rational
+ // timestamps
+
+ // TODO: clicking on a dot should set the new zero time
+ var rules = chart.selectAll("g.rule")
+ .data(y.ticks(10))
+ .attr("transform", ty);
+ rules.select("text")
+ .text(t => y.tickFormat(10, "s")(t)+"s");
+ var newrules = rules.enter().insert("svg:g")
+ .attr("class", "rule")
+ .attr("transform", ty)
+ ;
+ newrules.append("svg:line")
+ .attr("class", "rule-tick")
+ .attr("stroke", "black");
+ chart.selectAll("line.rule-tick")
+ .attr("x1", x(0.5)-5)
+ .attr("x2", x(0.5));
+ newrules.append("svg:line")
+ .attr("class", "rule-red")
+ .attr("stroke", "red")
+ .attr("stroke-opacity", .3);
+ chart.selectAll("line.rule-red")
+ .attr("x1", x(0.5))
+ .attr("x2", x(MAX_COLUMN));
+ newrules.append("svg:text")
+ .attr("class", "rule-text")
+ .attr("dx", ".1em")
+ .attr("dy", "-0.2em")
+ .attr("text-anchor", "start")
+ .attr("fill", "black")
+ .text(t => y.tickFormat(10, "s")(t)+"s");
+ chart.selectAll("text.rule-text")
+ .attr("x", 6 + 9);
+ rules.exit().remove();
+ }
+
+
+ redraw();
+ return;
- var w = Number(container.style("width").slice(0,-2));
function y_off(d) {
return (LANE_HEIGHT * (d.side*(data.lanes.length+1) + d.lane)
@@ -43,20 +712,6 @@ $.getJSON("data.json", function(d) {
}
var bottom_rule_y = LANE_HEIGHT * data.sides.length * (data.lanes.length+1);
var bottom_y = bottom_rule_y + 45;
-
- var tip = d3.tip()
- .attr("class", "d3-tip")
- .html(function(d) { return "" + d.details_str + ""; })
- .direction("s")
- ;
-
- var chart = container.append("svg:svg")
- .attr("id", "outer_chart")
- .attr("width", w)
- .attr("pointer-events", "all")
- .call(zoom)
- .call(tip)
- ;
//var chart_g = chart.append("svg:g");
// this "backboard" rect lets us catch mouse events anywhere in the
@@ -154,7 +809,7 @@ $.getJSON("data.json", function(d) {
return duration;
}
- function redraw() {
+ function oldredraw() {
// at this point zoom/pan must be fixed
var min = data.bounds.min + x.domain()[0];
var max = data.bounds.min + x.domain()[1];
@@ -279,10 +934,28 @@ $.getJSON("data.json", function(d) {
// lines: these represent the time at which the server sent a message
// which finished a bar. These get an SVG group, and a line
- var lines = chart.selectAll("g.lines")
- .data(clipped.lines, (d) => d.start_time)
+ var linedata = clipped.lines.map(d => [
+ [d.server_sent, 0],
+ [d.server_sent, LANE_HEIGHT],
+ [d.finish_time, 0],
+ ]);
+
+ function lineshape(d) {
+ var l = d3.svg.line()
+ .x(d => x(d[0]))
+ .y(d => y_off(d) + 12345);
+ }
+ function update_line(sel) {
+ sel.attr("d", lineshape)
+ .attr("class", d => "line lane-"+d.lane)
+ ;
+ }
+
+ var lines = chart.selectAll("polyline.lines")
+ .data(linedata)
+
.attr("transform",
- (d) => "translate("+left_server(d)+","+y_off(d)+")")
+ (d) => "translate("+left(d)+","+y_off(d)+")")
;
lines.exit().remove();
var new_lines = lines.enter()
@@ -292,7 +965,17 @@ $.getJSON("data.json", function(d) {
(d) => "translate("+left_server(d)+","+(y_off(d))+")")
;
new_lines.append("svg:line")
- .attr("x1", 0).attr("y1", -5).attr("x2", "0").attr("y2", LANE_HEIGHT)
+ .attr("x1", 0)
+ .attr("y1", -5)
+ .attr("x2", "0")
+ .attr("y2", LANE_HEIGHT)
+ .attr("class", (d) => "line lane-"+d.lane)
+ .attr("stroke", "red")
+ ;
+ new_lines.append("svg:line")
+ .attr("x1", 0).attr("y1", -5)
+ .attr("x2", (d) => x(d.finish_time - d.server_sent))
+ .attr("y2", 0)
.attr("class", (d) => "line lane-"+d.lane)
.attr("stroke", "red")
;
@@ -354,3 +1037,17 @@ $.getJSON("data.json", function(d) {
redraw();
$.get("done", function(_) {});
});
+
+/*
+TODO
+
+* identify the largest gaps in the timeline (biggest is probably waiting for
+ the recipient to start the program, followed by waiting for recipient to
+ type in code, followed by waiting for recipient to approve transfer, with
+ the time of actual transfer being anywhere among the others).
+* identify groups of events that are separated by those gaps
+* put a [1 2 3 4 all] set of buttons at the top of the page
+* clicking on each button will zoom the display to 10% beyond the span of
+ events in the given group, or reset the zoom to include all events
+
+*/