Compare commits

..

2 Commits

Author SHA1 Message Date
CyberMind
a025701a69
Merge b582019a12 into 4f8eb711f3 2026-06-25 13:48:03 +00:00
b582019a12 perf(sidebar): kill per-module health storm, use batch endpoint (#738)
The navbar refreshed LEDs by firing ONE /api/v1/<module>/health request per
module — ~119 requests every 30s, in batches of 8 — straight at the aggregator's
single shared event loop. Combined with the in-process module mount this is a
prime driver of the recurring board-wide 502 wedge (user-identified).

checkAllHealth + refreshStaleHealth now call /api/v1/hub/public/health-batch
ONCE (served by the dedicated, double-buffered hub process) and populate every
module's LED from that single response. 119 reqs/cycle -> 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:47:58 +02:00

View File

@ -1711,28 +1711,33 @@
// If already ok/warn/error, keep that status until new result arrives // If already ok/warn/error, keep that status until new result arrives
}); });
for (var i = 0; i < modules.length; i += BATCH_SIZE) { // #740: ONE batch call instead of one /api/v1/<mod>/health per module.
var batch = modules.slice(i, i + BATCH_SIZE); // The per-module loop fired ~119 requests every cycle and hammered the
var results = await Promise.allSettled(batch.map(function(mod) { // aggregator's single shared event loop (board-wide 502s). The hub batch
return checkModuleHealth(mod); // endpoint returns every module's status in a single hit.
})); await applyHealthBatch(modules);
results.forEach(function(result, idx) {
var mod = batch[idx];
if (result.status === 'fulfilled') {
healthCache[mod] = result.value;
} else {
healthCache[mod] = { status: 'error', msg: 'Check failed', timestamp: Date.now() };
}
});
// Update LEDs after each batch
updateAllLEDs();
}
return healthCache; return healthCache;
} }
// Populate healthCache for `modules` from the single hub batch endpoint
// (/api/v1/hub/public/health-batch — served by the dedicated hub process,
// double-buffered). Replaces the per-module health storm.
async function applyHealthBatch(modules) {
var token = localStorage.getItem('sbx_token');
var headers = token ? { 'Authorization': 'Bearer ' + token } : {};
var res = await safeFetch(BATCH_HEALTH_API, { headers: headers }, 5000);
if (!res.ok || !res.data || !res.data.modules) return;
var mods = res.data.modules;
modules.forEach(function(mod) {
var hb = mods[mod] || mods[mod.replace(/_/g, '-')] || mods[mod.replace(/-/g, '_')];
healthCache[mod] = hb
? { status: hb.status, msg: hb.msg || '', timestamp: Date.now() }
: { status: 'unknown', msg: 'no health API', timestamp: Date.now() };
});
updateAllLEDs();
try { savePreCache(healthCache); } catch (e) {}
}
async function refreshStaleHealth() { async function refreshStaleHealth() {
var now = Date.now(); var now = Date.now();
var stale = []; var stale = [];
@ -1744,17 +1749,9 @@
if (stale.length === 0) return; if (stale.length === 0) return;
// Incremental update - update each LED as result comes in // #740: refresh stale modules via the single batch endpoint, not one
for (var i = 0; i < stale.length; i += BATCH_SIZE) { // /health request per module.
var batch = stale.slice(i, i + BATCH_SIZE); await applyHealthBatch(stale);
await Promise.allSettled(batch.map(async function(mod) {
var result = await checkModuleHealth(mod);
healthCache[mod] = result;
// Update single LED immediately
var item = document.querySelector('.nav-item[data-module="' + mod + '"]');
if (item) updateSingleLED(item, result);
}));
}
// Save cache once at end, no re-sort needed // Save cache once at end, no re-sort needed
savePreCache(healthCache); savePreCache(healthCache);