From 548de3904007c14bd8a3beb9033b2856ff173b78 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 7 Mar 2023 08:48:29 -0600 Subject: [PATCH] add relay scores --- src/classes/relay.ts | 20 ++++ src/components/connected-relays.tsx | 19 ++- src/components/relay-score-breakdown.tsx | 43 +++++++ src/components/zap-modal.tsx | 4 +- ...elays.tsx => use-fallback-user-relays.tsx} | 6 +- src/services/db/schema.ts | 1 + src/services/relay-pool.ts | 12 +- src/services/relay-scoreboard.ts | 110 +++++++++++++++--- src/views/relays/index.tsx | 12 +- src/views/user/components/header.tsx | 4 +- src/views/user/notes.tsx | 12 +- src/views/user/relays.tsx | 13 ++- 12 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 src/components/relay-score-breakdown.tsx rename src/hooks/{use-merged-user-relays.tsx => use-fallback-user-relays.tsx} (74%) diff --git a/src/classes/relay.ts b/src/classes/relay.ts index 5a0f45628..3c2b2358b 100644 --- a/src/classes/relay.ts +++ b/src/classes/relay.ts @@ -36,6 +36,8 @@ export enum RelayMode { } export type RelayConfig = { url: string; mode: RelayMode }; +const CONNECTION_TIMEOUT = 1000 * 30; + export class Relay { url: string; onOpen = new Subject(); @@ -64,7 +66,25 @@ export class Relay { this.ws = new WebSocket(this.url); this.connectionTimer = relayScoreboardService.relayConnectionTime.get(this.url).createTimer(); + const connectionTimeout: number = window.setTimeout(() => { + // end the connection timer after CONNECTION_TIMEOUT + if (this.connectionTimer) { + this.connectionTimer(); + this.connectionTimer = undefined; + } + // relayScoreboardService.relayTimeouts.get(this.url).addIncident(); + }, CONNECTION_TIMEOUT); + + // for local dev, cancel timeout if module reloads + if (import.meta.hot) { + import.meta.hot.prune(() => { + window.clearTimeout(connectionTimeout); + this.ws?.close(); + }); + } + this.ws.onopen = () => { + window.clearTimeout(connectionTimeout); this.onOpen.next(this); this.ejectTimer = relayScoreboardService.relayEjectTime.get(this.url).createTimer(); diff --git a/src/components/connected-relays.tsx b/src/components/connected-relays.tsx index c5d5cb081..d7413b1b6 100644 --- a/src/components/connected-relays.tsx +++ b/src/components/connected-relays.tsx @@ -26,6 +26,7 @@ import { RelayIcon } from "./icons"; import { Relay } from "../classes/relay"; import { RelayFavicon } from "./relay-favicon"; import relayScoreboardService from "../services/relay-scoreboard"; +import { RelayScoreBreakdown } from "./relay-score-breakdown"; export const ConnectedRelays = () => { const isMobile = useIsMobile(); @@ -63,11 +64,9 @@ export const ConnectedRelays = () => { Relay - Claims - Avg Connect - Avg Response - Avg Eject - Status + Claims + Score + Status @@ -79,11 +78,11 @@ export const ConnectedRelays = () => { {url} - {relayPoolService.getRelayClaims(url).size} - {relayScoreboardService.getAverageConnectionTime(url).toFixed(0)} - {relayScoreboardService.getAverageResponseTime(url).toFixed(0)} - {relayScoreboardService.getAverageEjectTime(url).toFixed(0)} - + {relayPoolService.getRelayClaims(url).size} + + + + diff --git a/src/components/relay-score-breakdown.tsx b/src/components/relay-score-breakdown.tsx new file mode 100644 index 000000000..a3a3692c1 --- /dev/null +++ b/src/components/relay-score-breakdown.tsx @@ -0,0 +1,43 @@ +import { + Button, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverTrigger, + Text, +} from "@chakra-ui/react"; +import relayScoreboardService from "../services/relay-scoreboard"; + +export function RelayScoreBreakdown({ relay }: { relay: string }) { + return ( + + + + + + + + + + Avg Connect: {relayScoreboardService.getAverageConnectionTime(relay).toFixed(0)} ( + {relayScoreboardService.getConnectionTimeScore(relay)} pt) + + + Avg Response: {relayScoreboardService.getAverageResponseTime(relay).toFixed(0)} ( + {relayScoreboardService.getResponseTimeScore(relay)} pt) + + + Avg Eject: {relayScoreboardService.getAverageEjectTime(relay).toFixed(0)} ( + {relayScoreboardService.getEjectTimeScore(relay)} pt) + + {/* + Timeouts: {relayScoreboardService.getTimeoutCount(relay).toFixed(0)} ( + {relayScoreboardService.getTimeoutsScore(relay)} pt) + */} + + + + ); +} diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx index 5e3716248..225cae25b 100644 --- a/src/components/zap-modal.tsx +++ b/src/components/zap-modal.tsx @@ -155,7 +155,9 @@ export default function ZapModal({ document.removeEventListener("visibilitychange", listener); } }; - document.addEventListener("visibilitychange", listener); + setTimeout(() => { + document.addEventListener("visibilitychange", listener); + }, 1000 * 2); }; const handleClose = () => { diff --git a/src/hooks/use-merged-user-relays.tsx b/src/hooks/use-fallback-user-relays.tsx similarity index 74% rename from src/hooks/use-merged-user-relays.tsx rename to src/hooks/use-fallback-user-relays.tsx index dc953ca18..d1e368605 100644 --- a/src/hooks/use-merged-user-relays.tsx +++ b/src/hooks/use-fallback-user-relays.tsx @@ -4,9 +4,9 @@ import { normalizeRelayConfigs } from "../helpers/relay"; import { useUserContacts } from "./use-user-contacts"; import { useUserRelays } from "./use-user-relays"; -export default function useMergedUserRelays(pubkey: string) { - const contacts = useUserContacts(pubkey); - const userRelays = useUserRelays(pubkey); +export default function useFallbackUserRelays(pubkey: string, alwaysFetch = false) { + const contacts = useUserContacts(pubkey, [], alwaysFetch); + const userRelays = useUserRelays(pubkey, [], alwaysFetch); return useMemo(() => { let relays: RelayConfig[] = userRelays?.relays ?? []; diff --git a/src/services/db/schema.ts b/src/services/db/schema.ts index b39062fe1..809880b45 100644 --- a/src/services/db/schema.ts +++ b/src/services/db/schema.ts @@ -37,6 +37,7 @@ export interface CustomSchema extends DBSchema { responseTimes?: [number, Date][]; ejectTimes?: [number, Date][]; connectionTimes?: [number, Date][]; + timeouts?: Date[]; }; }; settings: { diff --git a/src/services/relay-pool.ts b/src/services/relay-pool.ts index 9de6e0ce8..fd0ea2671 100644 --- a/src/services/relay-pool.ts +++ b/src/services/relay-pool.ts @@ -67,10 +67,18 @@ export class RelayPoolService { const relayPoolService = new RelayPoolService(); setInterval(() => { - relayPoolService.reconnectRelays(); - relayPoolService.pruneRelays(); + if (document.visibilityState === "visible") { + relayPoolService.reconnectRelays(); + // relayPoolService.pruneRelays(); + } }, 1000 * 15); +document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") { + relayPoolService.reconnectRelays(); + } +}); + if (import.meta.env.DEV) { // @ts-ignore window.relayPoolService = relayPoolService; diff --git a/src/services/relay-scoreboard.ts b/src/services/relay-scoreboard.ts index fc59146da..57a1dd46a 100644 --- a/src/services/relay-scoreboard.ts +++ b/src/services/relay-scoreboard.ts @@ -2,12 +2,17 @@ import moment from "moment"; import { SuperMap } from "../classes/super-map"; import db from "./db"; +function clamp(v: number, min: number, max: number) { + return Math.min(Math.max(v, min), max); +} + interface PersistentMeasure { load(data: any): this; save(): any; } interface RelayMeasure { relay: string; + reset(): this; prune(cutOff: Date): this; } @@ -22,7 +27,15 @@ class IncidentMeasure implements RelayMeasure, PersistentMeasure { addIncident(date: Date = new Date()) { this.incidents.unshift(date); } + getCount(since?: Date) { + const points = since ? this.incidents.filter((d) => d > since) : this.incidents; + return points.length; + } + reset() { + this.incidents = []; + return this; + } prune(cutOff: Date): this { while (true) { const last = this.incidents.pop(); @@ -60,13 +73,21 @@ class TimeMeasure implements RelayMeasure, PersistentMeasure { addTime(time: number, date: Date = new Date()) { this.measures.unshift([time, date]); } + getCount(since?: Date){ + const points = since ? this.measures.filter((m) => m[1] > since) : this.measures; + return points.length + } getAverage(since?: Date, undef: number = Infinity) { const points = since ? this.measures.filter((m) => m[1] > since) : this.measures; - if (points.length === 0) return Infinity; + if (points.length === 0) return undef; const total = points.reduce((total, [time]) => total + time, 0); return total / points.length; } + reset() { + this.measures = []; + return this; + } prune(cutOff: Date): this { while (true) { const last = this.measures.pop(); @@ -90,15 +111,21 @@ class TimeMeasure implements RelayMeasure, PersistentMeasure { } class RelayScoreboardService { + /** the time it takes for relays to respond to queries */ relayResponseTimes = new SuperMap((relay) => new TimeMeasure(relay)); + /** the time it takes before the relay closes the connection */ relayEjectTime = new SuperMap((relay) => new TimeMeasure(relay)); + /** the time it takes to connect to the relay */ relayConnectionTime = new SuperMap((relay) => new TimeMeasure(relay)); + /** the number of times the connection has timed out */ + // relayTimeouts = new SuperMap((relay) => new IncidentMeasure(relay)); prune() { const cutOff = moment().subtract(1, "week").toDate(); for (const [relay, measure] of this.relayResponseTimes) measure.prune(cutOff); for (const [relay, measure] of this.relayEjectTime) measure.prune(cutOff); for (const [relay, measure] of this.relayConnectionTime) measure.prune(cutOff); + // for (const [relay, measure] of this.relayTimeouts) measure.prune(cutOff); } getAverageResponseTime(relay: string, since?: Date) { @@ -110,26 +137,71 @@ class RelayScoreboardService { getAverageConnectionTime(relay: string, since?: Date) { return this.relayConnectionTime.get(relay).getAverage(since); } + // getTimeoutCount(relay: string, since?: Date) { + // return this.relayTimeouts.get(relay).getCount(since); + // } + + hasConnected(relay: string, since?: Date){ + return this.relayConnectionTime.get(relay).getCount(since)>0; + } + getResponseTimeScore(relay: string, since?: Date) { + const responseTime = this.getAverageResponseTime(relay, since); + const connected = this.hasConnected(relay, since); + + // no points if we have never connected + if (!connected) return 0; + + // 1 point (max 10) for ever 10 ms under 1000. negative points for over 1000 + return clamp(Math.round(-(responseTime - 1000) / 100), -10, 10); + } + getConnectionTimeScore(relay: string, since?: Date) { + const connectionTime = this.getAverageConnectionTime(relay, since); + + // no points if we have never connected + if (connectionTime === Infinity) return 0; + + // 1 point (max 10) for ever 10 ms under 1000. negative points for over 1000 + return clamp(Math.round(-(connectionTime - 1000) / 100), -10, 10); + } + getEjectTimeScore(relay: string, since?: Date) { + const ejectTime = this.getAverageEjectTime(relay, since); + const connected = this.hasConnected(relay, since); + + // no points if we have never connected + if (!connected) return 0; + + let score = 0; + if (ejectTime > 1000 * 20) score += 1; + if (ejectTime > 1000 * 60) score += 4; + if (ejectTime > 1000 * 120) score += 5; + if (ejectTime > 1000 * 200) score += 5; + return score; + } + // getTimeoutsScore(relay: string, since?: Date) { + // const timeouts = this.getTimeoutCount(relay, since); + // // subtract 5 points for ever time its timed out + // return -(timeouts * 5); + // } + getRelayScore(relay: string, since?: Date) { + let score = 0; + + score += this.getResponseTimeScore(relay, since); + score += this.getConnectionTimeScore(relay, since); + score += this.getEjectTimeScore(relay, since); + // score += this.getTimeoutsScore(relay, since); + + return score; + } getRankedRelays(customRelays?: string[]) { const relays = customRelays ?? this.getRelays(); - const relayAverageResponseTime = new SuperMap(() => 0); - const relayAverageConnectionTime = new SuperMap(() => 0); - const relayAverageEjectTime = new SuperMap(() => 0); + const relayScores = new Map(); for (const relay of relays) { - relayAverageResponseTime.set(relay, this.relayResponseTimes.get(relay).getAverage()); - relayAverageConnectionTime.set(relay, this.relayConnectionTime.get(relay).getAverage()); - relayAverageEjectTime.set(relay, this.relayEjectTime.get(relay).getAverage()); + relayScores.set(relay, this.getRelayScore(relay)); } - return relays.sort((a, b) => { - let diff = 0; - diff += Math.sign(relayAverageResponseTime.get(a) - relayAverageResponseTime.get(b)); - diff += Math.sign(relayAverageConnectionTime.get(a) - relayAverageConnectionTime.get(b)) / 2; - diff += Math.sign(relayAverageEjectTime.get(b) - relayAverageEjectTime.get(a)); - return diff; - }); + return relays.sort((a, b) => (relayScores.get(b) ?? 0) - (relayScores.get(a) ?? 0)); } private getRelays() { @@ -137,9 +209,17 @@ class RelayScoreboardService { for (const [relay, measure] of this.relayResponseTimes) relays.add(relay); for (const [relay, measure] of this.relayEjectTime) relays.add(relay); for (const [relay, measure] of this.relayConnectionTime) relays.add(relay); + // for (const [relay, measure] of this.relayTimeouts) relays.add(relay); return Array.from(relays); } + resetScores() { + this.relayResponseTimes.forEach((m) => m.reset()); + this.relayEjectTime.forEach((m) => m.reset()); + this.relayConnectionTime.forEach((m) => m.reset()); + // this.relayTimeouts.forEach((m) => m.reset()); + } + async loadStats() { const stats = await db.getAll("relayScoreboardStats"); @@ -147,6 +227,7 @@ class RelayScoreboardService { this.relayResponseTimes.get(relayStats.relay).load(relayStats.responseTimes); this.relayEjectTime.get(relayStats.relay).load(relayStats.ejectTimes); this.relayConnectionTime.get(relayStats.relay).load(relayStats.connectionTimes); + // this.relayTimeouts.get(relayStats.relay).load(relayStats.timeouts); } } @@ -157,6 +238,7 @@ class RelayScoreboardService { const responseTimes = this.relayResponseTimes.get(relay).save(); const ejectTimes = this.relayEjectTime.get(relay).save(); const connectionTimes = this.relayConnectionTime.get(relay).save(); + // const timeouts = this.relayTimeouts.get(relay).save(); transaction.store.put({ relay, responseTimes, ejectTimes, connectionTimes }); } await transaction.done; diff --git a/src/views/relays/index.tsx b/src/views/relays/index.tsx index 7be52effe..f00af9167 100644 --- a/src/views/relays/index.tsx +++ b/src/views/relays/index.tsx @@ -24,8 +24,8 @@ import { useList } from "react-use"; import { RelayUrlInput } from "../../components/relay-url-input"; import useSubject from "../../hooks/use-subject"; import { RelayStatus } from "../../components/relay-status"; -import relayScoreboardService from "../../services/relay-scoreboard"; import { normalizeRelayUrl } from "../../helpers/url"; +import { RelayScoreBreakdown } from "../../components/relay-score-breakdown"; export default function RelaysView() { const relays = useSubject(clientRelaysService.relays); @@ -81,9 +81,7 @@ export default function RelaysView() { Url - Avg Connect - Avg Response - Avg Eject + Score Status @@ -97,9 +95,9 @@ export default function RelaysView() { {relay.url} - {relayScoreboardService.getAverageConnectionTime(relay.url).toFixed(0)} - {relayScoreboardService.getAverageResponseTime(relay.url).toFixed(0)} - {relayScoreboardService.getAverageEjectTime(relay.url).toFixed(0)} + + + diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 4e8a7929d..4b56c78d4 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -15,13 +15,13 @@ import { truncatedId } from "../../../helpers/nostr-event"; import { fixWebsiteUrl, getUserDisplayName } from "../../../helpers/user-metadata"; import { useCurrentAccount } from "../../../hooks/use-current-account"; import { useIsMobile } from "../../../hooks/use-is-mobile"; -import useMergedUserRelays from "../../../hooks/use-merged-user-relays"; +import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays"; import { useUserMetadata } from "../../../hooks/use-user-metadata"; import relayScoreboardService from "../../../services/relay-scoreboard"; import { UserProfileMenu } from "./user-profile-menu"; function useUserShareLink(pubkey: string) { - const userRelays = useMergedUserRelays(pubkey); + const userRelays = useFallbackUserRelays(pubkey); return useMemo(() => { const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url); diff --git a/src/views/user/notes.tsx b/src/views/user/notes.tsx index 103c7ddc6..1c9cf5859 100644 --- a/src/views/user/notes.tsx +++ b/src/views/user/notes.tsx @@ -22,17 +22,15 @@ import { RelayMode } from "../../classes/relay"; import { RelayIcon } from "../../components/icons"; import { Note } from "../../components/note"; import { isNote } from "../../helpers/nostr-event"; -import useMergedUserRelays from "../../hooks/use-merged-user-relays"; +import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; +import useRankedRelayConfigs from "../../hooks/use-ranked-relay-configs"; import { useTimelineLoader } from "../../hooks/use-timeline-loader"; const UserNotesTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; - const userRelays = useMergedUserRelays(pubkey); - const relays = userRelays - .filter((r) => r.mode & RelayMode.WRITE) - .map((r) => r.url) - .filter(Boolean) - .slice(0, 4) as string[]; + const userRelays = useFallbackUserRelays(pubkey).filter((r) => r.mode & RelayMode.WRITE); + const ranked = useRankedRelayConfigs(userRelays); + const relays = ranked.map((r) => r.url).slice(0, 4) as string[]; const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure(); diff --git a/src/views/user/relays.tsx b/src/views/user/relays.tsx index 09f05ce19..f21cd5a63 100644 --- a/src/views/user/relays.tsx +++ b/src/views/user/relays.tsx @@ -1,21 +1,24 @@ import { Text, Box, IconButton, Flex, Badge } from "@chakra-ui/react"; import { useNavigate, useOutletContext } from "react-router-dom"; import { GlobalIcon } from "../../components/icons"; -import relayScoreboardService from "../../services/relay-scoreboard"; import { RelayMode } from "../../classes/relay"; -import useMergedUserRelays from "../../hooks/use-merged-user-relays"; +import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; +import { RelayScoreBreakdown } from "../../components/relay-score-breakdown"; +import useRankedRelayConfigs from "../../hooks/use-ranked-relay-configs"; const UserRelaysTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; - const userRelays = useMergedUserRelays(pubkey); + const userRelays = useFallbackUserRelays(pubkey); const navigate = useNavigate(); + const ranked = useRankedRelayConfigs(userRelays); + return ( - {userRelays.map((relayConfig) => ( + {ranked.map((relayConfig) => ( {relayConfig.url} - {relayScoreboardService.getAverageResponseTime(relayConfig.url).toFixed(2)}ms + {relayConfig.mode & RelayMode.WRITE ? Write : null} {relayConfig.mode & RelayMode.READ ? Read : null}