Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(swingset): slogfile visualization: PlantUML, causeway (WIP) #3624

Draft
wants to merge 45 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ec62b75
wip(startPSM): instasllGovContracts
dckc Aug 22, 2022
7b8f867
chore(inter-protocol): update vat installation in startPSM
dckc Aug 22, 2022
2833838
build(inter-protocol): build bundle-psm.js
dckc Aug 22, 2022
28a8654
chore(inter-protocol): install PSM contract with gov contracts
dckc Aug 22, 2022
6d07021
chore(inter-protocol): export PSM related manifests
dckc Aug 22, 2022
cd89d55
feat(vats): boot-psm provides kernel access to bootstrap space
dckc Aug 22, 2022
db92ef2
feat(vats): boot-psm starts the PSM contract
dckc Aug 22, 2022
9777300
chore(vats): attenuate calls from boot-psm using manifest
dckc Aug 22, 2022
23b7a9d
feat(vats): PSM-only SwingSet config
dckc Aug 22, 2022
0e9aede
test(cosmic-swingset): scenario2-run-chain-psm Makefile rule
dckc Aug 22, 2022
9d32116
feat(vats): configurable top-level agoricNames
dckc Aug 22, 2022
e3ef2af
chore(vats): publish appropriate top-level agoricNames in boot-psm
dckc Aug 22, 2022
26416ef
chore(vats): startWalletFactory without provisioning mailbox access
dckc Aug 23, 2022
ac01a32
chore(vats): keep smartWalletStartResult in bootstrap space
dckc Aug 23, 2022
7d9814f
feat: convert Swingset slogfile to causeway format (WIP)
dckc Aug 7, 2021
3fd4c6e
chore: use .mjs extension for slog-to-causeway
dckc Aug 7, 2021
2b13ead
feat(swingset): slogfile to causeway works for one file
dckc Aug 7, 2021
bf038aa
fix: anchor, FQDN for log class (SQUASHME?)
dckc Aug 7, 2021
a5bd2bd
docs: braindump TODOs
dckc Aug 7, 2021
2afa785
feat(swingset): slog to PlantUML
dckc Aug 25, 2021
16acf82
feat: promise resolutions in slog-to-diagram
dckc Aug 25, 2021
ca98c65
feat: show compute in slog-to-diagram
dckc Aug 25, 2021
64fbfe9
feat: highlight expensive compute cranks in slog-to-diagram
dckc Aug 25, 2021
6ed5777
feat: show active vat; dotted resolve arrows
dckc Oct 14, 2021
34197fe
feat: vat names on sequence diagrams
dckc Oct 14, 2021
a7db80c
chore: another run at slog-to-causeway (WIP)
dckc Oct 14, 2021
386dba7
fix: SentIf events need message
dckc Oct 15, 2021
d3a025d
chore: maybe colons aren't allowed?
dckc Oct 17, 2021
ffe4793
feat: provide _some_ trace calls in all cases
dckc Oct 17, 2021
0c03bc5
chore: omit trace on promise resolution events, for now
dckc Oct 17, 2021
e7b0793
style: provide @yields
dckc Oct 26, 2021
bb81b1f
chore: console / comment TODO; types
dckc Oct 26, 2021
1ef9a70
fix: represent promise resolution just like message delivery
dckc Oct 26, 2021
671eb8c
chore: revert vat label to vNN:name rather than vNN_name
dckc Oct 26, 2021
0c8385a
chore: move causeway tools under misc-tools
dckc Oct 26, 2021
9bce8d6
style(slog-to-diagram): lint
dckc Mar 10, 2022
92622ed
fix(slog-to-diagram): more robust against missing elements
dckc Mar 10, 2022
8fb8130
refactor(slog-to-diagram): factor out puml-specific part
dckc May 16, 2022
1be2500
feat(slog-to-diagram): mermaid output (WIP)
dckc May 16, 2022
18b3a4b
fix(slog-to-diagram): stray )
dckc May 16, 2022
011a6da
fixup: more refactor
dckc May 16, 2022
91593c3
chore(slog-to-diagram): clean up activate / deactivate
dckc May 16, 2022
03321e1
chore(slog-to-causeway): ignore new types
dckc May 16, 2022
ca10e4f
chore(SwingSet): update slog-to-diagram w.r.t. methargs etc.
dckc Aug 25, 2022
2ee9664
feat(slog-to-diagram): put crank num on labels
dckc Aug 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 390 additions & 0 deletions packages/SwingSet/misc-tools/slog-to-causeway.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
/**
* TODO: hoist some top level docs
*/

