Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
46 changes: 27 additions & 19 deletions src/features/Overview/ShredsProgression/ShredsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import type uPlot from "uplot";
import { chartAxisColor, gridLineColor, gridTicksColor } from "../../../colors";
import type { AlignedData } from "uplot";
import { xRangeMs } from "./const";
import { shredsProgressionPlugin } from "./shredsProgressionPlugin";
import { useMedia, useRafLoop } from "react-use";
import { Box } from "@radix-ui/themes";
import {
shredsProgressionPlugin,
type LabelPositions,
} from "./shredsProgressionPlugin";
import { Box, Flex } from "@radix-ui/themes";
import ShredsSlotLabels from "./ShredsSlotLabels";

const REDRAW_INTERVAL_MS = 40;

Expand Down Expand Up @@ -65,6 +69,7 @@ export default function ShredsChart({

const uplotRef = useRef<uPlot>();
const lastRedrawRef = useRef(0);
const labelPositionsRef = useRef<LabelPositions>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

don't get why we need this ref? This isn't used anywhere but the plugin right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is so I can compare the last + current draw label positions, and not update width / x position if it hasn't changed

Copy link
Collaborator

Choose a reason for hiding this comment

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

but why a ref vs just making some vars in your plugin? Since you're not consuming the ref from anywhere outside the plugin

Copy link
Collaborator

Choose a reason for hiding this comment

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

or are you re-initializing your plugin and you need a stable reference?


const handleCreate = useCallback((u: uPlot) => {
uplotRef.current = u;
Expand Down Expand Up @@ -136,7 +141,7 @@ export default function ShredsChart({
},
},
],
plugins: [shredsProgressionPlugin(isOnStartupScreen)],
plugins: [shredsProgressionPlugin(isOnStartupScreen, labelPositionsRef)],
};
}, [isOnStartupScreen, xIncrs]);

Expand All @@ -152,21 +157,24 @@ export default function ShredsChart({
});

return (
<Box height="100%" mx={`-${chartXPadding}px`}>
<AutoSizer>
{({ height, width }) => {
options.width = width;
options.height = height;
return (
<UplotReact
id={chartId}
options={options}
data={chartData}
onCreate={handleCreate}
/>
);
}}
</AutoSizer>
</Box>
<Flex direction="column" gap="2px" height="100%">
{!isOnStartupScreen && <ShredsSlotLabels />}
<Box flexGrow="1" mx={`-${chartXPadding}px`}>
<AutoSizer>
{({ height, width }) => {
options.width = width;
options.height = height;
return (
<UplotReact
id={chartId}
options={options}
data={chartData}
onCreate={handleCreate}
/>
);
}}
</AutoSizer>
</Box>
</Flex>
);
}
120 changes: 120 additions & 0 deletions src/features/Overview/ShredsProgression/ShredsSlotLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useAtomValue } from "jotai";
import { Flex, Text } from "@radix-ui/themes";
import { getSlotGroupLabelId, getSlotLabelId } from "./utils";
import styles from "./shreds.module.css";
import { useMemo } from "react";
import { slotsPerLeader } from "../../../consts";
import { shredsAtoms } from "./atoms";
import { useSlotInfo } from "../../../hooks/useSlotInfo";
import clsx from "clsx";
import PeerIcon from "../../../components/PeerIcon";
import { skippedClusterSlotsAtom } from "../../../atoms";
import { isStartupProgressVisibleAtom } from "../../StartupProgress/atoms";

const height = 30;

/**
* Labels for shreds slots.
* Don't render during startup, because there will be multiple overlapping slots
* during the catching up phase.
*/
export default function ShredsSlotLabels() {
const isStartup = useAtomValue(isStartupProgressVisibleAtom);
const groupLeaderSlots = useAtomValue(shredsAtoms.groupLeaderSlots);

if (isStartup) return;

return (
<Flex
overflow="hidden"
position="relative"
// extra space for borders
height={`${height + 2}px`}
Copy link
Collaborator

Choose a reason for hiding this comment

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

changing box sizing to border-box would work too i think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It doesn't seem to work with position: absolute 😢

style={{ opacity: 0.8 }}
>
{groupLeaderSlots.map((slot) => (
<SlotGroupLabel key={slot} firstSlot={slot} />
))}
</Flex>
);
}

interface SlotGroupLabelProps {
firstSlot: number;
}
function SlotGroupLabel({ firstSlot }: SlotGroupLabelProps) {
const { peer, name, isLeader } = useSlotInfo(firstSlot);
const slots = useMemo(() => {
return Array.from({ length: slotsPerLeader }, (_, i) => firstSlot + i);
}, [firstSlot]);

const skippedClusterSlots = useAtomValue(skippedClusterSlotsAtom);
const skippedSlots = useMemo(() => {
const skipped = new Set<number>();
for (const slot of slots) {
if (skippedClusterSlots.has(slot)) {
skipped.add(slot);
}
}
return skipped;
}, [slots, skippedClusterSlots]);

return (
<Flex
height={`${height}px`}
direction="column"
gap="2px"
position="absolute"
align="center"
overflow="hidden"
id={getSlotGroupLabelId(firstSlot)}
className={clsx(styles.slotGroupLabel, {
[styles.you]: isLeader,
})}
>
<Flex
align="center"
flexGrow="1"
px="2px"
className={clsx(styles.slotGroupTopContainer, {
[styles.skipped]: skippedSlots.size > 0,
})}
>
<Flex
justify="center"
align="center"
width="100%"
gap="4px"
wrap="nowrap"
className={styles.slotGroupNameContainer}
>
<PeerIcon
url={peer?.info?.icon_url}
size={17}
isYou={isLeader}
hideTooltip
/>
<Text className={styles.name}>{name}</Text>
</Flex>
</Flex>

<Flex
width="100%"
height="3px"
position="relative"
overflow="hidden"
className={styles.slotBarsContainer}
>
{slots.map((slot) => (
<div
key={slot}
className={clsx(styles.slotBar, {
[styles.skipped]: skippedSlots.has(slot),
})}
id={getSlotLabelId(slot)}
/>
))}
</Flex>
</Flex>
);
}
17 changes: 13 additions & 4 deletions src/features/Overview/ShredsProgression/__tests__/atoms.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
});
});

