From 469844de3a5d18351f8b29e1e24005ef15612b9f Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Mon, 6 Mar 2023 08:09:41 -0600 Subject: [PATCH] add more relay measures --- src/classes/relay.ts | 38 ++++-- src/components/connected-relays.tsx | 8 +- src/hooks/use-merged-user-relays.tsx | 8 +- src/hooks/use-ranked-relay-configs.ts | 10 ++ src/services/db/schema.ts | 7 +- src/services/relay-scoreboard.ts | 189 +++++++++++++++++--------- src/views/relays/index.tsx | 10 +- 7 files changed, 182 insertions(+), 88 deletions(-) create mode 100644 src/hooks/use-ranked-relay-configs.ts diff --git a/src/classes/relay.ts b/src/classes/relay.ts index 421066e69..5a0f45628 100644 --- a/src/classes/relay.ts +++ b/src/classes/relay.ts @@ -47,9 +47,11 @@ export class Relay { ws?: WebSocket; mode: RelayMode = RelayMode.ALL; + private connectionTimer?: () => void; + private ejectTimer?: () => void; private intentionalClose = false; + private subscriptionResTimer = new Map void>(); private queue: NostrOutgoingMessage[] = []; - private subscriptionStartTime = new Map(); constructor(url: string, mode: RelayMode = RelayMode.ALL) { this.url = url; @@ -61,9 +63,16 @@ export class Relay { this.intentionalClose = false; this.ws = new WebSocket(this.url); + this.connectionTimer = relayScoreboardService.relayConnectionTime.get(this.url).createTimer(); this.ws.onopen = () => { this.onOpen.next(this); + this.ejectTimer = relayScoreboardService.relayEjectTime.get(this.url).createTimer(); + if (this.connectionTimer) { + this.connectionTimer(); + this.connectionTimer = undefined; + } + this.sendQueued(); if (import.meta.env.DEV) { @@ -73,7 +82,10 @@ export class Relay { this.ws.onclose = () => { this.onClose.next(this); - if (!this.intentionalClose) relayScoreboardService.submitDisconnect(this.url); + if (!this.intentionalClose && this.ejectTimer) { + this.ejectTimer(); + this.ejectTimer = undefined; + } if (import.meta.env.DEV) { console.info(`Relay: ${this.url} disconnected`); @@ -88,7 +100,7 @@ export class Relay { // record start time if (json[0] === "REQ") { - this.subStartMeasure(json[1]); + this.startSubResTimer(json[1]); } } else this.queue.push(json); } @@ -96,17 +108,17 @@ export class Relay { close() { this.ws?.close(); this.intentionalClose = true; - this.subscriptionStartTime.clear(); + this.subscriptionResTimer.clear(); } - private subStartMeasure(sub: string) { - this.subscriptionStartTime.set(sub, new Date()); + private startSubResTimer(sub: string) { + this.subscriptionResTimer.set(sub, relayScoreboardService.relayResponseTimes.get(this.url).createTimer()); } - private subEndMeasure(sub: string) { - const date = this.subscriptionStartTime.get(sub); - if (date) { - relayScoreboardService.submitResponseTime(this.url, new Date().valueOf() - date.valueOf()); - this.subscriptionStartTime.delete(sub); + private endSubResTimer(sub: string) { + const endTimer = this.subscriptionResTimer.get(sub); + if (endTimer) { + endTimer(); + this.subscriptionResTimer.delete(sub); } } @@ -154,14 +166,14 @@ export class Relay { switch (type) { case "EVENT": this.onEvent.next({ relay: this, type, subId: data[1], body: data[2] }); - this.subEndMeasure(data[1]); + this.endSubResTimer(data[1]); break; case "NOTICE": this.onNotice.next({ relay: this, type, message: data[1] }); break; case "EOSE": this.onEOSE.next({ relay: this, type, subId: data[1] }); - this.subEndMeasure(data[1]); + this.endSubResTimer(data[1]); break; case "OK": this.onCommandResult.next({ relay: this, type, eventId: data[1], status: data[2], message: data[3] }); diff --git a/src/components/connected-relays.tsx b/src/components/connected-relays.tsx index 306847c09..c5d5cb081 100644 --- a/src/components/connected-relays.tsx +++ b/src/components/connected-relays.tsx @@ -64,8 +64,9 @@ export const ConnectedRelays = () => { Relay Claims + Avg Connect Avg Response - Disconnects + Avg Eject Status @@ -79,8 +80,9 @@ export const ConnectedRelays = () => { {relayPoolService.getRelayClaims(url).size} - {relayScoreboardService.getAverageResponseTime(url).toFixed(2)}ms - {relayScoreboardService.getDisconnects(url)} + {relayScoreboardService.getAverageConnectionTime(url).toFixed(0)} + {relayScoreboardService.getAverageResponseTime(url).toFixed(0)} + {relayScoreboardService.getAverageEjectTime(url).toFixed(0)} diff --git a/src/hooks/use-merged-user-relays.tsx b/src/hooks/use-merged-user-relays.tsx index 209b8b8bc..dc953ca18 100644 --- a/src/hooks/use-merged-user-relays.tsx +++ b/src/hooks/use-merged-user-relays.tsx @@ -1,7 +1,6 @@ import { useMemo } from "react"; -import { RelayConfig, RelayMode } from "../classes/relay"; +import { RelayConfig } from "../classes/relay"; import { normalizeRelayConfigs } from "../helpers/relay"; -import relayScoreboardService from "../services/relay-scoreboard"; import { useUserContacts } from "./use-user-contacts"; import { useUserRelays } from "./use-user-relays"; @@ -18,9 +17,6 @@ export default function useMergedUserRelays(pubkey: string) { } // normalize relay urls and remove bad ones - const normalized = normalizeRelayConfigs(relays); - - const rankedUrls = relayScoreboardService.getRankedRelays(normalized.map((r) => r.url)); - return rankedUrls.map((u) => normalized.find((r) => r.url === u) as RelayConfig); + return normalizeRelayConfigs(relays); }, [userRelays, contacts]); } diff --git a/src/hooks/use-ranked-relay-configs.ts b/src/hooks/use-ranked-relay-configs.ts new file mode 100644 index 000000000..e9e3e1764 --- /dev/null +++ b/src/hooks/use-ranked-relay-configs.ts @@ -0,0 +1,10 @@ +import { useMemo } from "react"; +import { RelayConfig } from "../classes/relay"; +import relayScoreboardService from "../services/relay-scoreboard"; + +export default function useRankedRelayConfigs(relays: RelayConfig[]) { + return useMemo(() => { + const rankedUrls = relayScoreboardService.getRankedRelays(relays.map((r) => r.url)); + return rankedUrls.map((u) => relays.find((r) => r.url === u) as RelayConfig); + }, [relays.join("|")]); +} diff --git a/src/services/db/schema.ts b/src/services/db/schema.ts index 819a0b849..b39062fe1 100644 --- a/src/services/db/schema.ts +++ b/src/services/db/schema.ts @@ -32,7 +32,12 @@ export interface CustomSchema extends DBSchema { relayInfo: { key: string; value: RelayInformationDocument }; relayScoreboardStats: { key: string; - value: { relay: string; responseTimes: [number, Date][]; disconnects: Date[] }; + value: { + relay: string; + responseTimes?: [number, Date][]; + ejectTimes?: [number, Date][]; + connectionTimes?: [number, Date][]; + }; }; settings: { key: string; diff --git a/src/services/relay-scoreboard.ts b/src/services/relay-scoreboard.ts index 48d20981c..fc59146da 100644 --- a/src/services/relay-scoreboard.ts +++ b/src/services/relay-scoreboard.ts @@ -2,95 +2,162 @@ import moment from "moment"; import { SuperMap } from "../classes/super-map"; import db from "./db"; +interface PersistentMeasure { + load(data: any): this; + save(): any; +} +interface RelayMeasure { + relay: string; + prune(cutOff: Date): this; +} + +class IncidentMeasure implements RelayMeasure, PersistentMeasure { + relay: string; + private incidents: Date[] = []; + + constructor(relay: string) { + this.relay = relay; + } + + addIncident(date: Date = new Date()) { + this.incidents.unshift(date); + } + + prune(cutOff: Date): this { + while (true) { + const last = this.incidents.pop(); + if (!last) break; + if (last >= cutOff) { + this.incidents.push(last); + break; + } + } + return this; + } + + load(data: any) { + if (!Array.isArray(data)) return this; + this.incidents = data.sort(); + return this; + } + save() { + return this.incidents; + } +} + +class TimeMeasure implements RelayMeasure, PersistentMeasure { + relay: string; + private measures: [number, Date][] = []; + + constructor(relay: string) { + this.relay = relay; + } + + createTimer() { + const start = new Date(); + return () => this.addTime(new Date().valueOf() - start.valueOf()); + } + addTime(time: number, date: Date = new Date()) { + this.measures.unshift([time, date]); + } + getAverage(since?: Date, undef: number = Infinity) { + const points = since ? this.measures.filter((m) => m[1] > since) : this.measures; + if (points.length === 0) return Infinity; + const total = points.reduce((total, [time]) => total + time, 0); + return total / points.length; + } + + prune(cutOff: Date): this { + while (true) { + const last = this.measures.pop(); + if (!last) break; + if (last[1] >= cutOff) { + this.measures.push(last); + break; + } + } + return this; + } + + load(data: any) { + if (!Array.isArray(data)) return this; + this.measures = data; + return this; + } + save() { + return this.measures; + } +} + class RelayScoreboardService { - private relays = new Set(); - private relayResponseTimes = new SuperMap(() => []); - private relayDisconnects = new SuperMap(() => []); + relayResponseTimes = new SuperMap((relay) => new TimeMeasure(relay)); + relayEjectTime = new SuperMap((relay) => new TimeMeasure(relay)); + relayConnectionTime = new SuperMap((relay) => new TimeMeasure(relay)); - submitResponseTime(relay: string, time: number) { - this.relays.add(relay); - const arr = this.relayResponseTimes.get(relay); - arr.unshift([time, new Date()]); - } - submitDisconnect(relay: string) { - this.relays.add(relay); - const arr = this.relayDisconnects.get(relay); - arr.unshift(new Date()); - } - - pruneResponseTimes() { + prune() { const cutOff = moment().subtract(1, "week").toDate(); - for (const [relay, arr] of this.relayResponseTimes) { - while (true) { - const lastResponse = arr.pop(); - if (!lastResponse) break; - if (lastResponse[1] >= cutOff) { - arr.push(lastResponse); - break; - } - } - } - } - pruneResponseDisconnects() { - const cutOff = moment().subtract(1, "week").toDate(); - for (const [relay, arr] of this.relayDisconnects) { - while (true) { - const lastDisconnect = arr.pop(); - if (!lastDisconnect) break; - if (lastDisconnect >= cutOff) { - arr.push(lastDisconnect); - break; - } - } - } + 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); } - getAverageResponseTime(relay: string) { - const times = this.relayResponseTimes.get(relay); - if (times.length === 0) return Infinity; - const total = times.reduce((total, [time]) => total + time, 0); - return total / times.length; + getAverageResponseTime(relay: string, since?: Date) { + return this.relayResponseTimes.get(relay).getAverage(since); } - getDisconnects(relay: string) { - return this.relayDisconnects.get(relay).length; + getAverageEjectTime(relay: string, since?: Date) { + return this.relayEjectTime.get(relay).getAverage(since); + } + getAverageConnectionTime(relay: string, since?: Date) { + return this.relayConnectionTime.get(relay).getAverage(since); } getRankedRelays(customRelays?: string[]) { - const relays = customRelays ?? Array.from(this.relays); - const relayAverageResponseTimes = new SuperMap(() => 0); - const relayDisconnects = new SuperMap(() => 0); + const relays = customRelays ?? this.getRelays(); + const relayAverageResponseTime = new SuperMap(() => 0); + const relayAverageConnectionTime = new SuperMap(() => 0); + const relayAverageEjectTime = new SuperMap(() => 0); for (const relay of relays) { - const averageResponseTime = this.getAverageResponseTime(relay); - const disconnectTimes = this.relayDisconnects.get(relay).length; - relayAverageResponseTimes.set(relay, averageResponseTime); - relayDisconnects.set(relay, disconnectTimes); + relayAverageResponseTime.set(relay, this.relayResponseTimes.get(relay).getAverage()); + relayAverageConnectionTime.set(relay, this.relayConnectionTime.get(relay).getAverage()); + relayAverageEjectTime.set(relay, this.relayEjectTime.get(relay).getAverage()); } return relays.sort((a, b) => { let diff = 0; - diff += Math.sign(relayAverageResponseTimes.get(a) - relayAverageResponseTimes.get(b)); - diff += Math.sign(relayDisconnects.get(a) - relayDisconnects.get(b)); + 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; }); } + private getRelays() { + const relays = new Set(); + 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); + return Array.from(relays); + } + async loadStats() { const stats = await db.getAll("relayScoreboardStats"); for (const relayStats of stats) { - this.relays.add(relayStats.relay); - this.relayResponseTimes.set(relayStats.relay, relayStats.responseTimes); - this.relayDisconnects.set(relayStats.relay, relayStats.disconnects); + this.relayResponseTimes.get(relayStats.relay).load(relayStats.responseTimes); + this.relayEjectTime.get(relayStats.relay).load(relayStats.ejectTimes); + this.relayConnectionTime.get(relayStats.relay).load(relayStats.connectionTimes); } } async saveStats() { const transaction = db.transaction("relayScoreboardStats", "readwrite"); - for (const relay of this.relays) { - const responseTimes = this.relayResponseTimes.get(relay); - const disconnects = this.relayDisconnects.get(relay); - transaction.store.put({ relay, responseTimes, disconnects }); + const relays = this.getRelays(); + for (const relay of relays) { + const responseTimes = this.relayResponseTimes.get(relay).save(); + const ejectTimes = this.relayEjectTime.get(relay).save(); + const connectionTimes = this.relayConnectionTime.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 7027170a6..7be52effe 100644 --- a/src/views/relays/index.tsx +++ b/src/views/relays/index.tsx @@ -81,8 +81,9 @@ export default function RelaysView() { Url - Avg Response - Disconnects + Avg Connect + Avg Response + Avg Eject Status @@ -96,8 +97,9 @@ export default function RelaysView() { {relay.url} - {relayScoreboardService.getAverageResponseTime(relay.url).toFixed(2)}ms - {relayScoreboardService.getDisconnects(relay.url)} + {relayScoreboardService.getAverageConnectionTime(relay.url).toFixed(0)} + {relayScoreboardService.getAverageResponseTime(relay.url).toFixed(0)} + {relayScoreboardService.getAverageEjectTime(relay.url).toFixed(0)}