syndicate-render-msd/src/index.ts

356 lines
14 KiB
TypeScript
Raw Normal View History

2022-01-24 08:28:56 +00:00
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Dataspace, IdentitySet, Ref, Bytes, Observe, Turn, Reader, genericEmbeddedTypeDecode, Schemas, GenericEmbedded, Dictionary, fromJS } from "@syndicate-lang/core";
2022-01-24 08:28:56 +00:00
import { QuasiValue as Q } from "@syndicate-lang/core";
const Trace = Schemas.trace;
import * as P from './pict';
import { boot as bootHtml, UIFragment } from "@syndicate-lang/html";
2022-01-24 08:28:56 +00:00
assertion type URLContent(url: string, content: Bytes | false);
const SWIMLANE_WIDTH = 100;
const LIFELINE_WIDTH = 5;
const ACTIVATION_WIDTH = LIFELINE_WIDTH * 2;
const VERTICAL_GAP = 20;
const POST_TURN_GAP = VERTICAL_GAP * 3;
const PLAYGROUND_PADDING = 10;
const TAIL_LENGTH = 60;
const TERMINATOR_SIZE = new P.Point(LIFELINE_WIDTH * 3, LIFELINE_WIDTH);
2022-01-24 08:28:56 +00:00
export function main() {
Dataspace.boot(ds => {
P.init();
bootHtml(ds);
bootFetcher(ds);
bootRenderer(ds);
});
}
function bootFetcher(ds: Ref) {
spawn named 'fetcher' {
at ds {
during Observe({
"pattern": :pattern URLContent(\Q.lit($url: string), \_)
}) => spawn named ['fetch', url] {
const facet = Turn.activeFacet;
facet.preventInertCheck();
fetch(url)
.then(response => {
if (!response.ok) {
console.log('unsuccessful request', url, response);
facet.turn(() => {
assert URLContent(url, false);
});
} else {
response.arrayBuffer().then(bs => facet.turn(() => {
assert URLContent(url, Bytes.from(bs));
}));
}
})
.catch(error => facet.turn(() => {
console.error('failed request', url, error);
assert URLContent(url, false);
}));
}
}
}
}
function bootRenderer(ds: Ref) {
spawn named 'renderer' {
const r = new Renderer(ds);
at ds {
const traceUrl = 'd2';
during URLContent(traceUrl, false) => {
alert("Couldn't fetch trace file: " + JSON.stringify(traceUrl));
}
during URLContent(traceUrl, $bs: Bytes) => {
const vs = new Reader(bs.fromUtf8(), {
embeddedDecode: genericEmbeddedTypeDecode,
}).readToEnd();
for (const v of vs) {
r.addTraceEntry(Trace.asTraceEntry(v));
}
r.container.scrollLeft =
(r.playground.width.baseVal.value - r.container.getBoundingClientRect().width)
/ 2;
}
}
}
}
class Swimlane {
readonly actorId: Schemas.trace.ActorId<GenericEmbedded>;
readonly renderer: Renderer;
readonly laneNumber: number;
readonly startY: number;
readonly entries: Array<P.BoundedPict<any>> = [];
2022-01-24 08:28:56 +00:00
readonly lifeline = P.rect([LIFELINE_WIDTH, 0], {"class": "lifeline"});
tail: P.BoundedPict<SVGRectElement> | null =
P.rect([LIFELINE_WIDTH, TAIL_LENGTH], {"class": "lifeline-tail"});
2022-01-24 08:28:56 +00:00
constructor (actorId: Schemas.trace.ActorId<GenericEmbedded>, laneNumber: number, renderer: Renderer) {
this.actorId = actorId;
this.renderer = renderer;
this.laneNumber = laneNumber;
this.startY = renderer.ypos;
this.lifeline.updateBounds(b => b.withTopMid([this.midX, this.startY]));
this.updateLifelineHeight();
this.renderer.underlay.appendChild(this.lifeline.node);
this.renderer.underlay.appendChild(this.tail!.node);
}
updateLifelineHeight() {
this.lifeline.updateBounds(b => b.withHeight(this.renderer.ypos - b.top));
this.tail?.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
2022-01-24 08:28:56 +00:00
}
get midX(): number {
return this.laneNumber * SWIMLANE_WIDTH;
}
addEntry(klass: string | null, entry: P.BoundedPict<any>): P.BoundedPict<any> {
2022-01-24 08:28:56 +00:00
this.entries.push(entry);
if (klass !== null) {
const pin = P.circle(2, {"class": "pin"});
pin.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
this.renderer.advance(pin.bounds.height);
this.renderer.overlay.appendChild(pin.node);
2022-01-24 08:28:56 +00:00
}
const n = klass === null ? entry : P.placard(entry, {"class": klass});
n.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
this.renderer.overlay.appendChild(n.node);
this.renderer.advance(n.bounds.height);
this.updateLifelineHeight();
2022-01-24 08:28:56 +00:00
this.renderer.adjustViewport();
return n;
}
finish() {
this.renderer.underlay.removeChild(this.tail!.node);
this.tail = null;
this.renderer.releaseSwimlane(this.laneNumber);
2022-01-24 08:28:56 +00:00
}
}
class Renderer {
ds: Ref;
readonly container: HTMLDivElement = document.getElementById('container')! as any;
readonly playground: SVGSVGElement = document.getElementById('playground')! as any;
readonly underlay: SVGGElement = this.playground.getElementById('underlay')! as any;
readonly overlay: SVGGElement = this.playground.getElementById('overlay')! as any;
readonly swimlanes = new Dictionary<GenericEmbedded, Swimlane>();
2022-01-24 08:28:56 +00:00
readonly busyLanes = new IdentitySet<number>();
readonly turnMap = new Dictionary<GenericEmbedded, P.Point>();
2022-01-24 08:28:56 +00:00
ypos = 0;
constructor (ds: Ref) {
this.ds = ds;
this.adjustViewport();
at this.ds {
on asserted UIFragment($_fragmentId, $_newSelector, $_newHtml, $_newOrderBy) => {
this.adjustViewport();
}
}
}
adjustViewport() {
const bbox = this.playground.getBBox();
this.playground.viewBox.baseVal.x = bbox.x - PLAYGROUND_PADDING;
this.playground.viewBox.baseVal.y = bbox.y - PLAYGROUND_PADDING;
this.playground.viewBox.baseVal.width = bbox.width + PLAYGROUND_PADDING * 2;
this.playground.viewBox.baseVal.height = bbox.height + PLAYGROUND_PADDING * 2;
this.playground.width.baseVal.value = bbox.width + PLAYGROUND_PADDING * 2;
this.playground.height.baseVal.value = bbox.height + PLAYGROUND_PADDING * 2;
2022-01-24 08:28:56 +00:00
}
allocateSwimlane(): number {
let i = 0;
while (this.busyLanes.has(i)) i++;
this.busyLanes.add(i);
return i;
}
releaseSwimlane(n: number) {
this.busyLanes.delete(n);
}
swimlaneFor(actorId: Schemas.trace.ActorId<GenericEmbedded>): Swimlane {
let k = fromJS(actorId);
let s = this.swimlanes.get(k);
if (s === void 0) {
this.swimlanes.set(k, s = new Swimlane(actorId, this.allocateSwimlane(), this));
}
return s;
}
advance(delta: number) {
this.ypos += delta;
}
connect(ps: P.Point, pe: P.Point) {
if (pe.x == ps.x) {
const pullMagnitude = pe.sub(ps).scale(0.1).length;
const pullAngle = Math.PI;
const pull = P.Point.rTheta(pullMagnitude + 30, pullAngle);
// TODO: compute marker size from the actual definition
const arrowheadSpace = P.Point.rTheta(15 - 3.75, pullAngle);
this.overlay.appendChild(
P.cubicBezier(ps,
ps.add(pull),
pe.add(pull),
pe.add(arrowheadSpace),
{"class": "arrow"}).node);
} else {
const pullMagnitude = pe.sub(ps).scale(0.1).length;
const pullAngle = pe.x > ps.x ? Math.PI/4 : 3*Math.PI/4;
const pull = P.Point.rTheta(pullMagnitude, pullAngle);
// TODO: compute marker size from the actual definition
const arrowheadSpace = P.Point.rTheta(15 - 3.75, pullAngle);
const pe1 = pe.sub(arrowheadSpace);
this.overlay.appendChild(
P.cubicBezier(ps,
ps.add(pull),
pe1.sub(pull),
pe1,
{"class": "arrow"}).node);
}
}
rewindTo<R>(pos: number, f: () => R): R {
const saved = this.ypos;
this.ypos = pos;
try {
return f();
} finally {
this.ypos = saved;
}
}
2022-01-24 08:28:56 +00:00
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
switch (entry.item._variant) {
case "start": {
this.advance(LIFELINE_WIDTH);
const swimlane = this.swimlaneFor(entry.actor);
swimlane.addEntry(null, P.rect(TERMINATOR_SIZE, {"class": "terminator"}));
this.advance(LIFELINE_WIDTH);
swimlane.addEntry("start", P.text(fromJS(entry.item).asPreservesText()));
break;
}
case "turn": {
const swimlane = this.swimlaneFor(entry.actor);
const activation = P.rect([ACTIVATION_WIDTH, 0], {"class": "activation"});
this.advance(LIFELINE_WIDTH);
activation.updateBounds(b => b.withTopMid([swimlane.midX, this.ypos]));
let causingTurnId: Schemas.trace.TurnId<GenericEmbedded> | null = null;
switch (entry.item.value.cause._variant) {
case "turn":
causingTurnId = entry.item.value.cause.id;
break;
case "delay":
causingTurnId = entry.item.value.cause.causingTurn;
break;
default:
break;
}
if (causingTurnId !== null) {
this.connect(this.turnMap.get(fromJS(causingTurnId))!,
new P.Point(swimlane.midX, this.ypos));
}
this.advance(LIFELINE_WIDTH);
swimlane.addEntry("turn-start", P.text(
entry.item.value.id.asPreservesText() + ': ' + fromJS(entry.item.value.cause).asPreservesText()));
entry.item.value.actions.forEach(a => {
let entryClass: string;
switch (a._variant) {
case "dequeue":
case "dequeueInternal":
entryClass = "event"; break;
case "enqueue":
case "enqueueInternal":
case "link":
case "spawn":
entryClass = "action"; break;
case "facetStart":
case "facetStop":
entryClass = "internal"; break;
case "linkedTaskStart":
entryClass = "start"; break;
}
this.advance(LIFELINE_WIDTH);
const fPos = this.ypos;
const e = swimlane.addEntry(entryClass, P.text(fromJS(a).asPreservesText()));
switch (a._variant) {
case "link": {
const other = this.swimlaneFor(a.childActor);
const f = this.rewindTo(fPos, () =>
other.addEntry(entryClass, P.text(
`linked to ${fromJS(a.parentActor).asPreservesText()}`)));
// TODO: compute marker size from the actual definition
const arrowheadSpace = new P.Point(
(15 - 3.75) * (other.midX > swimlane.midX ? -1 : 1),
0);
const ps = other.midX > swimlane.midX
? e.bounds.midRight.sub(arrowheadSpace)
: e.bounds.midLeft.sub(arrowheadSpace);
const pe = other.midX > swimlane.midX
? f.bounds.midLeft.add(arrowheadSpace)
: f.bounds.midRight.sub(arrowheadSpace);
this.overlay.appendChild(P.line(ps, pe, {"class": "doublearrow"}).node);
break;
}
case "spawn": {
if (!a.link) {
const other = this.swimlaneFor(a.id);
this.connect(new P.Point(swimlane.midX, this.ypos),
new P.Point(other.midX, this.ypos));
}
break;
}
default:
break;
}
});
this.advance(LIFELINE_WIDTH);
this.turnMap.set(fromJS(entry.item.value.id), new P.Point(swimlane.midX, this.ypos));
activation.updateBounds(b => b.withHeight(this.ypos - b.top));
this.underlay.appendChild(activation.node);
this.advance(POST_TURN_GAP - VERTICAL_GAP);
swimlane.updateLifelineHeight();
break;
}
case "stop": {
this.advance(VERTICAL_GAP);
const swimlane = this.swimlaneFor(entry.actor);
switch (entry.item.status._variant) {
case "ok":
swimlane.addEntry("stop-ok", P.text("✓"));
break;
case "Error":
swimlane.addEntry("stop-error", P.text(
"ERROR: " + fromJS(entry.item.status.value).asPreservesText(),
{"class": "stop-error"}));
break;
}
this.advance(LIFELINE_WIDTH);
swimlane.addEntry(null, P.rect(TERMINATOR_SIZE, {"class": "terminator"}));
this.advance(LIFELINE_WIDTH);
swimlane.finish();
break;
}
}
2022-01-24 08:28:56 +00:00
}
}