Skip to content

Commit 9fb493e

Browse files
committed
sim-rs: visualize recursive EBs
1 parent 343bab9 commit 9fb493e

File tree

7 files changed

+207
-83
lines changed

7 files changed

+207
-83
lines changed

sim-rs/parameters/full.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
leios-variant: full

ui/src/components/Blocks/modules/BlockContents.tsx

+167-59
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { ISimulationBlock, ISimulationTransaction } from '@/contexts/SimContext/types';
1+
import { ISimulationBlock, ISimulationEndorsementBlock, ISimulationInputBlock, ISimulationTransaction } from '@/contexts/SimContext/types';
22
import cx from "classnames";
3-
import { CSSProperties, FC, MouseEvent, PropsWithChildren, useMemo, useState } from "react";
3+
import { CSSProperties, FC, MouseEvent, PropsWithChildren, ReactElement, useMemo, useState } from "react";
44

55
import { printBytes } from '@/utils';
66
import classes from "./styles.module.css";
@@ -12,21 +12,34 @@ export interface IBlockContentsProps {
1212
interface IBoxProps extends PropsWithChildren {
1313
className?: string;
1414
style?: CSSProperties;
15-
selected?: boolean;
16-
select?: (e: MouseEvent) => void;
15+
hovered?: boolean;
16+
onHover?: (e: MouseEvent) => void;
17+
onClick?: (e: MouseEvent) => void;
1718
}
1819

19-
const Box: FC<IBoxProps> = ({ selected, select, children, className, style }) => {
20-
const color = selected ? "border-black" : "border-gray-400";
20+
const Box: FC<IBoxProps> = ({ hovered, onHover, onClick, children, className, style }) => {
21+
const color = hovered ? "border-black" : "border-gray-400";
2122
return (
22-
<span className={cx("border-2 border-solid min-h-24 max-h-48 w-48 flex flex-col items-center justify-center text-center", color, { 'cursor-pointer': !!select }, { [classes.selected]: selected }, className)} style={style}
23-
onMouseEnter={select}
24-
onMouseMove={select} >
23+
<span className={cx("border-2 border-solid min-h-24 max-h-48 w-48 flex flex-col items-center justify-center text-center", color, { 'cursor-pointer': !!onHover }, { [classes.hovered]: hovered }, className)} style={style}
24+
onMouseEnter={onHover}
25+
onMouseMove={onHover}
26+
onClick={onClick} >
2527
{children}
2628
</span >
2729
)
2830
}
2931

32+
interface ITabButtonProps {
33+
name: string;
34+
active: boolean;
35+
onClick: () => void;
36+
}
37+
38+
const TabButton: FC<ITabButtonProps> = ({ name, active, onClick }) => {
39+
const color = active ? "bg-[blue]" : "bg-gray-400";
40+
return <button className={`${color} text-white rounded-md px-4 py-2`} onClick={onClick}>{name}</button>
41+
}
42+
3043
interface ITXStats {
3144
name: string;
3245
slot: number;
@@ -64,11 +77,92 @@ const Stats: FC<IStatsProps> = ({ name, slot, txs, breakdown, position: [left, t
6477
)
6578
}
6679

67-
interface SelectState {
80+
interface HoverState {
6881
key: string,
6982
position: [number, number]
7083
}
7184

85+
enum ContentsView {
86+
Input,
87+
Endorser,
88+
}
89+
90+
interface IBlockProps {
91+
block: ISimulationBlock,
92+
hovered: boolean,
93+
onHover: (e: MouseEvent) => void;
94+
}
95+
96+
const Block: FC<IBlockProps> = ({ block, hovered, onHover }) => {
97+
return <div className={classes.block}>
98+
<Box hovered={hovered} onHover={onHover}>
99+
Block
100+
<span className='text-sm'>Slot {block.slot}, {block.txs.length} TX</span>
101+
</Box>
102+
</div>
103+
}
104+
105+
interface IInputBlocksProps {
106+
ibs: ISimulationInputBlock[],
107+
block: ISimulationBlock,
108+
hovered: HoverState | null,
109+
onHover: (box: string | null) => (e: MouseEvent) => void;
110+
}
111+
112+
const InputBlocksList: FC<IInputBlocksProps> = ({ ibs, hovered, block, onHover }) => {
113+
return <div className={cx("border-2 p-[25px] border-black max-w-[80vw] overflow-scroll")}>
114+
<div className="flex gap-2">
115+
{ibs.map(ib => {
116+
const isHovered = hovered?.key === ib.id;
117+
const proportion = Math.min(100, ib.txs.length * 1000 / block.txs.length);
118+
return (
119+
<Box key={ib.id} style={{ backgroundColor: `color-mix(in hsl, white, #82ca9d ${proportion}%)` }} hovered={isHovered} onHover={onHover(ib.id)} className={classes.input}>
120+
{isHovered ? 'Input Block' : 'IB'}
121+
<span className="text-sm">Slot&nbsp;{ib.slot}, {ib.txs.length}&nbsp;TX</span>
122+
</Box>
123+
);
124+
})}
125+
</div>
126+
</div>
127+
};
128+
129+
interface IEndorserBlockProps {
130+
eb: ISimulationEndorsementBlock,
131+
hovered: boolean,
132+
onHover: (e: MouseEvent) => void;
133+
onClick?: (e: MouseEvent) => void;
134+
}
135+
136+
const EndorserBlock: FC<IEndorserBlockProps> = ({ eb, hovered, onHover, onClick }) => {
137+
return <div className={classes.endorser}>
138+
<Box hovered={hovered} onHover={onHover} onClick={onClick}>
139+
Endorser Block
140+
<span className='text-sm'>Slot {eb.slot}</span>
141+
{eb.ibs.length ? <span className='text-sm'>References {eb.ibs.length} IB(s)</span> : null}
142+
{eb.ebs.length ? <span className='text-sm'>References {eb.ebs.length} EB(s)</span> : null}
143+
</Box>
144+
</div>
145+
};
146+
147+
interface IEndorserBlocksProps {
148+
ebs: ISimulationEndorsementBlock[],
149+
hovered: HoverState | null,
150+
onHover: (box: string | null) => (e: MouseEvent) => void;
151+
onClick: (eb: ISimulationEndorsementBlock) => (e: MouseEvent) => void;
152+
}
153+
154+
const EndorserBlocksList: FC<IEndorserBlocksProps> = ({ ebs, hovered, onHover, onClick }) => {
155+
return <div className={cx("border-2 p-[25px] border-black max-w-[80vw] overflow-scroll")}>
156+
<div className="flex gap-2">
157+
{ebs.map(eb => {
158+
return (
159+
<EndorserBlock key={eb.id} eb={eb} hovered={hovered?.key == eb.id} onHover={onHover(eb.id)} onClick={onClick(eb)} />
160+
);
161+
})}
162+
</div>
163+
</div>
164+
};
165+
72166
export const BlockContents: FC<IBlockContentsProps> = ({ block }) => {
73167
const stats = useMemo(() => {
74168
const result: Map<string, ITXStats> = new Map();
@@ -85,80 +179,94 @@ export const BlockContents: FC<IBlockContentsProps> = ({ block }) => {
85179
breakdown,
86180
});
87181
if (block.cert) {
88-
result.set("eb", {
89-
name: "Endorsement Block",
90-
slot: block.cert.eb.slot,
91-
txs: null,
92-
breakdown: [["Size", block.cert.eb.bytes]],
93-
});
94-
for (const ib of block.cert.eb.ibs) {
95-
result.set(ib.id, {
96-
name: "Input Block",
97-
slot: ib.slot,
98-
txs: ib.txs,
99-
breakdown: [
100-
["Header size", ib.headerBytes],
101-
],
182+
const ebs = [block.cert.eb];
183+
while (ebs.length) {
184+
const eb = ebs.pop()!;
185+
result.set(eb.id, {
186+
name: "Endorser Block",
187+
slot: eb.slot,
188+
txs: null,
189+
breakdown: [["Size", eb.bytes]],
102190
});
191+
for (const ib of eb.ibs) {
192+
result.set(ib.id, {
193+
name: "Input Block",
194+
slot: ib.slot,
195+
txs: ib.txs,
196+
breakdown: [
197+
["Header size", ib.headerBytes],
198+
],
199+
});
200+
}
201+
for (const nestedEb of eb.ebs) {
202+
ebs.push(nestedEb);
203+
}
103204
}
104205
}
105206
return result;
106207
}, [block]);
107208

108-
const [selected, setSelected] = useState<SelectState | null>(null);
109-
const selectBox = (box: string | null) => (e: MouseEvent) => {
209+
const [hovered, setHovered] = useState<HoverState | null>(null);
210+
const [viewing, setViewing] = useState(ContentsView.Input);
211+
const [ebTrail, setEbTrail] = useState(block.cert?.eb ? [block.cert.eb] : []);
212+
213+
const onHover = (box: string | null) => (e: MouseEvent) => {
110214
e.stopPropagation();
111215
if (box) {
112-
setSelected({
216+
setHovered({
113217
key: box,
114218
position: [e.pageX, e.pageY],
115219
});
116220
} else {
117-
setSelected(null);
221+
setHovered(null);
118222
}
119223
};
120224

121-
const eb = block.cert?.eb;
122-
const ibs = block.cert?.eb?.ibs ?? [];
225+
const viewParentEb = () => {
226+
setEbTrail(trail => trail.slice(1));
227+
}
228+
const viewChildEb = (eb: ISimulationEndorsementBlock) => () => {
229+
setEbTrail(trail => [eb, ...trail]);
230+
}
231+
232+
const eb: ISimulationEndorsementBlock | undefined = ebTrail[0];
233+
const ibs = eb?.ibs ?? [];
234+
const ebs = eb?.ebs ?? [];
235+
236+
const parent = ebTrail[1];
237+
const topRow = parent ? (
238+
<EndorserBlock eb={parent} hovered={hovered?.key == parent.id} onHover={onHover(parent.id)} onClick={viewParentEb} />
239+
) : (
240+
<Block block={block} hovered={hovered?.key == "block"} onHover={onHover("block")} />
241+
)
242+
243+
let bottomRow: ReactElement | null = null;
244+
if (viewing == ContentsView.Input && ibs.length) {
245+
bottomRow = <InputBlocksList ibs={ibs} block={block} hovered={hovered} onHover={onHover} />
246+
}
247+
if (viewing == ContentsView.Endorser && ebs.length) {
248+
bottomRow = <EndorserBlocksList ebs={ebs} hovered={hovered} onHover={onHover} onClick={viewChildEb} />
249+
}
123250

124251
return (
125252
<>
126-
{selected && <Stats {...stats.get(selected.key)!} position={selected.position} onMouseMove={selectBox(selected.key)} />}
253+
{hovered && <Stats {...stats.get(hovered.key)!} position={hovered.position} onMouseMove={onHover(hovered.key)} />}
127254

128-
<div className='flex flex-col w-full h-3/5 items-center' onMouseMove={selectBox(null)}>
255+
<div className='flex flex-col w-full h-3/5 items-center' onMouseMove={onHover(null)}>
129256
<h2 className='font-bold text-xl'>Block Transactions</h2>
130257
<div className="flex flex-col w-full h-full items-center justify-center">
131-
<div className={classes.block}>
132-
<Box selected={selected?.key === "block"} select={selectBox("block")}>
133-
Block
134-
<span className='text-sm'>Slot {block.slot}, {block.txs.length} TX</span>
135-
</Box>
136-
</div>
258+
{topRow}
137259
{eb && (<span className="flex flex-none w-[2px] h-[50px] bg-black" />)}
138260
{eb && (
139-
<div className={classes.endorser}>
140-
<Box selected={selected?.key === "eb"} select={selectBox("eb")}>
141-
Endorsement Block
142-
<span className='text-sm'>Slot {eb.slot}</span>
143-
</Box>
261+
<div className='flex gap-4 items-center'>
262+
<TabButton name="View IB(s)" active={viewing == ContentsView.Input} onClick={() => setViewing(ContentsView.Input)} />
263+
<EndorserBlock eb={eb} hovered={hovered?.key == eb.id} onHover={onHover(eb.id)} />
264+
<TabButton name="View EB(s)" active={viewing == ContentsView.Endorser} onClick={() => setViewing(ContentsView.Endorser)} />
144265
</div>
145266
)}
146-
{ibs.length ? <>
147-
<span className="flex flex-none w-[2px] h-[25px] bg-black" />
148-
<div className={cx("border-2 p-[25px] border-black max-w-[80vw] overflow-scroll")}>
149-
<div className="flex gap-2">
150-
{ibs.map(ib => {
151-
const isSelected = selected?.key === ib.id;
152-
const proportion = Math.min(100, ib.txs.length * 1000 / block.txs.length);
153-
return (
154-
<Box key={ib.id} style={{ backgroundColor: `color-mix(in hsl, white, #82ca9d ${proportion}%)` }} selected={isSelected} select={selectBox(ib.id)} className={classes.input}>
155-
{isSelected ? 'Input Block' : 'IB'}
156-
<span className="text-sm">Slot&nbsp;{ib.slot}, {ib.txs.length}&nbsp;TX</span>
157-
</Box>
158-
);
159-
})}
160-
</div>
161-
</div>
267+
{bottomRow ? <>
268+
<span className="flex flex-none w-[2px] h-[50px] bg-black" />
269+
{bottomRow}
162270
</> : null}
163271
</div>
164272
</div>

ui/src/components/Blocks/modules/styles.module.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
flex-shrink: 1;
1616
}
1717

18-
.input.selected {
18+
.input.hovered {
1919
flex-shrink: 0;
2020
}

ui/src/components/Graph/modules/NodeStats.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const NodeStats: FC = () => {
7575
const data = [
7676
{ name: "Transactions", ...getCounts("tx"), color: '#26de81' },
7777
{ name: "Input Blocks", ...getCounts("ib"), color: '#2bcbba' },
78-
{ name: "Endorsement Blocks", ...getCounts("eb"), color: '#4b7bec' },
78+
{ name: "Endorser Blocks", ...getCounts("eb"), color: '#4b7bec' },
7979
{ name: "Votes", ...getCounts("votes"), color: '#2d98da' },
8080
{ name: "Blocks", ...getCounts("pb"), color: '#fc5c65' },
8181
]

ui/src/components/Sim/hooks/utils.ts

+30-22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ISimulationAggregatedData,
55
ISimulationAggregatedDataState,
66
ISimulationBlock,
7+
ISimulationEndorsementBlock,
78
ISimulationIntermediateDataState,
89
} from "@/contexts/SimContext/types";
910

@@ -51,6 +52,33 @@ const getNodeData = (aggregationNodeDataRef: ISimulationAggregatedDataState, nod
5152
return data;
5253
};
5354

55+
const extractEb = (intermediate: ISimulationIntermediateDataState, ebId: string): ISimulationEndorsementBlock => {
56+
const eb = intermediate.ebs.get(ebId)!;
57+
const ibs = eb.ibs.map(id => {
58+
const ib = intermediate.ibs.get(id)!;
59+
for (const tx of ib.txs) {
60+
if (!intermediate.praosTxs.has(tx)) {
61+
intermediate.leiosTxs.add(tx);
62+
}
63+
}
64+
const txs = ib.txs.map(tx => intermediate.txs[tx]);
65+
return {
66+
id,
67+
slot: ib.slot,
68+
headerBytes: ib.headerBytes,
69+
txs,
70+
};
71+
});
72+
const ebs = eb.ebs.map(id => extractEb(intermediate, id));
73+
return {
74+
id: ebId,
75+
slot: eb.slot,
76+
bytes: eb.bytes,
77+
ibs,
78+
ebs,
79+
}
80+
};
81+
5482
export const processMessage = (
5583
json: IServerMessage,
5684
aggregatedData: ISimulationAggregatedDataState,
@@ -91,30 +119,9 @@ export const processMessage = (
91119
if (message.endorsement != null) {
92120
bytes += message.endorsement.bytes;
93121
const ebId = message.endorsement.eb.id;
94-
const eb = intermediate.ebs.get(ebId)!;
95-
const ibs = eb.ibs.map(id => {
96-
const ib = intermediate.ibs.get(id)!;
97-
for (const tx of ib.txs) {
98-
if (!intermediate.praosTxs.has(tx)) {
99-
intermediate.leiosTxs.add(tx);
100-
}
101-
}
102-
const txs = ib.txs.map(tx => intermediate.txs[tx]);
103-
return {
104-
id,
105-
slot: ib.slot,
106-
headerBytes: ib.headerBytes,
107-
txs,
108-
};
109-
})
110122
block.cert = {
111123
bytes: message.endorsement.bytes,
112-
eb: {
113-
id: ebId,
114-
slot: eb.slot,
115-
bytes: eb.bytes,
116-
ibs,
117-
}
124+
eb: extractEb(intermediate, ebId),
118125
}
119126
}
120127
trackDataGenerated(aggregatedData, intermediate, message.producer, "pb", message.id, bytes);
@@ -131,6 +138,7 @@ export const processMessage = (
131138
slot: message.slot,
132139
bytes: message.bytes,
133140
ibs: message.input_blocks.map(ib => ib.id),
141+
ebs: message.endorser_blocks.map(eb => eb.id),
134142
});
135143
} else if (message.type === EMessageType.EBSent) {
136144
trackDataSent(aggregatedData, intermediate, message.sender, "eb", message.id);

0 commit comments

Comments
 (0)