From 3318ac9e4e6e1ad1f358ab9f468404a9fc698655 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Wed, 28 Nov 2018 17:52:35 +0000 Subject: [PATCH] Track more information in order to produce an interesting visualisation --- packages/broker/d.html | 15 ++ packages/broker/d.js | 212 +++++++++++++++++++ packages/core/src/dataspace.js | 163 +++++++++----- packages/core/src/ground.js | 8 + packages/core/src/skeleton.js | 5 +- packages/syntax-playground/src/chatserver.js | 6 +- 6 files changed, 352 insertions(+), 57 deletions(-) create mode 100644 packages/broker/d.html create mode 100644 packages/broker/d.js diff --git a/packages/broker/d.html b/packages/broker/d.html new file mode 100644 index 0000000..04522fe --- /dev/null +++ b/packages/broker/d.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/packages/broker/d.js b/packages/broker/d.js new file mode 100644 index 0000000..b2406b9 --- /dev/null +++ b/packages/broker/d.js @@ -0,0 +1,212 @@ +var svg = d3.select("svg#conversation-animation"); +svg.attr("width", document.body.clientWidth); +svg.attr("height", document.body.clientHeight); + +svg.append("defs").selectAll("marker") + .data(["arrowhead", "arrowhead_actor"]) + .enter().append("marker") + .attr("id", function(d) { return d; }) + .attr("viewBox", "0 -5 10 10") + .attr("refX", function (d) { return d === "arrowhead_actor" ? 125 : 25; }) + .attr("refY", 0) + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("path") + .attr("d", "M0,-5L10,0L0,5 L0, -5") + .style("stroke", "black") + .style("opacity", "1"); + +var width = +svg.attr("width"), + height = +svg.attr("height"); + +var color = d3.scaleOrdinal(d3.schemeCategory20); + +var zoom = d3.zoom() + .scaleExtent([1 / 16, 2]) + .wheelDelta(function () { return -d3.event.deltaY * (d3.event.deltaMode ? 60 : 1) / 500; }) + .filter(function () { return d3.event.type === 'wheel' || d3.event.button === 1; }); + +var initialTransform = d3.zoomIdentity.translate(width/2, height/2).scale(0.5); + +svg.append("rect") + .attr("width", width) + .attr("height", height) + .style("fill", "none") + .style("pointer-events", "all") + .call(zoom) + .call(zoom.transform, initialTransform); + +var g = svg.append("g").attr("transform", initialTransform), + link = g.append("g").attr("stroke", "#000").attr("stroke-width", 1.5).selectAll(".link"), + node = g.append("g").attr("stroke", "#fff").attr("stroke-width", 1.5).selectAll(".node"); + +zoom.on("zoom", function () { g.attr("transform", d3.event.transform) }); + +var simulation = d3.forceSimulation() + .force("link", d3.forceLink() + .distance(function(d) { + if (d.target.type === "topic" && d.source.type === "actor") return 100; + if (d.target.type === "topic" || d.source.type === "topic") return 5; + if (d.target.type === "assertion" && d.source.type === "endpoint") return 10; + if (d.target.type === "facet" && d.source.type === "facet") return 50; + if (d.target.type === "facet" && d.source.type === "actor") return 50; + if (d.target.type === "actor" && d.source.type === "actor") return 500; + return 100; + }) + .strength(function(d) { + if (d.target.type === "actor" && d.source.type === "actor") return 0.1; + return 0.5; + }) + .id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(0, 0)) + .on("tick", function () { + link + .attr("x1", function(d) { return d.source.x; }) + .attr("y1", function(d) { return d.source.y; }) + .attr("x2", function(d) { return d.target.x; }) + .attr("y2", function(d) { return d.target.y; }); + node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); + }); + +// function mkActor(n,c) { return {type: 'actor', id: n, color: c}; } +// function mkAssertion(n) { return {type: 'assertion', id: n}; } +// function mkLink(s,t,v) { return {source: s, target: t, value: v || 1}; } + +var matchWeight = 10; + +var graph = dataspaceContents; + +// var clientCounter = 1; +// function clickHandler() { +// var currentCounter = clientCounter; +// clientCounter++; +// var c = "Client" + currentCounter; +// var n = "name" + currentCounter; + +// graph.nodes.push(mkActor(c, "#FFC0C0")); +// graph.nodes.push(mkAssertion(entry(n,"127.0.0."+currentCounter))); +// graph.nodes.push(mkAssertion(obs(entry(n,"*")))); +// graph.nodes.push(mkAssertion(obs(laterThan(currentCounter)))); + +// graph.edges.push(mkLink("Cache", obs(entry(n,"*")))); +// graph.edges.push(mkLink("Server", entry(n,"127.0.0."+currentCounter))); +// graph.edges.push(mkLink(c, obs(entry(n,"*")))); +// graph.edges.push(mkLink("Cache", obs(laterThan(currentCounter)))); + +// graph.edges.push(mkLink(entry(n,"127.0.0."+currentCounter), obs(entry(n,"*")), matchWeight)); +// graph.edges.push(mkLink(obs(entry(n,"*")), obs(obs(entry("*","*"))), matchWeight)); +// graph.edges.push(mkLink(obs(laterThan(currentCounter)), obs(obs(laterThan("*"))), matchWeight)); + +// setTimeout(function () { +// graph.nodes.splice(graph.nodes.findIndex(function (n) { return n.id === c; }), 1); +// graph.edges.splice(graph.edges.findIndex(function (l) { return l.source.id === c; }), 1); +// setupGraph(); +// }, 3000); + +// setupGraph(); +// } + +// svg.on("click", clickHandler); +// svg.on("doubleclick", clickHandler); + +function setupGraph() { + node = node.data(graph.nodes, function(d) { return d.id; }); + node.exit().remove(); + + var group = node.enter().append("g") + .attr("class", function(d) { return d.type; }) + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + group.append("circle") + .attr("r", function(d) { + switch (d.type) { + case "actor": return 60; + case "facet": return 20; + case "endpoint": return 10; + default: return 5; + } + }) + .attr("fill", function(d) { + switch (d.type) { + case "actor": return "green"; + case "facet": return "cyan"; + case "topic": return "blue"; + case "endpoint": return "yellow"; + default: return d.color || "red"; + } + }) + .on("click", function(d) { + var text = d3.select('#label_' + d.id); + text.style('opacity', text.style('opacity') == 1 ? 0 : 1); + release_pinning(d); + }) + + group.append("text") + .attr("id", function(d) { return 'label_' + d.id; }) + .attr("stroke", "none") + .attr("text-anchor", function(d) { + switch (d.type) { + case "actor": return "middle"; + default: return "start"; + } + }) + .attr("dx", function(d) { + switch (d.type) { + case "actor": return 0; + case "facet": return 20; + default: return 12; + } + }) + .attr("dy", ".35em") + .style("pointer-events", "none") + .style("opacity", 0) + .text(function(d) { return d.label; }); + + node = group.merge(node); + + link = link.data(graph.edges, function(d) { return d.source.id + "-" + d.target.id; }); + link.exit().remove(); + link = link.enter().append("line") + .attr("stroke-width", 1) + .style("marker-end", function (d) { + if (!(d.dir === 'forward' || d.dir === 'both')) return void 0; + // TODO: gross! Why don't I get d.target being an object here? + if (d.target.startsWith('ac_')) return "url('#arrowhead_actor')"; + return "url('#arrowhead')"; + }) + .merge(link); + + // Update and restart the simulation. + simulation.nodes(graph.nodes); + simulation.force("link").links(graph.edges); + simulation.alpha(3).restart(); +} + +function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(3).restart(); + d.fx = d.x; + d.fy = d.y; +} + +function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; +} + +function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + // d.fx = null; + // d.fy = null; +} + +function release_pinning(d) { + d.fx = null; + d.fy = null; +} + +setupGraph(); diff --git a/packages/core/src/dataspace.js b/packages/core/src/dataspace.js index 86c96cd..8074614 100644 --- a/packages/core/src/dataspace.js +++ b/packages/core/src/dataspace.js @@ -44,7 +44,7 @@ function Dataspace(bootProc) { this.pendingActions = Immutable.List([ new ActionGroup(null, Immutable.List([new Spawn(null, bootProc, Immutable.Set())]))]); this.activatedModules = Immutable.Set(); - this.actors = Immutable.Set(); + this.actors = Immutable.Map(); } // Parameters @@ -184,8 +184,8 @@ Dataspace.prototype.refreshAssertions = function () { }); }; -Dataspace.prototype.addActor = function (name, bootProc, initialAssertions) { - let ac = new Actor(this, name, initialAssertions); +Dataspace.prototype.addActor = function (name, bootProc, initialAssertions, parentActor) { + let ac = new Actor(this, name, initialAssertions, parentActor && parentActor.id); this.applyPatch(ac, ac.adhocAssertions.snapshot()); ac.addFacet(null, () => { // Root facet is a dummy "system" facet that exists to hold @@ -213,8 +213,11 @@ Dataspace.prototype.applyPatch = function (ac, delta) { }); }; -Dataspace.prototype.sendMessage = function (m) { +Dataspace.prototype.sendMessage = function (m, sendingActor) { this.index.sendMessage(m); + // this.index.sendMessage(m, (leaf, _m) => { + // sendingActor.touchedTopics = sendingActor.touchedTopics.add(leaf); + // }); }; Dataspace.prototype.adjustIndex = function (a, count) { @@ -256,62 +259,72 @@ Dataspace.prototype._debugString = function (outerIndent) { Dataspace.prototype._dotGraph = function () { let id = 0; const assertionIds = {}; + + const nodes = []; + const edges = []; + const pieces = []; + + function emitNode(type, id, _label, attrs) { + const label = _str(_label); + pieces.push(`\n ${id} [label=${JSON.stringify(label)}];`); + nodes.push(Object.assign({}, attrs || {}, {type, id, label})); + } + + function emitEdge(source, target, maybeDir) { + pieces.push(`\n ${source} -- ${target} [dir=${maybeDir || 'none'}];`); + edges.push({source, target, dir: maybeDir || 'none'}); + } + function _aId(aStr) { + // if (aStr.startsWith('observe(Request(') || aStr.startsWith('Request(')) return null; + // if (aStr.startsWith('observe(Connection(') || aStr.startsWith('Connection(')) return null; if (!(aStr in assertionIds)) assertionIds[aStr] = id++; return assertionIds[aStr]; } - const assertions = {}; + + let topics = Immutable.Map(); + function topicForLeaf(leaf) { + if (topics.has(leaf)) { + return topics.get(leaf); + } else { + const topic = {id: id++, hasEmitter: false, senders: {}, inbound: {}, outbound: {}}; + topics = topics.set(leaf, topic); + return topic; + } + } + function _str(a) { return '' + a; } - function walkNode(n) { - n.edges.forEach((table) => table.forEach(walkNode)); - n.continuation.leafMap.forEach((cvMap) => cvMap.forEach((leaf) => { - leaf.handlerMap.forEach((handler) => { - handler.callbacks.forEach((cb) => { - const observing_assertion_str = _str(cb.__endpoint.handler.assertion); - leaf.cachedAssertions.forEach((observed_assertion) => { - const observed_assertion_str = _str(observed_assertion); - _aId(observed_assertion_str); - if (!(observed_assertion_str in assertions)) assertions[observed_assertion_str] = {}; - const t = assertions[observed_assertion_str]; - if (!(observing_assertion_str in t)) t[observing_assertion_str] = true; - _aId(observing_assertion_str); - }); - }); - }); - })); - } - walkNode(this.index.root); - const pieces = []; + pieces.push('graph G {'); pieces.push('\n overlap=false;'); - for (const a in assertionIds) { - pieces.push(`\n assn_${assertionIds[a]} [label=${JSON.stringify(a).replace(/ /g,'\\n')}];`); - } - for (const observed in assertions) { - for (const observing in assertions[observed]) { - pieces.push(`\n assn_${assertionIds[observed]} -- assn_${assertionIds[observing]};`); - } - } + this.actors.forEach((ac) => { - const acId = id++; - pieces.push(`\n ac_${acId} [label=${JSON.stringify(ac.toString())}];`); + const acId = ac.id; + emitNode('actor', `ac_${acId}`, ac.toString()); + if (this.actors.has(ac.parentId)) { + emitEdge(`ac_${ac.parentId}`, `ac_${acId}`, 'forward'); + } + // ac.touchedTopics.forEach((leaf) => { + // const topic = topicForLeaf(leaf); + // topic.senders[acId] = true; + // topic.hasEmitter = true; + // }); + // ac.touchedTopics = Immutable.Set(); function walkFacet(parent) { return (f) => { const facetId = id++; - pieces.push(`\n facet_${facetId} [label="Facet ${f.id}"];`); - pieces.push(`\n ${parent} -- facet_${facetId};`); + emitNode('facet', `facet_${facetId}`, `Facet ${f.id}`, {parent}); + emitEdge(parent, `facet_${facetId}`); f.endpoints.forEach((ep) => { - const aStr = _str(ep.assertion); - pieces.push(`\n ep_${ep.id} [label="${ep.id}"];`); - pieces.push(`\n facet_${facetId} -- ep_${ep.id};`); - if (aStr in assertionIds) { - pieces.push(`\n ep_${ep.id} -- assn_${assertionIds[_str(ep.assertion)]};`); - } else { - const aId = _aId(aStr); - pieces.push(`\n assn_${aId} [label=${JSON.stringify(aStr).replace(/ /g,'\\n')}];`); - pieces.push(`\n ep_${ep.id} -- assn_${aId};`); + if (ep.assertion !== void 0) { + const aId = _aId(_str(ep.assertion)); + if (aId) { + emitNode('endpoint', `ep_${ep.id}`, ep.id); + emitEdge(`facet_${facetId}`, `ep_${ep.id}`); + emitEdge(`ep_${ep.id}`, `assn_${aId}`); + } } }); f.children.forEach(walkFacet(`facet_${facetId}`)); @@ -319,11 +332,57 @@ Dataspace.prototype._dotGraph = function () { } ac.rootFacet.children.forEach(walkFacet(`ac_${acId}`)); }); + + function walkNode(n) { + n.edges.forEach((table) => table.forEach(walkNode)); + n.continuation.leafMap.forEach((cvMap) => cvMap.forEach((leaf) => { + const topic = topicForLeaf(leaf); + leaf.cachedAssertions.forEach((observed_assertion) => { + const observed_assertion_id = _aId(_str(observed_assertion)); + if (observed_assertion_id) { + topic.inbound[observed_assertion_id] = true; + topic.hasEmitter = true; + } + }); + leaf.handlerMap.forEach((handler) => { + handler.callbacks.forEach((cb) => { + const observing_assertion_id = _aId(_str(cb.__endpoint.handler.assertion)); + if (observing_assertion_id) { + topic.outbound[observing_assertion_id] = true; + } + }); + }); + })); + } + walkNode(this.index.root); + + for (const a in assertionIds) { + emitNode('assertion', `assn_${assertionIds[a]}`, a); + } + + topics.forEach((topic) => { + if (topic.hasEmitter) { + emitNode('topic', 'topic_' + topic.id, ''); // `Topic ${topic.id}`); + for (const acId in topic.senders) { + emitEdge(`ac_${acId}`, `topic_${topic.id}`, 'forward'); + } + for (const aId in topic.inbound) { + emitEdge(`assn_${aId}`, `topic_${topic.id}`, 'forward'); + } + for (const aId in topic.outbound) { + emitEdge(`topic_${topic.id}`, `assn_${aId}`, 'forward'); + } + } + }); + pieces.push('\n}'); + + require('fs').writeFileSync('d.json', 'var dataspaceContents = ' + JSON.stringify({nodes, edges}, null, 2)); + return pieces.join(''); }; -function Actor(dataspace, name, initialAssertions) { +function Actor(dataspace, name, initialAssertions, parentActorId) { this.id = dataspace.nextId++; this.dataspace = dataspace; this.name = name; @@ -334,7 +393,9 @@ function Actor(dataspace, name, initialAssertions) { this.pendingActions = Immutable.List(); this.adhocAssertions = new Bag.MutableBag(initialAssertions); // no negative counts allowed this.cleanupChanges = new Bag.MutableBag(); // negative counts allowed! - dataspace.actors = dataspace.actors.add(this); + this.parentId = parentActorId; + // this.touchedTopics = Immutable.Set(); + dataspace.actors = dataspace.actors.set(this.id, this); } Actor.prototype.runPendingScripts = function () { @@ -478,7 +539,7 @@ function Message(body) { Message.prototype.perform = function (ds, ac) { if (this.body !== void 0) { - ds.sendMessage(Preserves.fromJS(this.body)); + ds.sendMessage(Preserves.fromJS(this.body), ac); } }; @@ -493,7 +554,7 @@ function Spawn(name, bootProc, initialAssertions) { } Spawn.prototype.perform = function (ds, ac) { - ds.addActor(this.name, this.bootProc, this.initialAssertions); + ds.addActor(this.name, this.bootProc, this.initialAssertions, ac); }; Dataspace.spawn = function (name, bootProc, initialAssertions) { @@ -506,7 +567,7 @@ function Quit() { // TODO: rename? Perhaps to Cleanup? Quit.prototype.perform = function (ds, ac) { ds.applyPatch(ac, ac.cleanupChanges.snapshot()); - ds.actors = ds.actors.remove(ac); + ds.actors = ds.actors.remove(ac.id); }; function DeferredTurn(continuation) { diff --git a/packages/core/src/ground.js b/packages/core/src/ground.js index b44959a..cc02d4a 100644 --- a/packages/core/src/ground.js +++ b/packages/core/src/ground.js @@ -107,6 +107,14 @@ function bootModule(mod) { process.on('SIGQUIT', () => { console.log('---------------------------------------------------------------------------'); console.log(g._debugString()); + + g._dotGraph(); + // const child_process = require('child_process'); + // const sp = child_process.spawn('dotpreview.sh 100% neato', { + // shell: true, + // stdio: ['pipe', 'ignore', 'ignore'] + // }); + // sp.stdin.end(g._dotGraph()); }); g.start(); } diff --git a/packages/core/src/skeleton.js b/packages/core/src/skeleton.js index db775a3..42f7b4a 100644 --- a/packages/core/src/skeleton.js +++ b/packages/core/src/skeleton.js @@ -270,8 +270,9 @@ Index.prototype.adjustAssertion = function(outerValue, delta) { Index.prototype.addAssertion = function(v) { this.adjustAssertion(v, +1); }; Index.prototype.removeAssertion = function (v) { this.adjustAssertion(v, -1); }; -Index.prototype.sendMessage = function(v) { - this.root.modify(v, ()=>{}, ()=>{}, (h, vs) => { +const _nop = () => {}; +Index.prototype.sendMessage = function(v, leafCallback) { + this.root.modify(v, _nop, leafCallback || _nop, (h, vs) => { h.callbacks.forEach((cb) => { cb(EVENT_MESSAGE, vs); return true; diff --git a/packages/syntax-playground/src/chatserver.js b/packages/syntax-playground/src/chatserver.js index 85fae3b..2201721 100644 --- a/packages/syntax-playground/src/chatserver.js +++ b/packages/syntax-playground/src/chatserver.js @@ -29,10 +29,8 @@ spawn named 'chatserver' { stop on retracted S.Duplex(id); assert Present(me); - during Present($who) { - on start send S.Push(id, `${who} arrived.\n`, null); - on stop send S.Push(id, `${who} departed.\n`, null); - } + on asserted Present($who) send S.Push(id, `${who} arrived.\n`, null); + on retracted Present($who) send S.Push(id, `${who} departed.\n`, null); on message S.Line(id, $line) send Speak(me, line); on message Speak($who, $what) send S.Push(id, `${who}: ${what}\n`, null);