Skip to content

Commit

Permalink
Horizontal sequence diagram (#486)
Browse files Browse the repository at this point in the history
* make sequence diagram horizontal

* horizontal sequence diagram to show messages

* move into TimeTravelSlider

* introduce lerp and resize observer

* padding

* tweak widths

* put input on top of diagram

* disable explore button if there's no choose fn

* still show hops in sequence diagram when we rewind

* break out time travel slider

* put explore form inside of time travel slider

* rearrange slider

* filter out user ticks

* try putting ticks into the diagram

* fix ticks

* fix up patterns

ugh I hate the lack of type safety

* fix maxTime calculation
  • Loading branch information
vilterp committed Jun 17, 2024
1 parent f7f5af4 commit f5507d1
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 120 deletions.
2 changes: 2 additions & 0 deletions apps/actors/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useEffectfulReducer } from "../../uiCommon/generic/hooks";
import { CollapsibleWithHeading } from "../../uiCommon/generic/collapsible";
import { MultiClient } from "./ui/multiClient";
import { lastItem } from "../../util/util";
import { SequenceDiagram } from "../../uiCommon/visualizations/sequence";
import { Term, Statement, rec, varr } from "../../core/types";

const initialSystemsState = initialState(SYSTEMS);

Expand Down
24 changes: 22 additions & 2 deletions apps/actors/patterns.dl
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,35 @@ requestResponse{
internal.visualization{
name: "Sequence",
spec: sequence{
actors: actor{id: ID},
hops: hop{from: FromTick, to: ToTick},
actors: clientServerActor{id: ID},
hops: clientServerHop{from: FromTick, to: ToTick},
}
}.

messageFromActor{id: ID, fromActorID: FromActor, toActorID: ToActor, payload: Payload} :-
message{id: ID, fromTickID: FromTick, toActorID: ToActor, payload: Payload} &
tick{id: FromTick, actorID: FromActor}.

clientServerTick{time: TickID, place: ActorID} :-
tick{id: TickID, actorID: ActorID} &
clientServerActor{id: ActorID}.

clientServerActor{id: ID} :-
actor{id: ID, type: Type} &
Type != "user".

clientServerHop{
from: clientServerTick{time: FromTickID, place: FromActorID},
to: clientServerTick{time: ToTickID, place: ToActorID},
message: Payload,
} :-
hop{
from: tick{time: FromTickID, place: FromActorID},
to: tick{time: ToTickID, place: ToActorID},
message: Payload
} &
clientServerActor{id: FromActorID}.

hop{
from: tick{time: FromTickID, place: FromActorID},
to: tick{time: ToTickID, place: ToActorID},
Expand Down
14 changes: 14 additions & 0 deletions apps/actors/step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function step<ActorState extends Json, Msg extends Json>(
id: str(nextInitiator.to),
spawningTickID: int(nextInitiator.init.spawningTickID),
initialState: jsonToDL(spawn.initialState),
type: str(getActorType(nextInitiator.to)),
})
);
}
Expand Down Expand Up @@ -166,6 +167,19 @@ export function step<ActorState extends Json, Msg extends Json>(
return { newTrace, newInits: newMessages };
}

// TODO: pass this through directly from system
function getActorType(actorID: string): string {
if (actorID.startsWith("user")) {
return "user";
} else if (actorID.startsWith("client")) {
return "client";
} else if (actorID === "server") {
return "server";
} else {
throw new Error(`Unknown actor type for actorID: ${actorID}`);
}
}

