diff --git a/src/index.ts b/src/index.ts index ee3b18b..7d9991b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones -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[]): 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): string { + switch (n._variant) { + case "named": + return indented(n.name); + case "anonymous": + return '(anonymous)'; + } +} + +function prettyTarget(e: Schemas.trace.Target): 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): string { + switch (a._variant) { + case "value": return indented(a.value); + case "opaque": return `⌜${stringify(a.description)}⌝`; + } +} + class Swimlane { readonly actorId: Schemas.trace.ActorId; + actorName: Schemas.trace.Name = 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)``); + 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): P.BoundedPict { this.entries.push(entry); @@ -149,7 +216,9 @@ class Renderer { readonly overlay: SVGGElement = this.playground.getElementById('overlay')! as any; readonly swimlanes = new Dictionary(); readonly busyLanes = new IdentitySet(); - readonly turnMap = new Dictionary(); + readonly turnMap = new Dictionary(); + readonly handleMap = + new Dictionary>(); ypos = 0; constructor (ds: Ref) { @@ -236,6 +305,25 @@ class Renderer { } } + prettyTurnEvent(t: Schemas.trace.TurnEvent): 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) { 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 | 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 | 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)`${tooltip}`); + } 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; } diff --git a/src/pict.ts b/src/pict.ts index 2d0c37b..d04f957 100644 --- a/src/pict.ts +++ b/src/pict.ts @@ -2,7 +2,7 @@ /// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones import { makeTemplate } from "@syndicate-lang/html"; -const svg = makeTemplate('svg'); +export const svg = makeTemplate('svg'); export type Pointlike = { x: number, y: number } | [number, number];