Skip to content

Commit ec026eb

Browse files
feat: shreds slot labels
1 parent 30d9bb4 commit ec026eb

File tree

7 files changed

+633
-38
lines changed

7 files changed

+633
-38
lines changed

src/features/Overview/ShredsProgression/ShredsChart.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import type uPlot from "uplot";
55
import { chartAxisColor, gridLineColor, gridTicksColor } from "../../../colors";
66
import type { AlignedData } from "uplot";
77
import { xRangeMs } from "./const";
8-
import { shredsProgressionPlugin } from "./shredsProgressionPlugin";
98
import { useMedia, useRafLoop } from "react-use";
10-
import { Box } from "@radix-ui/themes";
9+
import {
10+
shredsProgressionPlugin,
11+
shredsXScaleKey,
12+
} from "./shredsProgressionPlugin";
13+
import { Box, Flex } from "@radix-ui/themes";
14+
import ShredsSlotLabels from "./ShredsSlotLabels";
1115

1216
const REDRAW_INTERVAL_MS = 40;
1317

@@ -89,24 +93,25 @@ export default function ShredsChart({
8993
width: 0,
9094
height: 0,
9195
scales: {
92-
x: { time: false },
96+
[shredsXScaleKey]: { time: false },
9397
y: {
9498
time: false,
9599
range: [0, 1],
96100
},
97101
},
98-
series: [{}, {}],
102+
series: [{ scale: shredsXScaleKey }, {}],
99103
cursor: {
100104
show: false,
101105
drag: {
102106
// disable zoom
103-
x: false,
107+
[shredsXScaleKey]: false,
104108
y: false,
105109
},
106110
},
107111
legend: { show: false },
108112
axes: [
109113
{
114+
scale: shredsXScaleKey,
110115
incrs: xIncrs,
111116
size: 30,
112117
ticks: {
@@ -152,21 +157,24 @@ export default function ShredsChart({
152157
});
153158

154159
return (
155-
<Box height="100%" mx={`-${chartXPadding}px`}>
156-
<AutoSizer>
157-
{({ height, width }) => {
158-
options.width = width;
159-
options.height = height;
160-
return (
161-
<UplotReact
162-
id={chartId}
163-
options={options}
164-
data={chartData}
165-
onCreate={handleCreate}
166-
/>
167-
);
168-
}}
169-
</AutoSizer>
170-
</Box>
160+
<Flex direction="column" gap="2px" height="100%">
161+
{!isOnStartupScreen && <ShredsSlotLabels />}
162+
<Box flexGrow="1" mx={`-${chartXPadding}px`}>
163+
<AutoSizer>
164+
{({ height, width }) => {
165+
options.width = width;
166+
options.height = height;
167+
return (
168+
<UplotReact
169+
id={chartId}
170+
options={options}
171+
data={chartData}
172+
onCreate={handleCreate}
173+
/>
174+
);
175+
}}
176+
</AutoSizer>
177+
</Box>
178+
</Flex>
171179
);
172180
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useAtomValue } from "jotai";
2+
import { Flex, Text } from "@radix-ui/themes";
3+
import { getSlotGroupLabelId, getSlotLabelId } from "./utils";
4+
import styles from "./shreds.module.css";
5+
import { useMemo } from "react";
6+
import { slotsPerLeader } from "../../../consts";
7+
import { shredsAtoms } from "./atoms";
8+
import { useSlotInfo } from "../../../hooks/useSlotInfo";
9+
import clsx from "clsx";
10+
import PeerIcon from "../../../components/PeerIcon";
11+
import { skippedClusterSlotsAtom } from "../../../atoms";
12+
import { isStartupProgressVisibleAtom } from "../../StartupProgress/atoms";
13+
14+
/**
15+
* Labels for shreds slots.
16+
* Don't render during startup, because there will be multiple overlapping slots
17+
* during the catching up phase.
18+
*/
19+
export default function ShredsSlotLabels() {
20+
const isStartup = useAtomValue(isStartupProgressVisibleAtom);
21+
const groupLeaderSlots = useAtomValue(shredsAtoms.groupLeaderSlots);
22+
23+
if (isStartup) return;
24+
25+
return (
26+
<Flex
27+
overflowX="hidden"
28+
position="relative"
29+
// extra space for borders
30+
height="30px"
31+
style={{ opacity: 0.8 }}
32+
>
33+
{groupLeaderSlots.map((slot) => (
34+
<SlotGroupLabel key={slot} firstSlot={slot} />
35+
))}
36+
</Flex>
37+
);
38+
}
39+
40+
interface SlotGroupLabelProps {
41+
firstSlot: number;
42+
}
43+
function SlotGroupLabel({ firstSlot }: SlotGroupLabelProps) {
44+
const { peer, name, isLeader } = useSlotInfo(firstSlot);
45+
const slots = useMemo(() => {
46+
return Array.from({ length: slotsPerLeader }, (_, i) => firstSlot + i);
47+
}, [firstSlot]);
48+
49+
const skippedClusterSlots = useAtomValue(skippedClusterSlotsAtom);
50+
const skippedSlots = useMemo(() => {
51+
const skipped = new Set<number>();
52+
for (const slot of slots) {
53+
if (skippedClusterSlots.has(slot)) {
54+
skipped.add(slot);
55+
}
56+
}
57+
return skipped;
58+
}, [slots, skippedClusterSlots]);
59+
60+
return (
61+
<Flex
62+
height="100%"
63+
direction="column"
64+
gap="2px"
65+
position="absolute"
66+
id={getSlotGroupLabelId(firstSlot)}
67+
className={clsx(styles.slotGroupLabel, {
68+
[styles.you]: isLeader,
69+
})}
70+
>
71+
<Flex
72+
justify="center"
73+
flexGrow="1"
74+
minWidth="0"
75+
px="2px"
76+
className={clsx(styles.slotGroupTopContainer, {
77+
[styles.skipped]: skippedSlots.size > 0,
78+
})}
79+
>
80+
<Flex
81+
align="center"
82+
gap="4px"
83+
minWidth="0"
84+
className={styles.slotGroupNameContainer}
85+
>
86+
<PeerIcon
87+
url={peer?.info?.icon_url}
88+
size={17}
89+
isYou={isLeader}
90+
hideTooltip
91+
/>
92+
<Text className={styles.name}>{name}</Text>
93+
</Flex>
94+
</Flex>
95+
96+
<Flex
97+
height="3px"
98+
position="relative"
99+
className={styles.slotBarsContainer}
100+
>
101+
{slots.map((slot) => (
102+
<div
103+
key={slot}
104+
className={clsx(styles.slotBar, {
105+
[styles.skipped]: skippedSlots.has(slot),
106+
})}
107+
id={getSlotLabelId(slot)}
108+
/>
109+
))}
110+
</Flex>
111+
</Flex>
112+
);
113+
}

src/features/Overview/ShredsProgression/__tests__/atoms.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
120120
});
121121
});
122122

