add more relay measures

This commit is contained in:
hzrd149
2023-03-06 08:09:41 -06:00
parent ce3e61b48a
commit 469844de3a
7 changed files with 182 additions and 88 deletions

View File

@@ -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] });

View File

@@ -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>

View File

@@ -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]);
}

View 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("|")]);
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>