it("for non-startup: deletes slot numbers before max completed slot number that was completed after chart min X", () => {
it("for non-startup: deletes slot numbers before max completed slot number that was completed before chart min X", () => {
vi.useFakeTimers({
toFake: ["Date"],
});
Expand Down Expand Up @@ -161,13 +161,13 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
{
slot: 2,
// this will be deleted even if it has an event in chart range,
// because a slot number larger than it is marked as completed and being deleted
// because a slot number larger than it is marked as completed and before chart min x
ts: chartRangeNs + 1_000_000,
e: ShredEvent.shred_repair_request,
},
{
// max slot number that is complete before chart min X
// delete this and all slot numbers before it
// keep this and delete all slot numbers before it
slot: 3,
ts: chartRangeNs - 1_000_000,
e: ShredEvent.slot_complete,
Expand Down Expand Up @@ -252,6 +252,15 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
expect(result.current.slotsShreds).toEqual({
referenceTs: 0,
slots: new Map([
[
3,
{
shreds: [],
minEventTsDelta: -1,
maxEventTsDelta: -1,
completionTsDelta: -1,
},
],
[
4,
{
Expand All @@ -272,7 +281,7 @@ describe("live shreds atoms with reference ts and ts deltas", () => {
]),
});
expect(result.current.range).toEqual({
min: 4,
min: 3,
max: 6,
});

Expand Down
25 changes: 21 additions & 4 deletions src/features/Overview/ShredsProgression/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { atom } from "jotai";
import type { LiveShreds } from "../../../api/types";
import { maxShredEvent, ShredEvent } from "../../../api/entities";
import { delayMs, xRangeMs } from "./const";
import { nsPerMs } from "../../../consts";
import { nsPerMs, slotsPerLeader } from "../../../consts";
import { getSlotGroupLeader } from "../../../utils";

type ShredEventTsDeltaMs = number | undefined;
/**
Expand Down Expand Up @@ -42,6 +43,18 @@ export function createLiveShredsAtoms() {
*/
minCompletedSlot: atom((get) => get(_minCompletedSlotAtom)),
range: atom((get) => get(_slotRangeAtom)),
groupLeaderSlots: atom((get) => {
const range = get(_slotRangeAtom);
if (!range) return [];

const slots = [getSlotGroupLeader(range.min)];
while (slots[slots.length - 1] + slotsPerLeader - 1 < range.max) {
slots.push(
getSlotGroupLeader(slots[slots.length - 1] + slotsPerLeader),
);
}
return slots;
}),
slotsShreds: atom((get) => get(_liveShredsAtom)),
addShredEvents: atom(
null,
Expand Down Expand Up @@ -187,8 +200,10 @@ export function createLiveShredsAtoms() {
slot.completionTsDelta != null &&
isBeforeChartX(slot.completionTsDelta, now, prev.referenceTs)
) {
// once we find a slot that is complete and far enough in the past, delete all slot numbers less it
// once we find a slot that is complete and far enough in the past,
// delete all slot numbers less it but keep this one for label spacing reference
shouldDeleteSlot = true;
continue;
}

if (shouldDeleteSlot) {
Expand All @@ -203,8 +218,10 @@ export function createLiveShredsAtoms() {
if (!prevRange || !prev.slots.size) {
return;
}
prevRange.min = Math.min(...remainingSlotNumbers);
return prevRange;
return {
min: Math.min(...remainingSlotNumbers),
max: prevRange.max,
};
});

return prev;
Expand Down
68 changes: 68 additions & 0 deletions src/features/Overview/ShredsProgression/shreds.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.slot-group-label {
--group-x: -100000px;
--group-name-opacity: 0;

background-color: #080b13;
border-radius: 2px;
border: 1px solid #3c4652;
will-change: transform, width;
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is width needed here and below?

transform: translate(var(--group-x));

&.you {
border: 1px solid #2a7edf;
}

.slot-group-top-container {
width: 100%;
background-color: #15181e;

&.skipped {
background-color: var(--red-2);
}

.slot-group-name-container {
opacity: var(--group-name-opacity);
transition: opacity 0.8s;
will-change: opacity;

.name {
font-size: 14px;
line-height: normal;
color: #ccc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}

.slot-bars-container {
flex-shrink: 0;
.slot-bar {
--slot-x: 0;

position: absolute;
will-change: transform, width;
transform: translate(var(--slot-x));

height: 100%;
border-radius: 3px;
&:nth-child(1) {
background-color: var(--blue-7);
}
&:nth-child(2) {
background-color: var(--blue-6);
}
&:nth-child(3) {
background-color: var(--blue-5);
}
&:nth-child(4) {
background-color: var(--blue-4);
}

&.skipped {
background-color: var(--red-7);
}
}
}
}
Loading