// @ts-check

import { pipeline } from 'stream';

import './types.js';

const { freeze } = Object;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is freeze different from harden?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freeze is part of the JS standard library. I was avoiding dependencies on the Agoric platform while building this tool.

Substantively: freeze isn't recursive.


/**
* @typedef { |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does @typedef { | Something } mean?

oh, it's only used on line 17, so it expands to

@typedef {
org.ref_send.log.
org.ref_send.log.Something
} LogClassT

Should the first entry end in a bare dot? Nope, from the def'n on line 20, it looks like the empty case shouldn't be included. Does @typedef drop it, or is this a mistake?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The leading | in @typedef { | Something } is a little like the trailing , in const x = [1,2,3,]. It's ignored.

But line breaking rules in typescript-in-jsdoc are weird, and the declaration breaks without it.

* 'Event' | 'Got' | 'Sent' | 'Resolved' | 'Fulfilled' | 'Rejected' | 'SentIf' | 'Returned'
* } LogClassName
* @typedef { `org.ref_send.log.${LogClassName}` } LogClassT
*/
/** @type { Record<string, LogClassT> } */
const LogClass = freeze({
Event: 'org.ref_send.log.Event',
Got: 'org.ref_send.log.Got',
Sent: 'org.ref_send.log.Sent',
// Progressed: 'log.ref_send.Progressed',
Resolved: 'org.ref_send.log.Resolved',
Fulfilled: 'org.ref_send.log.Fulfilled',
Rejected: 'org.ref_send.log.Rejected',
SentIf: 'org.ref_send.log.SentIf',
Returned: 'org.ref_send.log.Returned',
});

/**
* uniquely identifies the origin; for example,
* as message send as the 2nd messaging event from the buyer vat, turn 3
*
* @typedef {{
* number: number,
* turn: {
* loop: string, // identifies the vat by a unique string
* number: number,
* },
* }} Anchor
*/

/**
* @typedef {{
* anchor: Anchor,
* text?: string,
* trace?: {
* calls: Array<{
* name: string,
* source: string,
* span: Span,
* }>
* }
* }} TraceRecord
*
* @typedef { TraceRecord & { class: LogClassT[] }} TraceEvent
*
* @typedef { [start: Loc] | [start: Loc, end: Loc] } Span
* @typedef { [line: number] | [line: number, column: number] } Loc
*/

