Relay firehose browser tools: https://compare.hose.cam

show some latencies in a histogram

+56 -37
package-lock.json
··· 8 8 "name": "firehose-diff", 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 + "@atcute/tid": "^1.0.2", 11 12 "@emotion/react": "^11.14.0", 12 13 "@emotion/styled": "^11.14.0", 13 14 "@mui/material": "^7.1.0", 14 - "@mui/x-charts": "^8.3.1", 15 + "@mui/x-charts": "^8.10.2", 15 16 "@skyware/firehose": "^0.5.1", 16 17 "react": "^19.1.0", 17 18 "react-dom": "^19.1.0" ··· 84 85 "dependencies": { 85 86 "@atcute/uint8array": "^1.0.1" 86 87 } 88 + }, 89 + "node_modules/@atcute/tid": { 90 + "version": "1.0.2", 91 + "resolved": "https://registry.npmjs.org/@atcute/tid/-/tid-1.0.2.tgz", 92 + "integrity": "sha512-ahmjroNyeDPJhtuf3+HTJropaH04HmJ8fhntDu73Gpz/RkAF7+nkz6kcP2QTgfvMCgMPAJUdskAAP82GPDTY9w==", 93 + "license": "MIT" 87 94 }, 88 95 "node_modules/@atcute/uint8array": { 89 96 "version": "1.0.1", ··· 316 323 } 317 324 }, 318 325 "node_modules/@babel/runtime": { 319 - "version": "7.27.1", 320 - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", 321 - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", 326 + "version": "7.28.3", 327 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", 328 + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", 322 329 "license": "MIT", 323 330 "engines": { 324 331 "node": ">=6.9.0" ··· 1403 1410 } 1404 1411 }, 1405 1412 "node_modules/@mui/types": { 1406 - "version": "7.4.2", 1407 - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", 1408 - "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", 1413 + "version": "7.4.5", 1414 + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz", 1415 + "integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==", 1409 1416 "license": "MIT", 1410 1417 "dependencies": { 1411 - "@babel/runtime": "^7.27.1" 1418 + "@babel/runtime": "^7.28.2" 1412 1419 }, 1413 1420 "peerDependencies": { 1414 1421 "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" ··· 1420 1427 } 1421 1428 }, 1422 1429 "node_modules/@mui/utils": { 1423 - "version": "7.1.0", 1424 - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", 1425 - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", 1430 + "version": "7.3.1", 1431 + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.1.tgz", 1432 + "integrity": "sha512-/31y4wZqVWa0jzMnzo6JPjxwP6xXy4P3+iLbosFg/mJQowL1KIou0LC+lquWW60FKVbKz5ZUWBg2H3jausa0pw==", 1426 1433 "license": "MIT", 1427 1434 "dependencies": { 1428 - "@babel/runtime": "^7.27.1", 1429 - "@mui/types": "^7.4.2", 1430 - "@types/prop-types": "^15.7.14", 1435 + "@babel/runtime": "^7.28.2", 1436 + "@mui/types": "^7.4.5", 1437 + "@types/prop-types": "^15.7.15", 1431 1438 "clsx": "^2.1.1", 1432 1439 "prop-types": "^15.8.1", 1433 - "react-is": "^19.1.0" 1440 + "react-is": "^19.1.1" 1434 1441 }, 1435 1442 "engines": { 1436 1443 "node": ">=14.0.0" ··· 1450 1457 } 1451 1458 }, 1452 1459 "node_modules/@mui/x-charts": { 1453 - "version": "8.3.1", 1454 - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.3.1.tgz", 1455 - "integrity": "sha512-jZClK40++ftcMwCeHKudGKmazd0MsgnrIP6RhYi2lH1kg0jK2upueokyxVIIxqquwWsQYE3WsflJBP61DvYXOQ==", 1460 + "version": "8.10.2", 1461 + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.10.2.tgz", 1462 + "integrity": "sha512-F4SdpixbaAeNaZHylYLpaj/WD1jKn6gMD1Twbd+Y8FzZneE2mrtXwaQc6W2Hbri/VdJl2OV55nL5pFMpDSlwAA==", 1456 1463 "license": "MIT", 1457 1464 "dependencies": { 1458 - "@babel/runtime": "^7.27.1", 1459 - "@mui/utils": "^7.0.2", 1460 - "@mui/x-charts-vendor": "8.3.1", 1461 - "@mui/x-internals": "8.3.1", 1465 + "@babel/runtime": "^7.28.2", 1466 + "@mui/utils": "^7.3.1", 1467 + "@mui/x-charts-vendor": "8.6.0", 1468 + "@mui/x-internal-gestures": "0.2.4", 1469 + "@mui/x-internals": "8.10.2", 1462 1470 "bezier-easing": "^2.1.0", 1463 1471 "clsx": "^2.1.1", 1464 1472 "prop-types": "^15.8.1", ··· 1486 1494 } 1487 1495 }, 1488 1496 "node_modules/@mui/x-charts-vendor": { 1489 - "version": "8.3.1", 1490 - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.3.1.tgz", 1491 - "integrity": "sha512-UcUa7HDIpSfeVBYgeHewWoVALcB4Gg9we53l78j2cyadYBZOWdtLj8fezo9zAhxfZ5s9T+1yIyuD+CCnYJnUpQ==", 1497 + "version": "8.6.0", 1498 + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.6.0.tgz", 1499 + "integrity": "sha512-TTtfhxXuwtoZfyno7+4y3ZhZeFqavFJecWbteLEby0lFqALWB9GGJpkc1TIHWr3GkWE5UHEbdADZ0pfrPenezA==", 1492 1500 "license": "MIT AND ISC", 1493 1501 "dependencies": { 1494 - "@babel/runtime": "^7.27.1", 1502 + "@babel/runtime": "^7.27.6", 1495 1503 "@types/d3-color": "^3.1.3", 1496 1504 "@types/d3-delaunay": "^6.0.4", 1497 1505 "@types/d3-interpolate": "^3.0.4", ··· 1510 1518 "robust-predicates": "^3.0.2" 1511 1519 } 1512 1520 }, 1521 + "node_modules/@mui/x-internal-gestures": { 1522 + "version": "0.2.4", 1523 + "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.2.4.tgz", 1524 + "integrity": "sha512-Hpc5+LQfT0TrI7O0ngBlZ1LD6Gp1h5DUNs4ABbrotY/2OsOfJOzltV/QtBjINTmiBzDPnrE8l8nEiG97qOWX3Q==", 1525 + "license": "MIT", 1526 + "dependencies": { 1527 + "@babel/runtime": "^7.28.2" 1528 + } 1529 + }, 1513 1530 "node_modules/@mui/x-internals": { 1514 - "version": "8.3.1", 1515 - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.3.1.tgz", 1516 - "integrity": "sha512-8kIxT66cea63iEseEIHSWzKju2Wzl7MsWFoAUQEyRvYqOFa2j9Un2Vn/EH2vy9nm/MtMAYpwOE/nt68/KTIA2w==", 1531 + "version": "8.10.2", 1532 + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.2.tgz", 1533 + "integrity": "sha512-dlC0BQRRBdiWtqn1yDppaHYRUjU3OuPWTxy0UtqxDaJjJf4pfR8ALr243nbxgJAFqvQyWPWyO4A6p9x9eJMJEQ==", 1517 1534 "license": "MIT", 1518 1535 "dependencies": { 1519 - "@babel/runtime": "^7.27.1", 1520 - "@mui/utils": "^7.0.2" 1536 + "@babel/runtime": "^7.28.2", 1537 + "@mui/utils": "^7.3.1", 1538 + "reselect": "^5.1.1", 1539 + "use-sync-external-store": "^1.5.0" 1521 1540 }, 1522 1541 "engines": { 1523 1542 "node": ">=14.0.0" ··· 1992 2011 "license": "MIT" 1993 2012 }, 1994 2013 "node_modules/@types/prop-types": { 1995 - "version": "15.7.14", 1996 - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", 1997 - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", 2014 + "version": "15.7.15", 2015 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", 2016 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", 1998 2017 "license": "MIT" 1999 2018 }, 2000 2019 "node_modules/@types/react": { ··· 4460 4479 } 4461 4480 }, 4462 4481 "node_modules/react-is": { 4463 - "version": "19.1.0", 4464 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", 4465 - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", 4482 + "version": "19.1.1", 4483 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", 4484 + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", 4466 4485 "license": "MIT" 4467 4486 }, 4468 4487 "node_modules/react-refresh": {
+2 -1
package.json
··· 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "@atcute/tid": "^1.0.2", 13 14 "@emotion/react": "^11.14.0", 14 15 "@emotion/styled": "^11.14.0", 15 16 "@mui/material": "^7.1.0", 16 - "@mui/x-charts": "^8.3.1", 17 + "@mui/x-charts": "^8.10.2", 17 18 "@skyware/firehose": "^0.5.1", 18 19 "react": "^19.1.0", 19 20 "react-dom": "^19.1.0"
+26 -1
src/App.tsx
··· 28 28 29 29 function App() { 30 30 const [relays, setRelays] = useState([] as string[]); 31 + const [events, setEvents] = useState(['commit', 'sync', 'account', 'identity', 'unknown']); 32 + const [eventsSelected, setEventsSelected] = useState(new Set(events)); 31 33 const [receiver, setReceiver] = useState(() => noopReceiver); 32 34 const [keepalive, setKeepalive] = useState(() => () => {}); 33 35 const [rateBars, setRateBars] = useState({ series: [] } as any); ··· 143 145 return ( 144 146 <> 145 147 <h1>compare hoses</h1> 146 - <p><em>warning: enabling many relay connections requires a lot of bandwidth</em></p> 148 + <p><em>warning: enabling many relay connections requires a <strong>lot</strong> of bandwidth</em></p> 147 149 148 150 <form style={{ display: 'block', textAlign: 'left' }}> 149 151 {knownRelays.map(({ url, desc }: Relay) => ( ··· 193 195 </p> 194 196 </form> 195 197 198 + <form style={{ display: 'block', margin: '1rem 0' }}> 199 + <span>events: </span> 200 + {events.map(eventName => ( 201 + <label key={eventName}> 202 + <input 203 + type='checkbox' 204 + checked={eventsSelected.has(eventName)} 205 + onChange={e => { 206 + setEventsSelected(evs => { 207 + const s = new Set(evs); 208 + if (e.target.checked) s.add(eventName); 209 + else s.delete(eventName); 210 + return s; 211 + }) 212 + }} 213 + /> 214 + {' '} 215 + {eventName} 216 + </label> 217 + ))} 218 + </form> 219 + 196 220 <div style={{ display: 'flex', flexWrap: 'wrap', gap: '2em', textAlign: 'left' }}> 197 221 {relays.map(url => { 198 222 const { desc } = knownRelays.find((r: Relay) => r.url === url) ?? { desc: "custom relay" }; ··· 201 225 <Relay 202 226 url={url} 203 227 desc={desc} 228 + includeEvents={eventsSelected} 204 229 onRecieveEvent={(type: string, event: any) => receiver(url, type, event)} 205 230 /> 206 231 </div>
+89 -3
src/Relay.tsx
··· 1 - import { useEffect, useState } from 'react'; 1 + import { useCallback, useEffect, useState } from 'react'; 2 2 import { Firehose } from '@skyware/firehose'; 3 + import * as TID from '@atcute/tid'; 4 + import { BarChart } from '@mui/x-charts/BarChart'; 3 5 import './Relay.css'; 4 6 5 7 type HoseState = 'connecting' | 'connected' | 'errored' | 'closed'; 6 8 7 - function Relay({ url, desc, onRecieveEvent }: { 9 + const TIME_SIGNAL_NSID = 'app.bsky.feed.like'; 10 + const BUCKET_WIDTH = 32; // ms 11 + const BUCKETS = 32; 12 + const MAX_BUCKET = BUCKET_WIDTH * BUCKETS; 13 + 14 + function Relay({ url, desc, includeEvents, onRecieveEvent }: { 8 15 url: string, 9 16 desc: string, 17 + includeEvents: Set, 10 18 onRecieveEvent: (type: string, event: any) => void, 11 19 }) { 12 20 const [state, setState] = useState('connecting' as HoseState); 13 21 const [commits, setCommits] = useState(0); 14 22 const [reconnects, setReconnects] = useState(0); 23 + const [buckets, setBuckets] = useState({ 24 + idx: Array.from({ length: BUCKETS + 2 }).map(() => 0), 25 + recv: Array.from({ length: BUCKETS + 2 }).map(() => 0), 26 + }); 15 27 16 28 useEffect(() => { 17 29 const sendIt = (type: string, event: any) => { 30 + if (!includeEvents.has(type)) return; 18 31 onRecieveEvent(type, event); 19 32 setCommits(n => n + 1); 33 + if (type === 'commit' && event.ops.length === 1) { 34 + const op = event.ops[0]; 35 + try { 36 + const [nsid, rkey] = op.path.split('/'); 37 + if (nsid === TIME_SIGNAL_NSID) { 38 + const posted = TID.parse(rkey).timestamp / 1000; 39 + const indexed = Date.parse(event.time) 40 + const indexed_dt = indexed - posted; 41 + const received_dt = +new Date() - indexed; 42 + 43 + let idx_bucket, recv_bucket; 44 + 45 + if (indexed_dt < 0) { 46 + idx_bucket = -1; 47 + } else if (indexed_dt >= MAX_BUCKET) { 48 + idx_bucket = BUCKETS; 49 + } else { 50 + idx_bucket = Math.min(Math.floor(indexed_dt / BUCKET_WIDTH), MAX_BUCKET) 51 + } 52 + if (received_dt < 0) { 53 + recv_bucket = -1; 54 + } else if (received_dt >= MAX_BUCKET) { 55 + recv_bucket = BUCKETS 56 + } else { 57 + recv_bucket = Math.min(Math.floor(received_dt / BUCKET_WIDTH), MAX_BUCKET) 58 + } 59 + 60 + setBuckets(({ idx, recv }) => { 61 + idx = idx.slice(); 62 + recv = recv.slice(); 63 + idx[idx_bucket + 1] += 1; 64 + recv[recv_bucket + 1] += 1; 65 + return { idx, recv }; 66 + }); 67 + } 68 + } catch (e) {} 69 + } 20 70 }; 21 71 const firehose = new Firehose({ relay: url }); 22 72 firehose.on('open', () => setState('connected')); ··· 38 88 return () => { 39 89 firehose.close(); 40 90 }; 41 - }, [url]); 91 + }, [url, includeEvents]); 42 92 43 93 return ( 44 94 <div className="relay"> ··· 48 98 {(reconnects > 0) && ( 49 99 <p>reconnects: <code>{reconnects}</code></p> 50 100 )} 101 + <BarChart 102 + height={180} 103 + width={420} 104 + yAxis={[{ 105 + label: 'events', 106 + scaleType: 'symlog', 107 + }]} 108 + skipAnimation={true} 109 + xAxis={[{ 110 + data: [-1] 111 + .concat(Array.from({ length: BUCKETS }).map((_, i) => i * BUCKET_WIDTH)) 112 + .concat(['+']), 113 + label: 'index latency (ms)', 114 + }]} 115 + series={[{ 116 + data: buckets.idx, 117 + }]} 118 + /> 119 + <BarChart 120 + height={180} 121 + width={420} 122 + yAxis={[{ 123 + label: 'events', 124 + scaleType: 'symlog', 125 + }]} 126 + skipAnimation={true} 127 + xAxis={[{ 128 + data: [-1] 129 + .concat(Array.from({ length: BUCKETS }).map((_, i) => i * BUCKET_WIDTH)) 130 + .concat(['+']), 131 + label: 'receive latency (ms)', 132 + }]} 133 + series={[{ 134 + data: buckets.recv, 135 + }]} 136 + /> 51 137 </div> 52 138 ); 53 139 }