From ecb34840ae4a6d3a69f0b69925d6d52066f3f2a0 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 21:07:30 -0500 Subject: [PATCH] perf(dashboard): UGC page stays fast with tens of thousands of creations The UGC page ran a CDClient query per distinct car or rocket combination to find each preset's sample (thousands on a large server), re-read the car and rocket build types from the CDClient on every list request, and grouped every build into assemblies again on every page, sort or filter change of the Cars and rockets list. The modules and icon kinds are now read once at startup on the main thread (UgcRoutes::Preload), so the web threads never query the CDClient (which they must not), a combination's kind comes from that table, and the grouped assemblies are shared by the requests of 15 seconds. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/DashboardServer.cpp | 1 + dDashboardServer/routes/UgcRoutes.cpp | 59 ++++++++++++++++++++------- dDashboardServer/routes/UgcRoutes.h | 4 ++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/dDashboardServer/DashboardServer.cpp b/dDashboardServer/DashboardServer.cpp index c7ad718cd..d234af947 100644 --- a/dDashboardServer/DashboardServer.cpp +++ b/dDashboardServer/DashboardServer.cpp @@ -443,6 +443,7 @@ int main(int argc, char** argv) { { const auto start = std::chrono::steady_clock::now(); ClientAssets::Preload(); + UgcRoutes::Preload(); PreloadZoneData(); Scenery::Preload(); WorldView::Preload(); diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 99aac051d..5d3017a2e 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -1,7 +1,9 @@ #include "UgcRoutes.h" #include +#include #include +#include #include #include "CDClientDatabase.h" @@ -70,7 +72,7 @@ namespace { // Web thread: the icon kinds: player models, and each car or rocket build type in the client's data // (ModularBuildComponent), named after the object its Assembly LOT is - nlohmann::json IconKinds() { + nlohmann::json LoadIconKinds() { nlohmann::json kinds = nlohmann::json::array({ { { "kind", UgcIconParams::ModelKind() }, { "label", "Player models" } } }); auto result = CDClientDatabase::ExecuteQuery("SELECT buildType, xml FROM ModularBuildComponent GROUP BY buildType ORDER BY buildType;"); while (!result.eof()) { @@ -92,17 +94,24 @@ namespace { return kinds; } - // Web thread: a car or rocket's kind, from its first module's build type (ModuleComponent) + // Client data read once by Preload (main thread), only read after that + nlohmann::json g_IconKinds = nlohmann::json::array(); + std::map g_Modules; + + // The icon kinds: player models, then each car or rocket build type with its name + const nlohmann::json& IconKinds() { return g_IconKinds; } + + // Every module in the client's data (ModuleComponent) with its build type and name + const std::map& Modules() { return g_Modules; } + + // A car or rocket's kind, from its first module's build type std::optional ModularKind(std::string modules) { std::replace(modules.begin(), modules.end(), '-', '+'); // a combination's key too const auto lots = UgcModularKey::Lots(modules); if (lots.empty()) return std::nullopt; - auto query = CDClientDatabase::CreatePreppedStmt("SELECT m.buildType FROM ComponentsRegistry cr JOIN ModuleComponent m ON m.id = cr.component_id " - "WHERE cr.id = ? AND cr.component_type = 28 LIMIT 1;"); - query.bind(1, static_cast(lots.front())); - auto row = query.execQuery(); - if (row.eof()) return std::nullopt; - return UgcIconParams::BuildKind(row.getIntField("buildType", 0)); + const auto it = Modules().find(lots.front()); + if (it == Modules().end() || it->second.buildType < 0) return std::nullopt; + return UgcIconParams::BuildKind(it->second.buildType); } // Web thread: each kind with something to edit its preset on: the newest made player model, the most used combination of each build type @@ -132,12 +141,9 @@ namespace { return kinds; } - // Web thread: every module in the client's data (ModuleComponent) with its build type and name, read once - const std::map& Modules() { - static std::map modules; - static bool loaded = false; - if (loaded) return modules; - loaded = true; + // Main thread (Preload): every module in the client's data (ModuleComponent) with its build type and name + std::map LoadModules() { + std::map modules; auto result = CDClientDatabase::ExecuteQuery("SELECT cr.id AS lot, m.buildType AS buildType, o.name AS name, o.displayName AS displayName FROM ComponentsRegistry cr " "JOIN ModuleComponent m ON m.id = cr.component_id LEFT JOIN Objects o ON o.id = cr.id WHERE cr.component_type = 28;"); while (!result.eof()) { @@ -194,6 +200,24 @@ namespace { { "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs }, { "processCpuMs", entry.processCpuMs }, { "processMemoryKb", entry.processMemoryKb }, { "modelName", entry.modelName } }; } + // Web thread: the cars and rockets grouped into assemblies (one per combination of modules), from every build. + // Grouping all builds is the slow part of the Cars and rockets list, so the result is kept for a short while and + // shared by the requests in that time (it follows new builds and makes within ASSEMBLY_CACHE_TIME). + constexpr auto ASSEMBLY_CACHE_TIME = std::chrono::seconds(15); + std::vector AllBuilds(const IUgcLookup::UgcSearch& search); + std::shared_ptr> CachedAssemblies() { + static std::mutex mutex; + static std::shared_ptr> cached; + static std::chrono::steady_clock::time_point at; + std::lock_guard lock(mutex); + const auto now = std::chrono::steady_clock::now(); + if (!cached || now - at > ASSEMBLY_CACHE_TIME) { + cached = std::make_shared>(UgcAssemblies::Group(AllBuilds({}), Modules())); + at = now; + } + return cached; + } + // Web thread: every car and rocket build (the assemblies are made from them) std::vector AllBuilds(const IUgcLookup::UgcSearch& search = {}) { IUgcLookup::UgcListQuery query; @@ -233,6 +257,11 @@ namespace { } namespace UgcRoutes { + void Preload() { + g_Modules = LoadModules(); + g_IconKinds = LoadIconKinds(); + } + void RegisterRoutes() { Route(eHTTPMethod::GET, "/ugc", Perm("properties_view"), "What the UGC server made of players' models", [](HTTPReply& reply, const HTTPContext& context) { RenderPage(reply, context, "ugc.jinja2", "ugc"); }); @@ -294,7 +323,7 @@ namespace UgcRoutes { if (parsed.search.number) filter.moduleLot = static_cast(*parsed.search.number); } } - auto assemblies = UgcAssemblies::Group(AllBuilds(), modules); + auto assemblies = *CachedAssemblies(); std::erase_if(assemblies, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); }); UgcAssemblies::Sort(assemblies, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules, reverse); total = assemblies.size(); diff --git a/dDashboardServer/routes/UgcRoutes.h b/dDashboardServer/routes/UgcRoutes.h index f3c805605..009fea99a 100644 --- a/dDashboardServer/routes/UgcRoutes.h +++ b/dDashboardServer/routes/UgcRoutes.h @@ -3,4 +3,8 @@ // The UGC page: what the UGC server has made of players' models and modular builds, and making them again namespace UgcRoutes { void RegisterRoutes(); + + // Reads the client data the UGC page needs (modules, build types) once, on the main thread at startup, so the web + // threads never query the CDClient + void Preload(); }