/**
* An eventual send has two log entries: a `Sent` and its corresponding `Got`.
*
* The timestamp field is optional. Currently, Causeway ignores it.
*
* ref: http://wiki.erights.org/wiki/Causeway_Platform_Developer
* cribbed from https://github.com/cocoonfx/causeway/blob/master/src/js/com/teleometry/causeway/purchase_example/workers/makeCausewayLogger.js
*/
const makeCausewayFormatter = () => {
const self = freeze({
/**
* @param {Anchor} anchor
* @param {string} message a generated string which uniquely identifies a message
* @param { string } text
*/
makeGot: (anchor, message, text) =>
freeze({
class: [LogClass.Got, LogClass.Event],
anchor,
message,
trace: { calls: [{ name: text, source: '@@' }] },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does '@@' do? It looks like you expect the source location to be substituted, but I couldn't find any JS doc saying that's the case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'@@' means "I don't have a source file name, but the causeway code ignores this whole node unless it has something here, so I'll stick some arbitrary marker for now."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps '<unknown-source>'. I’ve used this pattern elsewhere to improve error messages like JSON parse errors for a file with a name that defaults to '<unknown-something>'.

}),
/**
* @param {Anchor} anchor
* @param { string } message
* @param { string } text
* @param {LogClassT[]} refinement
*/
makeSent: (anchor, message, text, refinement = []) =>
freeze({
class: [...refinement, LogClass.Sent, LogClass.Event],
anchor,
message,
text,
trace: { calls: [{ name: text, source: '@@' }] },
}),
/**
* @param {Anchor} anchor
* @param { string } message
* @param { string } text
*/
makeReturned: (anchor, message, text) =>
self.makeSent(anchor, message, text, [LogClass.Returned]),
/**
* @param {Anchor} anchor
* @param { string } message
* @param { string } condition
*/
makeSentIf: (anchor, message, condition) =>
freeze({
class: [LogClass.SentIf, LogClass.Sent, LogClass.Event],
anchor,
message,
condition,
// trace: { calls: [{ name: '???@@', source: '@@' }] },
}),
/**
* @param {Anchor} anchor
* @param { string } condition
* @param {LogClassT[]} status
*/
makeResolved: (anchor, condition, status = []) =>
freeze({
class: [...status, LogClass.Resolved, LogClass.Event],
anchor,
condition,
// trace: { calls: [{ name: '???@@', source: '@@' }] },
}),
/**
* @param {Anchor} anchor
* @param { string } condition
*/
makeFulfilled: (anchor, condition) =>
self.makeResolved(anchor, condition, [LogClass.Fulfilled]),
/**
* @param {Anchor} anchor
* @param { string } condition
*/
makeRejected: (anchor, condition) =>
self.makeResolved(anchor, condition, [LogClass.Rejected]),
});
return self;
};

