diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 5d3017a2e..0e9d81c3e 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -7,6 +7,7 @@ #include #include "CDClientDatabase.h" +#include "ServerState.h" #include "Database.h" #include "UgcIconParams.h" #include "NifFile.h" @@ -433,6 +434,29 @@ namespace UgcRoutes { }, WorkerPool::ePriority::URGENT); }); + Route(eHTTPMethod::GET, "/api/diagnostics/ugc", Perm("health_view"), + "The UGC server for the Diagnostics page: {enabled, online, status (its /status: queue, workers, CPU, memory, limits, " + "throttling, recent makes) or error, counts: {model, modular} per state}", + [](HTTPReply& reply, const HTTPContext& context) { + bool enabled = false, online = false; + { + std::lock_guard lock(ServerState::g_StatusMutex); + enabled = ServerState::g_UgcEnabled; + online = ServerState::g_UgcStatus.online; + } + const auto base = InternalUrl(); + Workers::Reply(reply, context, false, [base, enabled, online](HTTPReply& out) { + nlohmann::json body = { { "success", true }, { "enabled", enabled }, { "online", online }, + { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } } }; + if (enabled) { + const auto fetched = CachedGet(base + "/status", eCache::STATUS); + if (fetched->status == 200) body["status"] = nlohmann::json::parse(fetched->body, nullptr, false); + else body["error"] = fetched->status == 0 ? fetched->error : "answered " + std::to_string(fetched->status); + } + JsonReply(out, eHTTPStatusCode::OK, body); + }, WorkerPool::ePriority::URGENT); + }); + Route(eHTTPMethod::GET, "/api/ugc/files/:kind/:id/:file", Perm("properties_view"), "A file the UGC server made (fetched from ugc_internal_url): icon.png, model.nif, model.noao.nif (before the lighting bake), " "stats.json, or previous. (the version before it was made again). 408 while it's being made", diff --git a/dDashboardServer/templates/diagnostics.jinja2 b/dDashboardServer/templates/diagnostics.jinja2 index 88c13b6d3..90fc636af 100644 --- a/dDashboardServer/templates/diagnostics.jinja2 +++ b/dDashboardServer/templates/diagnostics.jinja2 @@ -61,6 +61,7 @@
+
UGC server
UGC page
Asking the UGC server…
Servers (averages over the range; click to filter)
Busiest message types
@@ -81,6 +82,42 @@ var direction = 'in'; var data = null; + // The UGC server (when it's enabled): its work, what it uses, and what it has made + function loadUgc() { + api.get('/api/diagnostics/ugc').then(function (d) { + var card = document.getElementById('ugcCard'), body = document.getElementById('ugcBody'); + card.classList.toggle('d-none', !d.enabled); + if (!d.enabled) return; + var counts = function (title, c) { + return '
' + esc(title) + ': ' + fmt.badge(nf.format(c.done || 0) + ' made', 'success') + ' ' + fmt.badge(nf.format(c.pending || 0) + ' waiting', 'secondary') + ' ' + + fmt.badge(nf.format(c.failed || 0) + ' failed', c.failed ? 'danger' : 'secondary') + (c.empty ? ' ' + fmt.badge(nf.format(c.empty) + ' empty', 'light') : '') + '
'; + }; + var html = counts('Models', d.counts.model) + counts('Cars and rockets', d.counts.modular); + var s = d.status; + if (!s) { + body.innerHTML = fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '
' + html; + return; + } + var u = s.usage || {}, l = s.limits || {}, cores = u.cores || 1; + var row = function (k, v) { return '' + esc(k) + '' + v + ''; }; + html = '
' + fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused', 'info') : '') + '
' + + '' + + row('Work', esc(s.active) + ' of ' + esc(s.workers) + ' workers busy, ' + esc(nf.format(s.queued)) + ' queued') + + row('CPU', esc(((u.cpuPercent || 0) / cores).toFixed(0)) + '% of ' + esc(cores) + ' cores' + (l.maxCpus ? ' (limit ' + esc((l.maxCpus / cores * 100).toFixed(0)) + '%)' : '')) + + row('Memory', esc(bytes(u.residentBytes || 0)) + ' in use; jobs ~' + esc(bytes(s.jobMemoryBytes || 0)) + (l.maxMemoryBytes ? ' of ' + esc(bytes(l.maxMemoryBytes)) : '') + (s.memoryWaits ? ', ' + esc(s.memoryWaits) + ' waits' : '')) + + row('Files', esc(bytes(s.storedBytes || 0)) + (s.maxStorageBytes ? ' of ' + esc(bytes(s.maxStorageBytes)) : '') + (s.evicted ? ', ' + esc(s.evicted) + ' evicted' : '')) + + row('Since start', esc(nf.format(s.made || 0)) + ' made, ' + esc(nf.format(s.failed || 0)) + ' failed attempts') + + '
' + html; + var recent = (s.recent || []).slice(0, 5); + if (recent.length) html += '
Last: ' + recent.map(function (r) { + return esc(r.kind) + ' ' + esc(r.id) + (r.ok ? ' ' + esc(ms(r.ms)) : ' failed'); + }).join(' · ') + '
'; + body.innerHTML = html; + }).catch(function () {}); + } + loadUgc(); + setInterval(loadUgc, 10000); + function bytes(v) { if (v >= 1073741824) return (v / 1073741824).toFixed(2) + ' GB'; if (v >= 1048576) return (v / 1048576).toFixed(1) + ' MB';