admin/monitoring: optimized for many connections

Massimo Melina committed May 6, 2026 at 10:25 UTC 2da20e99a6c964a7991beccfb9282f98756f83d1
3 files changed +27 -16
admin/src/MonitorPage.ts
+5 -4
@@ -36,7 +36,7 @@ export default function MonitorPage({ setTitleSide }: PageProps) {
36 function MoreInfo() {
37 const { data: status, element, reload } = useApiEx<typeof adminApis.get_status>('get_status')
38 useInterval(reload, 10_000) // status hardly change, but it can
39 - const { data: connections } = useApiEvents('get_connection_stats')
39 + const { data: stats } = useApiEvents('get_connection_stats')
40 const [allInfo, setAllInfo] = useState(false)
41 const md = useBreakpoint('md')
42 const sm = useBreakpoint('sm')
@@ -57,7 +57,7 @@ function MoreInfo() {
57 }),
58 pair('outSpeedKb', { label: "Output", render: formatSpeedK, minWidth: '8.5em' }),
59 pair('inSpeedKb', { label: "Input", render: formatSpeedK, minWidth: '8.5em' }),
60 - (allInfo || sm) && pair('ips', { label: "IPs", title: () => "Currently connected" }),
60 + (allInfo || sm) && pair('ips', { label: "IPs", title: () => stats && `${stats.connections.toLocaleString()} connections` }),
61 (md || allInfo && md || status?.http?.error) && pair('http', { label: "HTTP", render: port }),
62 (md || allInfo && md || status?.https?.error) && pair('https', { label: "HTTPS", render: port }),
63 (xl || allInfo) && pair('ram', { label: "RAM", render: formatBytes }),
@@ -81,7 +81,7 @@ function MoreInfo() {
81 }
82
83 function pair(k: string, { label, minWidth, render, title, onDelete }: PairOptions = {}) {
84 - let v = _.get(connections, k) ?? _.get(status, k)
84 + let v = _.get(stats, k) ?? _.get(status, k)
85 if (v === undefined)
86 return null
87 let color: Color = undefined
@@ -118,7 +118,7 @@ function Connections() {
118 const { monitorOnlyFiles } = useSnapState()
119 const { pause, pauseButton } = usePauseButton()
120 const rows = useMemo(() =>
121 - list?.filter((x: any) => !monitorOnlyFiles || x.op).map((x: any, id: number) => ({ id, ...x })),
121 + (!monitorOnlyFiles ? list : list?.filter((x: any) => x.op)) ?? [],
122 [!pause && list, monitorOnlyFiles]) //eslint-disable-line
123 const logAble = useBreakpoint('md')
124 const [wantLog, wantLogButton] = useToggleButton("Show log", "Hide log", v => ({
@@ -147,6 +147,7 @@ function Connections() {
147 persist: 'connections',
148 error,
149 rows,
150 + getRowId: (row: any) => row.ip + ':' + row.port,
151 fillFlex: true,
152 noRows: monitorOnlyFiles && "No downloads/uploads at the moment",
153 actionsHeader: pauseButton,
src/api.monitor.ts
+6 -12
@@ -9,6 +9,7 @@ import { totalGot, totalInSpeedKb, totalOutSpeedKb, totalSent } from './throttle
9 import { getCurrentUsername } from './auth'
10 import { SendListReadable } from './SendList'
11 import { storedMap } from './persistence'
12 +import { countUniqueBy } from './cross'
13
14 export default {
15
@@ -29,25 +30,22 @@ export default {
30 get_connections({}, ctx) {
31 const list = new SendListReadable({
32 diff: true,
32 - addAtStart: getConnections().map(c =>
33 - !ignore(c) && serializeConnection(c)).filter(Boolean),
33 + addAtStart: getConnections().map(serializeConnection),
34 })
35 type Change = Partial<Omit<Connection,'ip'>>
36 list.props({ you: ctx.ip })
37 return list.events(ctx, {
38 connection(conn: Connection) {
39 - if (ignore(conn)) return
39 list.add(serializeConnection(conn))
40 },
41 connectionClosed(conn: Connection) {
43 - if (ignore(conn)) return
42 list.remove(getConnAddress(conn))
43 },
44 connectionNewIp(conn: Connection, oldIp: string, newIp: string) {
45 list.update(getConnAddress(conn, oldIp), { ip: newIp })
46 },
47 connectionUpdated(conn: Connection, change: Change) {
50 - if (conn.socket.closed || ignore(conn) || ignore(change as any) || _.isEmpty(change)) return
48 + if (conn.socket.closed || _.isEmpty(change)) return
49 if (change.ctx) {
50 Object.assign(change, fromCtx(change.ctx))
51 change.ctx = undefined
@@ -59,13 +57,13 @@ export default {
57
58 async *get_connection_stats() {
59 while (1) {
62 - const filtered = getConnections().filter(x => !ignore(x))
60 + const connections = getConnections()
61 yield {
62 outSpeedKb: totalOutSpeedKb,
63 inSpeedKb: totalInSpeedKb,
64 sent_got: [totalSent.get(), totalGot.get(), totalGotSentResetTime.get()] as const,
67 - connections: filtered.length,
68 - ips: _.uniqBy(filtered, x => x.ip).length,
65 + connections: connections.length,
66 + ips: countUniqueBy(connections, conn => conn.ip),
67 }
68 await wait(1000)
69 }
@@ -80,10 +78,6 @@ export default {
78
79 } satisfies ApiHandlers
80
83 -function ignore(conn: Connection) {
84 - return false //conn.socket && isLocalHost(conn)
85 -}
86 -
81 export function serializeConnection(conn: Connection) {
82 const { socket, started, secure } = conn
83 return {
src/cross.ts
+16
@@ -198,6 +198,22 @@ export function onlyTruthy<T>(arr: T[]) {
198 return arr.filter(truthy)
199 }
200
201 +export function countUniqueBy<T, K>(items: Iterable<T>, keyFn: (item: T) => K, predicate?: (item: T) => boolean) {
202 + // use a Set so unique counting stays linear even on very large live lists
203 + const seen = new Set<K>()
204 + let count = 0
205 + for (const item of items) {
206 + if (predicate && !predicate(item))
207 + continue
208 + const key = keyFn(item)
209 + if (seen.has(key))
210 + continue
211 + seen.add(key)
212 + count++
213 + }
214 + return count
215 +}
216 +
217 export function setHidden<T, ADD>(dest: T, src: ADD) {
218 return Object.defineProperties(dest, newObj(src as any, value => ({
219 enumerable: false,