This commit is contained in:
Tony Garnock-Jones 2022-01-26 22:12:06 +01:00
parent f5a04ff608
commit 5990d13e07
2 changed files with 174 additions and 30 deletions

View File

@ -1,7 +1,7 @@
/// 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";
import { Dataspace, IdentitySet, Ref, Bytes, Observe, Turn, Reader, genericEmbeddedTypeDecode, Schemas, GenericEmbedded, Dictionary, fromJS, stringify } from "@syndicate-lang/core";
import { QuasiValue as Q } from "@syndicate-lang/core";
const Trace = Schemas.trace;
import * as P from './pict';
@ -70,19 +70,75 @@ function bootRenderer(ds: Ref) {
const vs = new Reader(bs.fromUtf8(), {
embeddedDecode: genericEmbeddedTypeDecode,
}).readToEnd();
for (const v of vs) {
r.addTraceEntry(Trace.asTraceEntry(v));
const f = Turn.activeFacet;
// let count = 0;
function loop(n: number) {
// if (count >= 100) return;
if (n > 0) {
const v = vs.shift();
if (v !== void 0) {
r.addTraceEntry(Trace.asTraceEntry(v));
// count++;
loop(n - 1);
}
} else {
setTimeout(() => f.turn(() => loop(1)), 100);
r.container.scrollLeft =
(r.playground.width.baseVal.value - r.container.getBoundingClientRect().width)
/ 2;
}
}
r.container.scrollLeft =
(r.playground.width.baseVal.value - r.container.getBoundingClientRect().width)
/ 2;
loop(0);
}
}
}
}
function indented(x: any): string {
return stringify(x, { indent: 2});
}
function prettyFacetPath(p: Schemas.trace.FacetId<GenericEmbedded>[]): string {
return p.map(i => stringify(i)).join(':');
}
function reindent(s: string, amount: number, firstAmount: number = amount): string {
const indent = ' '.repeat(amount);
return s.split('\n').map((line, i) => {
if (i === 0) {
return ' '.repeat(firstAmount) + line;
} else {
return indent + line;
}
}).join('\n');
}
function prettyName(n: Schemas.trace.Name<GenericEmbedded>): string {
switch (n._variant) {
case "named":
return indented(n.name);
case "anonymous":
return '(anonymous)';
}
}
function prettyTarget(e: Schemas.trace.Target<GenericEmbedded>): string {
const oid = typeof e.oid === 'number'
? ('0000000000000000' + e.oid.toString(16)).slice(-16)
: stringify(e.oid);
return `${stringify(e.actor)}/${stringify(e.facet)}:${oid}`;
}
function prettyAssertionDescription(a: Schemas.trace.AssertionDescription<GenericEmbedded>): string {
switch (a._variant) {
case "value": return indented(a.value);
case "opaque": return `${stringify(a.description)}`;
}
}
class Swimlane {
readonly actorId: Schemas.trace.ActorId<GenericEmbedded>;
actorName: Schemas.trace.Name<GenericEmbedded> = Trace.Name.anonymous();
readonly renderer: Renderer;
readonly laneNumber: number;
readonly startY: number;
@ -97,6 +153,8 @@ class Swimlane {
this.laneNumber = laneNumber;
this.startY = renderer.ypos;
this.lifeline.node.appendChild(P.svg.tag(SVGTitleElement)`<title></title>`);
this.lifelineTooltipAppend(`Actor ID: ${indented(this.actorId)}`);
this.lifeline.updateBounds(b => b.withTopMid([this.midX, this.startY]));
this.updateLifelineHeight();
@ -109,10 +167,19 @@ class Swimlane {
this.tail?.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
}
lifelineTooltipAppend(s: string) {
const title = this.lifeline.node.querySelector('title')!;
title.appendChild(document.createTextNode(s));
}
get midX(): number {
return this.laneNumber * SWIMLANE_WIDTH;
}
get label(): string {
return `${prettyName(this.actorName)}[${stringify(this.actorId)}]`;
}
addEntry(klass: string | null, entry: P.BoundedPict<any>): P.BoundedPict<any> {
this.entries.push(entry);
@ -149,7 +216,9 @@ class Renderer {
readonly overlay: SVGGElement = this.playground.getElementById('overlay')! as any;
readonly swimlanes = new Dictionary<GenericEmbedded, Swimlane>();
readonly busyLanes = new IdentitySet<number>();
readonly turnMap = new Dictionary<GenericEmbedded, P.Point>();
readonly turnMap = new Dictionary<GenericEmbedded, { point: P.Point, swimlane: Swimlane }>();
readonly handleMap =
new Dictionary<GenericEmbedded, Schemas.trace.AssertionDescription<GenericEmbedded>>();
ypos = 0;
constructor (ds: Ref) {
@ -236,6 +305,25 @@ class Renderer {
}
}
prettyTurnEvent(t: Schemas.trace.TurnEvent<GenericEmbedded>): string {
switch (t._variant) {
case "assert":
this.handleMap.set(t.handle, t.assertion);
return `assert H${t.handle}${prettyAssertionDescription(t.assertion)}`;
case "retract": {
const assertion = this.handleMap.get(t.handle)
?? Trace.AssertionDescription.opaque(Symbol.for('???'));
return `retract H${t.handle}${prettyAssertionDescription(assertion)}`;
}
case "message":
return `message ${prettyAssertionDescription(t.body)}`;
case "sync":
return `sync ${prettyTarget(t.peer)}`;
case "breakLink":
return `link broken`;
}
}
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
switch (entry.item._variant) {
case "start": {
@ -243,61 +331,114 @@ class Renderer {
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()));
swimlane.actorName = entry.item.actorName;
swimlane.addEntry("start", P.text(swimlane.label));
swimlane.lifelineTooltipAppend(`\nActor name: ${prettyName(entry.item.actorName)}`);
break;
}
case "turn": {
const turn = entry.item.value;
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) {
switch (turn.cause._variant) {
case "turn":
causingTurnId = entry.item.value.cause.id;
causingTurnId = turn.cause.id;
break;
case "delay":
causingTurnId = entry.item.value.cause.causingTurn;
causingTurnId = turn.cause.causingTurn;
break;
default:
break;
}
if (causingTurnId !== null) {
this.connect(this.turnMap.get(fromJS(causingTurnId))!,
new P.Point(swimlane.midX, this.ypos));
const cause = (causingTurnId !== null)
? this.turnMap.get(fromJS(causingTurnId))!
: null;
if (cause !== null) {
this.connect(cause.point, new P.Point(swimlane.midX, this.ypos));
}
let triggerEvent: Schemas.trace.TargetedTurnEvent<GenericEmbedded> | null = null;
this.advance(LIFELINE_WIDTH);
swimlane.addEntry("turn-start", P.text(
entry.item.value.id.asPreservesText() + ': ' + fromJS(entry.item.value.cause).asPreservesText()));
indented(turn.id) + ': ' + indented(turn.cause)));
entry.item.value.actions.forEach(a => {
turn.actions.forEach(a => {
let entryClass: string;
let label: string;
let tooltip: string = `Turn ${stringify(turn.id)} in ${swimlane.label}`;
if (cause !== null) {
tooltip += `\n caused by ${stringify(causingTurnId!)} in ${cause.swimlane.label}`;
}
if (a._variant !== "dequeue" && a._variant !== "dequeueInternal" && triggerEvent !== null) {
tooltip += `\n while processing ${prettyTarget(triggerEvent.target)}${reindent(this.prettyTurnEvent(triggerEvent.detail), 4, 0)}`;
}
let isInternal = false;
switch (a._variant) {
case "dequeue":
case "dequeueInternal":
entryClass = "event"; break;
case "enqueue":
isInternal = true;
/* FALL THROUGH */
case "dequeue":
entryClass = "event";
label = `received for ${prettyTarget(a.event.target)}:\n${reindent(this.prettyTurnEvent(a.event.detail), 2)}`;
triggerEvent = a.event;
break;
case "enqueueInternal":
case "link":
case "spawn":
entryClass = "action"; break;
isInternal = true;
/* FALL THROUGH */
case "enqueue":
entryClass = "action";
label = `sending to ${prettyTarget(a.event.target)}:\n${reindent(this.prettyTurnEvent(a.event.detail), 2)}`;
break;
case "link": {
const other = this.swimlaneFor(a.childActor);
entryClass = "action";
label = `linked to ${other.label}`;
break;
}
case "spawn": {
const other = this.swimlaneFor(a.id);
entryClass = "action";
label = `spawn ${other.label}`;
break;
}
case "facetStart":
entryClass = "internal";
label = `start ${prettyFacetPath(a.path)}`;
break;
case "facetStop":
entryClass = "internal"; break;
entryClass = "internal";
label = `stop ${prettyFacetPath(a.path)}`;
switch (a.reason._variant) {
case "explicitAction": label += " (explicit action)"; break;
case "parentStopping": label += " (parent facet stopping)"; break;
case "inert": label += " (facet is inert)"; break;
case "actorStopping": label += " (actor terminating)"; break;
default:
label += '\n' + indented(a.reason);
break;
}
break;
case "linkedTaskStart":
entryClass = "start"; break;
entryClass = "start";
label = indented(a);
break;
}
this.advance(LIFELINE_WIDTH);
const fPos = this.ypos;
const e = swimlane.addEntry(entryClass, P.text(fromJS(a).asPreservesText()));
const e = swimlane.addEntry(entryClass, P.text(label));
if (tooltip !== null) {
e.node.appendChild(P.svg.tag(SVGTitleElement)`<title>${tooltip}</title>`);
}
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()}`)));
other.addEntry(entryClass, P.text(`linked to ${swimlane.label}`)));
// TODO: compute marker size from the actual definition
const arrowheadSpace = new P.Point(
(15 - 3.75) * (other.midX > swimlane.midX ? -1 : 1),
@ -324,7 +465,10 @@ class Renderer {
}
});
this.advance(LIFELINE_WIDTH);
this.turnMap.set(fromJS(entry.item.value.id), new P.Point(swimlane.midX, this.ypos));
this.turnMap.set(fromJS(turn.id), {
point: new P.Point(swimlane.midX, this.ypos),
swimlane,
});
activation.updateBounds(b => b.withHeight(this.ypos - b.top));
this.underlay.appendChild(activation.node);
this.advance(POST_TURN_GAP - VERTICAL_GAP);
@ -340,7 +484,7 @@ class Renderer {
break;
case "Error":
swimlane.addEntry("stop-error", P.text(
"ERROR: " + fromJS(entry.item.status.value).asPreservesText(),
"ERROR: " + indented(entry.item.status.value),
{"class": "stop-error"}));
break;
}

View File

@ -2,7 +2,7 @@
/// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { makeTemplate } from "@syndicate-lang/html";
const svg = makeTemplate('svg');
export const svg = makeTemplate('svg');
export type Pointlike =
{ x: number, y: number } | [number, number];