mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 13:35:42 +00:00
Compare commits
6 Commits
fix/compos
...
fix/cache-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
499a6db2cc | ||
|
|
206d9bd64a | ||
|
|
ec35b291ee | ||
|
|
3f54632b07 | ||
|
|
609b12541e | ||
|
|
4369e58a3c |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- 'docs/**'
|
||||
|
||||
concurrency:
|
||||
group: deploy
|
||||
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
|
||||
@@ -33,6 +33,11 @@ type Server struct {
|
||||
memStatsMu sync.Mutex
|
||||
memStatsCache runtime.MemStats
|
||||
memStatsCachedAt time.Time
|
||||
|
||||
// Cached /api/stats response — recomputed at most once every 10s
|
||||
statsMu sync.Mutex
|
||||
statsCache *StatsResponse
|
||||
statsCachedAt time.Time
|
||||
}
|
||||
|
||||
// PerfStats tracks request performance.
|
||||
@@ -380,6 +385,17 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
const statsTTL = 10 * time.Second
|
||||
|
||||
s.statsMu.Lock()
|
||||
if s.statsCache != nil && time.Since(s.statsCachedAt) < statsTTL {
|
||||
cached := s.statsCache
|
||||
s.statsMu.Unlock()
|
||||
writeJSON(w, cached)
|
||||
return
|
||||
}
|
||||
s.statsMu.Unlock()
|
||||
|
||||
var stats *Stats
|
||||
var err error
|
||||
if s.store != nil {
|
||||
@@ -392,7 +408,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
counts := s.db.GetRoleCounts()
|
||||
writeJSON(w, StatsResponse{
|
||||
resp := &StatsResponse{
|
||||
TotalPackets: stats.TotalPackets,
|
||||
TotalTransmissions: &stats.TotalTransmissions,
|
||||
TotalObservations: stats.TotalObservations,
|
||||
@@ -411,7 +427,14 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
Companions: counts["companions"],
|
||||
Sensors: counts["sensors"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
s.statsMu.Lock()
|
||||
s.statsCache = resp
|
||||
s.statsCachedAt = time.Now()
|
||||
s.statsMu.Unlock()
|
||||
|
||||
writeJSON(w, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handlePerf(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -98,6 +98,11 @@ type PacketStore struct {
|
||||
// computed during Load() and incrementally updated on ingest.
|
||||
distHops []distHopRecord
|
||||
distPaths []distPathRecord
|
||||
|
||||
// Cached GetNodeHashSizeInfo result — recomputed at most once every 15s
|
||||
hashSizeInfoMu sync.Mutex
|
||||
hashSizeInfoCache map[string]*hashSizeNodeInfo
|
||||
hashSizeInfoAt time.Time
|
||||
}
|
||||
|
||||
// Precomputed distance records for fast analytics aggregation.
|
||||
@@ -3722,8 +3727,26 @@ type hashSizeNodeInfo struct {
|
||||
Inconsistent bool
|
||||
}
|
||||
|
||||
// GetNodeHashSizeInfo scans advert packets to compute per-node hash size data.
|
||||
// GetNodeHashSizeInfo returns cached per-node hash size data, recomputing at most every 15s.
|
||||
func (s *PacketStore) GetNodeHashSizeInfo() map[string]*hashSizeNodeInfo {
|
||||
const ttl = 15 * time.Second
|
||||
s.hashSizeInfoMu.Lock()
|
||||
if s.hashSizeInfoCache != nil && time.Since(s.hashSizeInfoAt) < ttl {
|
||||
cached := s.hashSizeInfoCache
|
||||
s.hashSizeInfoMu.Unlock()
|
||||
return cached
|
||||
}
|
||||
s.hashSizeInfoMu.Unlock()
|
||||
result := s.computeNodeHashSizeInfo()
|
||||
s.hashSizeInfoMu.Lock()
|
||||
s.hashSizeInfoCache = result
|
||||
s.hashSizeInfoAt = time.Now()
|
||||
s.hashSizeInfoMu.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
// computeNodeHashSizeInfo scans advert packets to compute per-node hash size data.
|
||||
func (s *PacketStore) computeNodeHashSizeInfo() map[string]*hashSizeNodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ services:
|
||||
image: corescope:latest
|
||||
container_name: corescope-prod
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "${PROD_HTTP_PORT:-80}:${PROD_HTTP_PORT:-80}"
|
||||
- "${PROD_HTTPS_PORT:-443}:${PROD_HTTPS_PORT:-443}"
|
||||
@@ -31,6 +33,8 @@ services:
|
||||
image: corescope:latest
|
||||
container_name: corescope-staging
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "${STAGING_HTTP_PORT:-81}:${STAGING_HTTP_PORT:-81}"
|
||||
- "${STAGING_MQTT_PORT:-1884}:1883"
|
||||
@@ -59,6 +63,8 @@ services:
|
||||
image: corescope-go:latest
|
||||
container_name: corescope-staging-go
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "${STAGING_GO_HTTP_PORT:-82}:80"
|
||||
- "${STAGING_GO_MQTT_PORT:-1885}:1883"
|
||||
|
||||
13
server.js
13
server.js
@@ -207,6 +207,13 @@ class TTLCache {
|
||||
if (key.startsWith(prefix)) this.store.delete(key);
|
||||
}
|
||||
}
|
||||
debouncedInvalidateBulkHealth() {
|
||||
if (this._bulkHealthTimer) return;
|
||||
this._bulkHealthTimer = setTimeout(() => {
|
||||
this._bulkHealthTimer = null;
|
||||
this.invalidate('bulk-health');
|
||||
}, 30000);
|
||||
}
|
||||
debouncedInvalidateAll() {
|
||||
if (this._debounceTimer) return;
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
@@ -410,7 +417,7 @@ app.get('/api/perf', (req, res) => {
|
||||
avgMs: perfStats.requests ? Math.round(perfStats.totalMs / perfStats.requests * 10) / 10 : 0,
|
||||
endpoints: Object.fromEntries(sorted),
|
||||
slowQueries: perfStats.slowQueries.slice(-20),
|
||||
cache: { size: cache.size, hits: cache.hits, misses: cache.misses, staleHits: cache.staleHits, recomputes: cache.recomputes, hitRate: cache.hits + cache.misses > 0 ? Math.round(cache.hits / (cache.hits + cache.misses) * 1000) / 10 : 0 },
|
||||
cache: { size: cache.size, hits: cache.hits, misses: cache.misses, staleHits: cache.staleHits, recomputes: cache.recomputes, hitRate: cache.hits + cache.staleHits + cache.misses > 0 ? Math.round((cache.hits + cache.staleHits) / (cache.hits + cache.staleHits + cache.misses) * 1000) / 10 : 0 },
|
||||
packetStore: pktStore.getStats(),
|
||||
sqlite: (() => {
|
||||
try {
|
||||
@@ -519,7 +526,7 @@ app.get('/api/health', (req, res) => {
|
||||
misses: cache.misses,
|
||||
staleHits: cache.staleHits,
|
||||
recomputes: cache.recomputes,
|
||||
hitRate: cache.hits + cache.misses > 0 ? Math.round(cache.hits / (cache.hits + cache.misses) * 1000) / 10 : 0,
|
||||
hitRate: cache.hits + cache.staleHits + cache.misses > 0 ? Math.round((cache.hits + cache.staleHits) / (cache.hits + cache.staleHits + cache.misses) * 1000) / 10 : 0,
|
||||
},
|
||||
websocket: {
|
||||
clients: wsClients,
|
||||
@@ -723,7 +730,7 @@ for (const source of mqttSources) {
|
||||
// Invalidate this node's caches on advert
|
||||
cache.invalidate('node:' + p.pubKey);
|
||||
cache.invalidate('health:' + p.pubKey);
|
||||
cache.invalidate('bulk-health');
|
||||
cache.debouncedInvalidateBulkHealth();
|
||||
|
||||
// Cross-reference: if this node's pubkey matches an existing observer, backfill observer name
|
||||
if (p.name && p.pubKey) {
|
||||
|
||||
@@ -1254,6 +1254,24 @@ seedTestData();
|
||||
lastPathSeenMap.delete(liveNode);
|
||||
});
|
||||
|
||||
// ── Cache hit rate includes stale hits ──
|
||||
await t('Cache hitRate includes staleHits in formula', async () => {
|
||||
cache.clear();
|
||||
cache.hits = 0;
|
||||
cache.misses = 0;
|
||||
cache.staleHits = 0;
|
||||
// Simulate: 3 hits, 2 stale hits, 5 misses => rate = (3+2)/(3+2+5) = 50%
|
||||
cache.hits = 3;
|
||||
cache.staleHits = 2;
|
||||
cache.misses = 5;
|
||||
const r = await request(app).get('/api/health').expect(200);
|
||||
assert(r.body.cache.hitRate === 50, 'hitRate should be (hits+staleHits)/(hits+staleHits+misses) = 50%, got ' + r.body.cache.hitRate);
|
||||
// Reset
|
||||
cache.hits = 0;
|
||||
cache.misses = 0;
|
||||
cache.staleHits = 0;
|
||||
});
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n═══ Server Route Tests: ${passed} passed, ${failed} failed ═══`);
|
||||
if (failed > 0) process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user