Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
50 changes: 29 additions & 21 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,
shredsXScaleKey,
} from "./shredsProgressionPlugin";
import { Box, Flex } from "@radix-ui/themes";
import ShredsSlotLabels from "./ShredsSlotLabels";

const REDRAW_INTERVAL_MS = 40;

Expand Down Expand Up @@ -89,24 +93,25 @@ export default function ShredsChart({
width: 0,
height: 0,
scales: {
x: { time: false },
[shredsXScaleKey]: { time: false },
y: {
time: false,
range: [0, 1],
},
},
series: [{}, {}],
series: [{ scale: shredsXScaleKey }, {}],
cursor: {
show: false,
drag: {
// disable zoom
x: false,
[shredsXScaleKey]: false,
y: false,
},
},
legend: { show: false },
axes: [
{
scale: shredsXScaleKey,
incrs: xIncrs,
size: 30,
ticks: {
Expand Down Expand Up @@ -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>
);
}
113 changes: 113 additions & 0 deletions src/features/Overview/ShredsProgression/ShredsSlotLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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";

/**
* 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
overflowX="hidden"
position="relative"
// extra space for borders
height="30px"
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="100%"
direction="column"
gap="2px"
position="absolute"
id={getSlotGroupLabelId(firstSlot)}
className={clsx(styles.slotGroupLabel, {
[styles.you]: isLeader,
})}
>
<Flex
justify="center"
flexGrow="1"
minWidth="0"
px="2px"
className={clsx(styles.slotGroupTopContainer, {
[styles.skipped]: skippedSlots.size > 0,
})}
>
<Flex
align="center"
gap="4px"
minWidth="0"
className={styles.slotGroupNameContainer}
>
<PeerIcon
url={peer?.info?.icon_url}
size={17}
isYou={isLeader}
hideTooltip
/>
<Text className={styles.name}>{name}</Text>
</Flex>
</Flex>

<Flex
height="3px"
position="relative"
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
30 changes: 25 additions & 5 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 All @@ -12,8 +13,11 @@ type ShredEventTsDeltaMs = number | undefined;
*/
export type ShredEventTsDeltas = ShredEventTsDeltaMs[];

type Slot = {
export type Slot = {
shreds: (ShredEventTsDeltas | undefined)[];
/**
* earliest event (start) of the slot
*/
minEventTsDelta?: number;
maxEventTsDelta?: number;
completionTsDelta?: number;
Expand Down Expand Up @@ -42,6 +46,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 +203,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 +221,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;
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;
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