From cf14701592137991bab8c5bf7d9c8fb613f53c02 Mon Sep 17 00:00:00 2001 From: you Date: Sat, 21 Mar 2026 19:59:11 +0000 Subject: [PATCH] perf: node paths endpoint uses disambiguateHops with prefix index Replaced inline resolveHopsInternal (allNodes.filter per hop) with shared disambiguateHops (prefix-indexed). Also uses _parsedPath cache and per-request disambig cache. /api/nodes/:pubkey/paths was 560ms cold, should now be much faster. --- server.js | 93 +++++-------------------------------------------------- 1 file changed, 7 insertions(+), 86 deletions(-) diff --git a/server.js b/server.js index 988f6a17..5f568306 100644 --- a/server.js +++ b/server.js @@ -2447,8 +2447,7 @@ app.get('/api/nodes/:pubkey/paths', (req, res) => { const matchingTx = []; for (const [, tx] of pktStore.byHash) { if (!tx.path_json) continue; - let hops; - try { hops = JSON.parse(tx.path_json); } catch { continue; } + const hops = tx._parsedPath || (tx.path_json ? (() => { try { return tx._parsedPath = JSON.parse(tx.path_json); } catch { return null; } })() : null); if (!Array.isArray(hops) || !hops.length) continue; const found = hops.some(h => { const hl = (typeof h === 'string' ? h : '').toLowerCase(); @@ -2457,96 +2456,18 @@ app.get('/api/nodes/:pubkey/paths', (req, res) => { if (found) matchingTx.push({ tx, hops }); } - // Resolve hops for each transmission using origin-anchored disambiguation - const dist = (lat1, lon1, lat2, lon2) => Math.sqrt((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2); - const MAX_HOP_DIST = MAX_HOP_DIST_SERVER; - - function resolveHopsInternal(hops, originLat, originLon, observerId) { - let observerLat = null, observerLon = null; - if (observerId) { - const obsNode = allNodes.find(n => n.name === observerId || n.public_key === observerId); - if (obsNode && obsNode.lat && obsNode.lon && !(obsNode.lat === 0 && obsNode.lon === 0)) { - observerLat = obsNode.lat; observerLon = obsNode.lon; - } - } - const resolved = {}; - for (const hop of hops) { - const hopLower = hop.toLowerCase(); - const candidates = allNodes.filter(n => n.public_key.toLowerCase().startsWith(hopLower)); - if (candidates.length === 0) resolved[hop] = { name: null, candidates: [] }; - else if (candidates.length === 1) resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, lat: candidates[0].lat, lon: candidates[0].lon }; - else resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, lat: candidates[0].lat, lon: candidates[0].lon, ambiguous: true, candidates: candidates.map(c => ({ name: c.name, pubkey: c.public_key, lat: c.lat, lon: c.lon })) }; - } - const hopPositions = {}; - for (const hop of hops) { - const r = resolved[hop]; - if (r && !r.ambiguous && r.pubkey) { - const nd = allNodes.find(n => n.public_key === r.pubkey); - if (nd && nd.lat && nd.lon && !(nd.lat === 0 && nd.lon === 0)) hopPositions[hop] = { lat: nd.lat, lon: nd.lon }; - } - } - // Forward pass - let lastPos = (originLat != null && originLon != null) ? { lat: originLat, lon: originLon } : null; - for (let i = 0; i < hops.length; i++) { - const hop = hops[i]; - if (hopPositions[hop]) { lastPos = hopPositions[hop]; continue; } - const r = resolved[hop]; if (!r || !r.ambiguous) continue; - const withLoc = r.candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0)); - if (!withLoc.length) continue; - let anchor = lastPos; - if (!anchor && i === hops.length - 1 && observerLat != null) anchor = { lat: observerLat, lon: observerLon }; - if (anchor) withLoc.sort((a, b) => dist(a.lat, a.lon, anchor.lat, anchor.lon) - dist(b.lat, b.lon, anchor.lat, anchor.lon)); - r.name = withLoc[0].name; r.pubkey = withLoc[0].pubkey; r.lat = withLoc[0].lat; r.lon = withLoc[0].lon; - hopPositions[hop] = { lat: withLoc[0].lat, lon: withLoc[0].lon }; lastPos = hopPositions[hop]; - } - // Backward pass - let nextPos = observerLat != null ? { lat: observerLat, lon: observerLon } : null; - for (let i = hops.length - 1; i >= 0; i--) { - const hop = hops[i]; - if (hopPositions[hop]) { nextPos = hopPositions[hop]; continue; } - const r = resolved[hop]; if (!r || !r.ambiguous) continue; - const withLoc = r.candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0)); - if (!withLoc.length || !nextPos) continue; - withLoc.sort((a, b) => dist(a.lat, a.lon, nextPos.lat, nextPos.lon) - dist(b.lat, b.lon, nextPos.lat, nextPos.lon)); - r.name = withLoc[0].name; r.pubkey = withLoc[0].pubkey; r.lat = withLoc[0].lat; r.lon = withLoc[0].lon; - hopPositions[hop] = { lat: withLoc[0].lat, lon: withLoc[0].lon }; nextPos = hopPositions[hop]; - } - // Sanity check - for (let i = 0; i < hops.length; i++) { - const pos = hopPositions[hops[i]]; if (!pos) continue; - const prev = i > 0 ? hopPositions[hops[i-1]] : null; - const next = i < hops.length-1 ? hopPositions[hops[i+1]] : null; - if (!prev && !next) continue; - const dPrev = prev ? dist(pos.lat, pos.lon, prev.lat, prev.lon) : 0; - const dNext = next ? dist(pos.lat, pos.lon, next.lat, next.lon) : 0; - const tooFarPrev = prev && dPrev > MAX_HOP_DIST; - const tooFarNext = next && dNext > MAX_HOP_DIST; - if ((tooFarPrev && tooFarNext) || (tooFarPrev && !next) || (tooFarNext && !prev)) { - const r = resolved[hops[i]]; if (r) r.unreliable = true; - delete hopPositions[hops[i]]; - } - } - return hops.map(h => ({ prefix: h, name: resolved[h]?.name || h, pubkey: resolved[h]?.pubkey || null, lat: resolved[h]?.lat || null, lon: resolved[h]?.lon || null })); - } + // Resolve and group paths using shared disambiguateHops (prefix-indexed) // Group by resolved path signature const pathGroups = {}; let totalTransmissions = 0; + const _pathsDisambigCache = {}; for (const { tx, hops } of matchingTx) { totalTransmissions++; - // Determine origin for disambiguation - let originLat = null, originLon = null; - if (tx.decoded_json) { - try { - const dec = typeof tx.decoded_json === 'string' ? JSON.parse(tx.decoded_json) : tx.decoded_json; - if (dec.lat && dec.lon) { originLat = dec.lat; originLon = dec.lon; } - else if (dec.pubKey) { - const src = allNodes.find(n => n.public_key === dec.pubKey); - if (src && src.lat && src.lon) { originLat = src.lat; originLon = src.lon; } - } - } catch {} - } - const resolvedHops = resolveHopsInternal(hops, originLat, originLon, tx.observer_id); + // Use disambiguateHops (has prefix index) instead of inline allNodes.filter per hop + const cacheKey = hops.join(','); + const resolved = _pathsDisambigCache[cacheKey] || (_pathsDisambigCache[cacheKey] = disambiguateHops(hops, allNodes)); + const resolvedHops = resolved.map(r => ({ prefix: r.hop, name: r.name, pubkey: r.pubkey || null, lat: r.lat || null, lon: r.lon || null })); const key = resolvedHops.map(h => h.pubkey || h.prefix).join('→'); if (!pathGroups[key]) pathGroups[key] = { hops: resolvedHops, count: 0, lastSeen: null, sampleHash: tx.hash }; pathGroups[key].count++;