export function spawnInitialActors<ActorState extends Json, Msg extends Json>(
update: UpdateFn<ActorState, Msg>,
interp: AbstractInterpreter,
Expand Down
49 changes: 27 additions & 22 deletions apps/actors/systems/kvSync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { spawnInitialActors } from "../../step";
import * as effects from "../../effects";
import {
ActorResp,
ChooseFn,
LoadedTickInitiator,
MessageToClient,
System,
Expand Down Expand Up @@ -43,29 +44,33 @@ export function makeActorSystem(app: KVApp): System<KVSyncState, KVSyncMsg> {
initialClientState: (id: string) =>
initialClientState(id, app.mutations, hashString(id)),
initialUserState: { type: "UserState" },
chooseNextMove: (system, state, randomSeed) => {
if (!app.choose) {
return [null, randomSeed];
}
const clientStates: { [clientID: string]: ClientState } = {};
for (const clientID of state.clientIDs) {
const clientState = state.trace.latestStates[`client${clientID}`];
clientStates[clientID] = clientState as ClientState;
}
const [mutation, nextRandomSeed] = app.choose(clientStates, randomSeed);
if (mutation === null) {
return [null, nextRandomSeed];
}
chooseNextMove: app.choose ? kvSyncChooseMove(app) : undefined,
};
}

function kvSyncChooseMove(app: KVApp): ChooseFn<KVSyncState, KVSyncMsg> {
return (system, state, randomSeed) => {
if (!app.choose) {
return [null, randomSeed];
}
const clientStates: { [clientID: string]: ClientState } = {};
for (const clientID of state.clientIDs) {
const clientState = state.trace.latestStates[`client${clientID}`];
clientStates[clientID] = clientState as ClientState;
}
const [mutation, nextRandomSeed] = app.choose(clientStates, randomSeed);
if (mutation === null) {
return [null, nextRandomSeed];
}

const msg: MessageToClient<MsgToClient> = {
clientID: mutation.clientID,
message: {
type: "RunMutation",
invocation: mutation.invocation,
},
};
return [msg, nextRandomSeed];
},
const msg: MessageToClient<MsgToClient> = {
clientID: mutation.clientID,
message: {
type: "RunMutation",
invocation: mutation.invocation,
},
};
return [msg, nextRandomSeed];
};
}

Expand Down
69 changes: 9 additions & 60 deletions apps/actors/ui/multiClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import {
TraceAction,
} from "../types";
import { Window } from "./window";
import { TimeTravelSlider } from "./timeTravelSlider";

export function MultiClient<St extends Json, Msg extends Json>(props: {
systemInstance: SystemInstance<St, Msg>;
dispatch: (action: TimeTravelAction<St, Msg>) => void;
}) {
const curState =
props.systemInstance.stateHistory[props.systemInstance.currentStateIdx];
const lastState =
props.systemInstance.stateHistory[
props.systemInstance.stateHistory.length - 1
];

const advance = (action: SystemInstanceAction<St, Msg>) => {
props.dispatch({ type: "Advance", action });
Expand Down Expand Up @@ -78,74 +83,18 @@ export function MultiClient<St extends Json, Msg extends Json>(props: {
</div>

<TimeTravelSlider<St, Msg>
interp={lastState.trace.interp}
exploreEnabled={
props.systemInstance.system.chooseNextMove === undefined
}
curIdx={props.systemInstance.currentStateIdx}
historyLength={props.systemInstance.stateHistory.length}
dispatch={(evt) => props.dispatch(evt)}
/>

{props.systemInstance.system.chooseNextMove ? (
<ExploreForm
onExplore={(steps) => props.dispatch({ type: "Explore", steps })}
/>
) : null}
</>
);
}

const DEFAULT_STEP_LIMIT = 100;

function ExploreForm(props: { onExplore: (steps: number) => void }) {
const [steps, setSteps] = React.useState(DEFAULT_STEP_LIMIT);

return (
<form onSubmit={() => props.onExplore(steps)}>
<button type="submit">Explore</button>{" "}
<input
type="number"
min={0}
max={3_000}
value={steps}
onChange={(evt) => setSteps(parseInt(evt.target.value))}
/>{" "}
steps
</form>
);
}

function TimeTravelSlider<St, Msg>(props: {
curIdx: number;
historyLength: number;
dispatch: (action: TimeTravelAction<St, Msg>) => void;
}) {
const atEnd =
props.historyLength === 0 || props.curIdx === props.historyLength - 1;

return (
<div>
<input
type="range"
min={0}
max={props.historyLength - 1}
value={props.curIdx}
onChange={(evt) => {
props.dispatch({
type: "TimeTravelTo",
idx: parseInt(evt.target.value),
});
}}
style={{ width: 500 }}
/>{" "}
{props.curIdx}/{props.historyLength - 1}{" "}
<button
disabled={atEnd}
onClick={() => props.dispatch({ type: "Branch" })}
>
Branch
</button>
</div>
);
}

function AddClientButton(props: { onClick: () => void }) {
return (
<div
Expand Down
95 changes: 95 additions & 0 deletions apps/actors/ui/timeTravelSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react";
import useResizeObserver from "use-resize-observer";
import { AbstractInterpreter } from "../../../core/abstractInterpreter";
import { rec, varr } from "../../../core/types";
import { SequenceDiagram } from "../../../uiCommon/visualizations/sequence";
import { TimeTravelAction } from "../types";

export function TimeTravelSlider<St, Msg>(props: {
interp: AbstractInterpreter;
curIdx: number;
historyLength: number;
exploreEnabled: boolean;
dispatch: (action: TimeTravelAction<St, Msg>) => void;
}) {
const { ref, width } = useResizeObserver();

const atEnd =
props.historyLength === 0 || props.curIdx === props.historyLength - 1;

return (
<div ref={ref}>
<div style={{ display: "flex", flexDirection: "row" }}>
{props.curIdx}/{props.historyLength - 1}{" "}
<input
type="range"
min={0}
max={props.historyLength - 1}
value={props.curIdx}
style={{ width: width - 40 }}
onChange={(evt) =>
props.dispatch({
type: "TimeTravelTo",
idx: parseInt(evt.target.value),
})
}
/>
<button
disabled={atEnd}
onClick={() => props.dispatch({ type: "Branch" })}
>
Branch
</button>{" "}
</div>
<SequenceDiagram
interp={props.interp}
id={"sequence"}
spec={rec("sequence", {
actors: rec("clientServerActor", { id: varr("ID") }),
ticks: rec("clientServerTick", {
time: varr("Time"),
place: varr("Place"),
}),
hops: rec("clientServerHop", {
from: varr("FromTick"),
to: varr("ToTick"),
}),
})}
width={width}
highlightedTerm={null}
setHighlightedTerm={() => {}}
runStatements={() => {}}
/>
{/* controls */}
<ExploreForm
disabled={props.exploreEnabled}
onExplore={(steps) => props.dispatch({ type: "Explore", steps })}
/>
</div>
);
}

const DEFAULT_STEP_LIMIT = 100;

function ExploreForm(props: {
disabled: boolean;
onExplore: (steps: number) => void;
}) {
const [steps, setSteps] = React.useState(DEFAULT_STEP_LIMIT);

return (
<form onSubmit={() => props.onExplore(steps)}>
<button type="submit" disabled={props.disabled}>
Explore
</button>{" "}
<input
type="number"
min={0}
max={3_000}
value={steps}
onChange={(evt) => setSteps(parseInt(evt.target.value))}
/>{" "}
steps
</form>
);
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@
"simple-markdown": "^0.7.3",
"use-hash-param": "^0.2.3",
"use-http": "^1.0.20",
"use-resize-observer": "^9.1.0",
"vega": "^5.22.1",
"vega-lite": "^5.2.0",
"w3c-hr-time": "^1.0.2"
},
"bin": {
"datalog-ts": "dist/cmd/repl.js"
}
}
}
Loading

0 comments on commit f5507d1

Please sign in to comment.