123-
it("for non-startup: deletes slot numbers before max completed slot number that was completed after chart min X", () => {
123+
it("for non-startup: deletes slot numbers before max completed slot number that was completed before chart min X", () => {
124124
vi.useFakeTimers({
125125
toFake: ["Date"],
126126
});
@@ -161,13 +161,13 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
161161
{
162162
slot: 2,
163163
// this will be deleted even if it has an event in chart range,
164-
// because a slot number larger than it is marked as completed and being deleted
164+
// because a slot number larger than it is marked as completed and before chart min x
165165
ts: chartRangeNs + 1_000_000,
166166
e: ShredEvent.shred_repair_request,
167167
},
168168
{
169169
// max slot number that is complete before chart min X
170-
// delete this and all slot numbers before it
170+
// keep this and delete all slot numbers before it
171171
slot: 3,
172172
ts: chartRangeNs - 1_000_000,
173173
e: ShredEvent.slot_complete,
@@ -252,6 +252,15 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
252252
expect(result.current.slotsShreds).toEqual({
253253
referenceTs: 0,
254254
slots: new Map([
255+
[
256+
3,
257+
{
258+
shreds: [],
259+
minEventTsDelta: -1,
260+
maxEventTsDelta: -1,
261+
completionTsDelta: -1,
262+
},
263+
],
255264
[
256265
4,
257266
{
@@ -272,7 +281,7 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
272281
]),
273282
});
274283
expect(result.current.range).toEqual({
275-
min: 4,
284+
min: 3,
276285
max: 6,
277286
});
278287

src/features/Overview/ShredsProgression/atoms.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { atom } from "jotai";
22
import type { LiveShreds } from "../../../api/types";
33
import { maxShredEvent, ShredEvent } from "../../../api/entities";
44
import { delayMs, xRangeMs } from "./const";
5-
import { nsPerMs } from "../../../consts";
5+
import { nsPerMs, slotsPerLeader } from "../../../consts";
6+
import { getSlotGroupLeader } from "../../../utils";
67

78
type ShredEventTsDeltaMs = number | undefined;
89
/**
@@ -12,8 +13,11 @@ type ShredEventTsDeltaMs = number | undefined;
1213
*/
1314
export type ShredEventTsDeltas = ShredEventTsDeltaMs[];
1415

15-
type Slot = {
16+
export type Slot = {
1617
shreds: (ShredEventTsDeltas | undefined)[];
18+
/**
19+
* earliest event (start) of the slot
20+
*/
1721
minEventTsDelta?: number;
1822
maxEventTsDelta?: number;
1923
completionTsDelta?: number;
@@ -42,6 +46,18 @@ export function createLiveShredsAtoms() {
4246
*/
4347
minCompletedSlot: atom((get) => get(_minCompletedSlotAtom)),
4448
range: atom((get) => get(_slotRangeAtom)),
49+
groupLeaderSlots: atom((get) => {
50+
const range = get(_slotRangeAtom);
51+
if (!range) return [];
52+
53+
const slots = [getSlotGroupLeader(range.min)];
54+
while (slots[slots.length - 1] + slotsPerLeader - 1 < range.max) {
55+
slots.push(
56+
getSlotGroupLeader(slots[slots.length - 1] + slotsPerLeader),
57+
);
58+
}
59+
return slots;
60+
}),
4561
slotsShreds: atom((get) => get(_liveShredsAtom)),
4662
addShredEvents: atom(
4763
null,
@@ -187,8 +203,10 @@ export function createLiveShredsAtoms() {
187203
slot.completionTsDelta != null &&
188204
isBeforeChartX(slot.completionTsDelta, now, prev.referenceTs)
189205
) {
190-
// once we find a slot that is complete and far enough in the past, delete all slot numbers less it
206+
// once we find a slot that is complete and far enough in the past,
207+
// delete all slot numbers less it but keep this one for label spacing reference
191208
shouldDeleteSlot = true;
209+
continue;
192210
}
193211

194212
if (shouldDeleteSlot) {
@@ -203,8 +221,10 @@ export function createLiveShredsAtoms() {
203221
if (!prevRange || !prev.slots.size) {
204222
return;
205223
}
206-
prevRange.min = Math.min(...remainingSlotNumbers);
207-
return prevRange;
224+
return {
225+
min: Math.min(...remainingSlotNumbers),
226+
max: prevRange.max,
227+
};
208228
});
209229

210230
return prev;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
.slot-group-label {
2+
--group-x: -100000px;
3+
--group-name-opacity: 0;
4+
5+
background-color: #080b13;
6+
border-radius: 2px;
7+
border: 1px solid #3c4652;
8+
will-change: transform;
9+
transform: translate(var(--group-x));
10+
11+
&.you {
12+
border: 1px solid #2a7edf;
13+
}
14+
15+
.slot-group-top-container {
16+
width: 100%;
17+
background-color: #15181e;
18+
19+
&.skipped {
20+
background-color: var(--red-2);
21+
}
22+
23+
.slot-group-name-container {
24+
opacity: var(--group-name-opacity);
25+
transition: opacity 0.8s;
26+
will-change: opacity;
27+
28+
.name {
29+
font-size: 14px;
30+
line-height: normal;
31+
color: #ccc;
32+
white-space: nowrap;
33+
overflow: hidden;
34+
text-overflow: ellipsis;
35+
}
36+
}
37+
}
38+
39+
.slot-bars-container {
40+
flex-shrink: 0;
41+
.slot-bar {
42+
--slot-x: 0;
43+
44+
position: absolute;
45+
will-change: transform;
46+
transform: translate(var(--slot-x));
47+
48+
height: 100%;
49+
border-radius: 3px;
50+
&:nth-child(1) {
51+
background-color: var(--blue-7);
52+
}
53+
&:nth-child(2) {
54+
background-color: var(--blue-6);
55+
}
56+
&:nth-child(3) {
57+
background-color: var(--blue-5);
58+
}
59+
&:nth-child(4) {
60+
background-color: var(--blue-4);
61+
}
62+
63+
&.skipped {
64+
background-color: var(--red-7);
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)