mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 12:00:32 +02:00
add more relay measures
This commit is contained in:
@@ -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<string, () => void>();
|
||||
private queue: NostrOutgoingMessage[] = [];
|
||||
private subscriptionStartTime = new Map<string, Date>();
|
||||
|
||||
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] });
|
||||
|
@@ -64,8 +64,9 @@ export const ConnectedRelays = () => {
|
||||
<Tr>
|
||||
<Th>Relay</Th>
|
||||
<Th isNumeric>Claims</Th>
|
||||
<Th isNumeric>Avg Connect</Th>
|
||||
<Th isNumeric>Avg Response</Th>
|
||||
<Th isNumeric>Disconnects</Th>
|
||||
<Th isNumeric>Avg Eject</Th>
|
||||
<Th isNumeric>Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -79,8 +80,9 @@ export const ConnectedRelays = () => {
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td isNumeric>{relayPoolService.getRelayClaims(url).size}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageResponseTime(url).toFixed(2)}ms</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getDisconnects(url)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageConnectionTime(url).toFixed(0)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageResponseTime(url).toFixed(0)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageEjectTime(url).toFixed(0)}</Td>
|
||||
<Td isNumeric>
|
||||
<RelayStatus url={url} />
|
||||
</Td>
|
||||
|
@@ -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]);
|
||||
}
|
||||
|
10
src/hooks/use-ranked-relay-configs.ts
Normal file
10
src/hooks/use-ranked-relay-configs.ts
Normal file
@@ -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("|")]);
|
||||
}
|
@@ -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;
|
||||
|
@@ -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<string>();
|
||||
private relayResponseTimes = new SuperMap<string, [number, Date][]>(() => []);
|
||||
private relayDisconnects = new SuperMap<string, Date[]>(() => []);
|
||||
relayResponseTimes = new SuperMap<string, TimeMeasure>((relay) => new TimeMeasure(relay));
|
||||
relayEjectTime = new SuperMap<string, TimeMeasure>((relay) => new TimeMeasure(relay));
|
||||
relayConnectionTime = new SuperMap<string, TimeMeasure>((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<string, number>(() => 0);
|
||||
const relayDisconnects = new SuperMap<string, number>(() => 0);
|
||||
const relays = customRelays ?? this.getRelays();
|
||||
const relayAverageResponseTime = new SuperMap<string, number>(() => 0);
|
||||
const relayAverageConnectionTime = new SuperMap<string, number>(() => 0);
|
||||
const relayAverageEjectTime = new SuperMap<string, number>(() => 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<string>();
|
||||
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;
|
||||
}
|
||||
|
@@ -81,8 +81,9 @@ export default function RelaysView() {
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Url</Th>
|
||||
<Th>Avg Response</Th>
|
||||
<Th>Disconnects</Th>
|
||||
<Th isNumeric>Avg Connect</Th>
|
||||
<Th isNumeric>Avg Response</Th>
|
||||
<Th isNumeric>Avg Eject</Th>
|
||||
<Th>Status</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
@@ -96,8 +97,9 @@ export default function RelaysView() {
|
||||
<Text>{relay.url}</Text>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{relayScoreboardService.getAverageResponseTime(relay.url).toFixed(2)}ms</Td>
|
||||
<Td>{relayScoreboardService.getDisconnects(relay.url)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageConnectionTime(relay.url).toFixed(0)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageResponseTime(relay.url).toFixed(0)}</Td>
|
||||
<Td isNumeric>{relayScoreboardService.getAverageEjectTime(relay.url).toFixed(0)}</Td>
|
||||
<Td>
|
||||
<RelayStatus url={relay.url} />
|
||||
</Td>
|
||||
|
Reference in New Issue
Block a user