/**
* @param {AsyncIterable<SlogEntry>} entries
* @yields { TraceEvent }
*/
async function* slogToCauseway(entries) {
const dest = makeCausewayFormatter();

/** @type { Map<string, SlogCreateVatEntry> } */
const vatInfo = new Map();

/** @type { (tag: string, obj: Record<string, unknown>) => Error } */
const notImpl = (tag, obj) =>
Error(`not implemented: ${tag}: ${JSON.stringify(Object.keys(obj))}`);
/** @type { (result: string, target: string, method: string) => string } */
// const msgId = (result, target, method) => `${result}<-${target}.${method}`;

/**
* @param { SlogVatEntry } entry
* @returns { Anchor }
*/
const anchor = entry => {
const { crankNum, vatID, deliveryNum } = entry;
const { name } = vatInfo.get(vatID);
const loop = `${vatID}:${name || '???'}`;
return freeze({
number: crankNum,
turn: { loop, number: deliveryNum },
});
};

/** @type { Map<string, SlogSyscallEntry> } */
const sent = new Map();
/** @type { Map<string, SlogDeliveryEntry> } */
const got = new Map();
/** @type { Map<string, SlogVatEntry & { rejected: boolean }> } */
const resolved = new Map();
/** @type { Map<string, SlogDeliveryEntry> } */
const notified = new Map();

for await (const entry of entries) {
switch (entry.type) {
case 'create-vat':
vatInfo.set(entry.vatID, entry);
break;
case 'deliver': {
const { kd } = entry;
switch (kd[0]) {
case 'message': {
const [_tag, _target, { result }] = kd;
got.set(result, entry);
break;
}
case 'notify': {
const [_tag, resolutions] = kd;
for (const [kp] of resolutions) {
notified.set(`R${kp}`, entry);
}
break;
}
case 'retireImports':
case 'retireExports':
case 'dropExports':
break; // ignore
default:
notImpl(kd[0], { kd });
}
break;
}
case 'syscall': {
switch (entry.ksc[0]) {
case 'send': {
const {
ksc: [_tag, _target, { result }],
} = entry;
sent.set(result, entry);
break;
}
case 'resolve': {
const {
ksc: [_, _thatVat, parts],
} = entry;
for (const [kp, rejected, _args] of parts) {
const { time, crankNum, vatID, deliveryNum } = entry;
resolved.set(`R${kp}`, {
time,
crankNum,
vatID,
deliveryNum,
rejected,
});
}
break;
}
case 'invoke':
case 'exit':
case 'subscribe':
case 'vatstoreGet':
case 'vatstoreSet':
case 'vatstoreDelete':
case 'vatstoreGetAfter':
case 'dropImports':
case 'retireImports':
case 'retireExports':
break; // irrelevant. ignore.
default:
throw notImpl(entry.ksc[0], {});
}
break;
}
case 'deliver-result':
case 'import-kernel-start':
case 'import-kernel-finish':
case 'vat-startup-start':
case 'vat-startup-finish':
case 'start-replay':
case 'finish-replay':
case 'start-replay-delivery':
case 'finish-replay-delivery':
case 'cosmic-swingset-begin-block':
case 'cosmic-swingset-end-block-start':
case 'cosmic-swingset-bootstrap-block-start':
case 'cosmic-swingset-bootstrap-block-finish':
case 'cosmic-swingset-end-block-finish':
case 'cosmic-swingset-deliver-inbound':
case 'syscall-result':
case 'clist':
case 'crank-start':
case 'crank-finish':
case 'terminate':
break; // irrelevant. ignore.
case 'console':
break; // TODO: log comment
default:
throw notImpl(entry.type, entry);
}
}

for (const [kp, src] of sent) {
// send / sent
const { ksc } = src;
if (ksc[0] !== 'send') throw TypeError();
const [
_,
ko,
{
method,
args: { body },
},
] = ksc;
const label = `${kp} <- ${ko}.${method}(${body.length})`;
yield dest.makeSent(anchor(src), kp, label);

// message / got
if (got.has(kp)) {
const target = got.get(kp);
if (target.kd[0] !== 'message') throw TypeError();
const [_0, t, { method: m }] = target.kd;
yield dest.makeGot(anchor(target), kp, `${t}.${m}(${body.length})`);
} else {
console.warn('no Got for', kp);
// TODO: check for missing data in the other direction?
}
}

for (const [rkp, src] of resolved) {
// resolve / resolved
const status = src.rejected ? 'rejected' : 'resolved';
yield dest.makeSent(anchor(src), rkp, `${status} ${rkp}`);
// deliver / returned
if (notified.has(rkp)) {
const target = notified.get(rkp);
yield dest.makeGot(anchor(target), rkp, rkp);
} else {
console.warn('no notified for', rkp);
// TODO: check for missing data in the other direction?
}
}
}

/**
* TODO: refactor as readLines, map JSON.parse
*
* @param {AsyncIterable<Buffer>} data
* @yields { unknown }
*/
async function* readJSONLines(data) {
let buf = '';
for await (const chunk of data) {
buf += chunk;
for (let pos = buf.indexOf('\n'); pos >= 0; pos = buf.indexOf('\n')) {
const line = buf.slice(0, pos);
yield JSON.parse(line);
buf = buf.slice(pos + 1);
}
}
}

/**
* @param { AsyncIterable<unknown> } items
* @yields { string }
*/
async function* writeJSONArray(items) {
yield '[';
let sep = false;
for await (const item of items) {
if (sep) {
yield ',';
} else {
sep = true;
}
yield '\n';
yield `${JSON.stringify(item)}`;
}
yield '\n]\n';
}

/**
* @param {{
* stdin: typeof process.stdin,
* stdout: typeof process.stdout,
* }} io
*/
const main = async ({ stdin, stdout }) =>
pipeline(
stdin,
readJSONLines,
slogToCauseway,
writeJSONArray,
stdout,
err => {
if (err) throw err;
},
);

/* TODO: only call main() if this is from CLI */
/* global process */
main({
stdin: process.stdin,
stdout: process.stdout,
}).catch(err => {
console.error(err);
process.exit(1);
});
Loading