diff --git a/approot/admin-profilercontroller.xml b/approot/admin-profilercontroller.xml new file mode 100644 index 00000000..0467e410 --- /dev/null +++ b/approot/admin-profilercontroller.xml @@ -0,0 +1,15 @@ + + + + +
+ ${tr:Lms.Admin.ProfilerController.profiler} +
+
+ ${export-btn class="btn btn-primary"} +
+
+
+
+ +
diff --git a/approot/main.xml b/approot/main.xml index 3facacf1..8d63c533 100644 --- a/approot/main.xml +++ b/approot/main.xml @@ -49,6 +49,7 @@
  • ${scan-settings class="dropdown-item"}
  • ${scanner class="dropdown-item"}
  • ${users class="dropdown-item"}
  • +
  • ${profiler class="dropdown-item"}
  • ${} diff --git a/approot/messages.xml b/approot/messages.xml index 77d99d7f..51441308 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -45,6 +45,7 @@ Libraries + Profiler Scan settings Scanner Users @@ -63,6 +64,10 @@ Path must be an existing directory Root directory + +Export profiling data +Profiler + Delimiter to be used for splitting artist tags Daily diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 3629fc1a..1ec396db 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -45,6 +45,7 @@ Bibliothèques + Profileur Paramètres du scan Scanner Utilisateurs @@ -63,6 +64,10 @@ Le chemin doit référencer un répertoire existant Répertoire racine + +Exporter les données de profilage +Profileur + Délimiteur à utiliser pour séparer les tags d'artistes Tous les jours diff --git a/approot/messages_it.xml b/approot/messages_it.xml index b16fba7e..a8f164b2 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -45,6 +45,7 @@ Librerie + Profilatore Impostazioni di scansione Scanner Utenti @@ -63,6 +64,10 @@ Il percorso deve essere una directory esistente Cartella principale + +Esporta dati di profilazione +Profilatore + Delimitatore da utilizzare per separare i tag degli artisti Giornaliera diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 2a06a2a1..00e2d059 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -49,6 +49,7 @@ + @@ -63,6 +64,10 @@ + + + + 每日 @@ -83,6 +88,7 @@ 更新开始时间 每周 + 无法获得音轨时间 无法解析文件 diff --git a/conf/lms.conf b/conf/lms.conf index e65b63bd..5823be3a 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -93,6 +93,13 @@ artist-image-file-names = ("artist"); # Playqueue max entry count playqueue-max-entry-count = 1000; +# Internal profiling, enable only if necessary as it brings some runtime overhead! +# Possible values are "disabled", "overview" or "detailed". +# If enabled, profiling information has to be dumped in the profiler view located in the admin menu +profiling-level = "disabled"; +# The profiling buffer size, in MBytes (min is 16) +profiling-buffer-size = 16; + # Set to true if you want to hide duplicate tracks scanner-skip-duplicate-mbid = false; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index e0b813e4..2154e399 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable(lms ui/admin/InitWizardView.cpp ui/admin/MediaLibrariesView.cpp ui/admin/MediaLibraryModal.cpp + ui/admin/ProfilerController.cpp ui/admin/ScannerController.cpp ui/admin/ScanSettingsView.cpp ui/admin/UserView.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 468c2c00..c03c8a5a 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -43,6 +43,7 @@ #include "utils/IChildProcessManager.hpp" #include "utils/IConfig.hpp" #include "utils/IOContextRunner.hpp" +#include "utils/IProfiler.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" #include "utils/WtLogger.hpp" @@ -75,6 +76,20 @@ namespace throw LmsException{ "Invalid config value for 'log-min-severity'" }; } + std::optional getProfilingLevel() + { + std::string_view profilingLevel{ Service::get()->getString("profiling-level", "disabled") }; + + if (profilingLevel == "disabled") + return std::nullopt; + else if (profilingLevel == "overview") + return profiling::Level::Overview; + else if (profilingLevel == "detailed") + return profiling::Level::Detailed; + + throw LmsException{ "Invalid config value for 'profiling-level'" }; + } + std::vector generateWtConfig(std::string execPath, Severity minSeverity) { std::vector args; @@ -233,6 +248,9 @@ int main(int argc, char* argv[]) Service config{ createConfig(configFilePath) }; const Severity minLogSeverity{getLogMinSeverity()}; Service logger{ std::make_unique(minLogSeverity) }; + std::optional> profiler; + if (const auto level{ getProfilingLevel() }) + profiler.emplace(profiling::createProfiler(level.value(), config->getULong("profiling-buffer-size", profiling::MinBufferSizeInMBytes))); // use system locale. libarchive relies on this to write filenames if (char* locale{ ::setlocale(LC_ALL, "") }) @@ -243,6 +261,7 @@ int main(int argc, char* argv[]) // Make sure the working directory exists std::filesystem::create_directories(config->getPath("working-dir")); std::filesystem::create_directories(config->getPath("working-dir") / "cache"); + std::filesystem::create_directories(config->getPath("working-dir") / "profiling"); // Construct WT configuration and get the argc/argv back const std::vector wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) }; diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 9c5be7a4..735341d5 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -38,15 +38,17 @@ #include "database/User.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/ILogger.hpp" +#include "utils/IProfiler.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" #include "admin/InitWizardView.hpp" #include "admin/MediaLibrariesView.hpp" +#include "admin/ProfilerController.hpp" +#include "admin/ScannerController.hpp" #include "admin/ScanSettingsView.hpp" #include "admin/UserView.hpp" #include "admin/UsersView.hpp" -#include "admin/ScannerController.hpp" #include "common/Template.hpp" #include "explore/Explore.hpp" #include "explore/Filters.hpp" @@ -77,6 +79,7 @@ namespace UserInterface res->use(appRoot + "admin-initwizard"); res->use(appRoot + "admin-medialibraries"); res->use(appRoot + "admin-medialibrary"); + res->use(appRoot + "admin-profilercontroller"); res->use(appRoot + "admin-scannercontroller"); res->use(appRoot + "admin-scansettings"); res->use(appRoot + "admin-user"); @@ -119,6 +122,7 @@ namespace UserInterface IdxAdminScanner, IdxAdminUsers, IdxAdminUser, + IdxAdminProfiler, }; void handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) @@ -146,6 +150,7 @@ namespace UserInterface { "/admin/scanner", IdxAdminScanner, true, Wt::WString::tr("Lms.Admin.ScannerController.scanner") }, { "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") }, { "/admin/user", IdxAdminUser, true, std::nullopt }, + { "/admin/profiler", IdxAdminProfiler, true, Wt::WString::tr("Lms.Admin.ProfilerController.profiler") }, }; LMS_LOG(UI, DEBUG, "Internal path changed to '" << wApp->internalPath() << "'"); @@ -264,6 +269,8 @@ namespace UserInterface void LmsApplication::init() { + LMS_SCOPED_PROFILE_OVERVIEW("UI", "ApplicationInit"); + setTheme(std::make_shared()); useStyleSheet("resources/font-awesome/css/font-awesome.min.css"); @@ -381,6 +388,8 @@ namespace UserInterface void LmsApplication::createHome() { + LMS_SCOPED_PROFILE_OVERVIEW("UI", "ApplicationCreateHome"); + _coverResource = std::make_shared(); declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); @@ -436,6 +445,11 @@ namespace UserInterface navbar->bindNew("scan-settings", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scan-settings" }, Wt::WString::tr("Lms.Admin.menu-scan-settings")); navbar->bindNew("scanner", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scanner" }, Wt::WString::tr("Lms.Admin.menu-scanner")); navbar->bindNew("users", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/users" }, Wt::WString::tr("Lms.Admin.menu-users")); + // Hide the entry if the profiler is not enabled + if (Service<::profiling::IProfiler>::get()) + navbar->bindNew("profiler", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/profiler" }, Wt::WString::tr("Lms.Admin.menu-profiler")); + else + navbar->bindEmpty("profiler"); } // Contents @@ -467,6 +481,7 @@ namespace UserInterface mainStack->addNew(); mainStack->addNew(); mainStack->addNew(); + mainStack->addNew(); } explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity()); @@ -544,6 +559,7 @@ namespace UserInterface { try { + LMS_SCOPED_PROFILE_OVERVIEW("UI", "ProcessEvent"); WApplication::notify(event); } catch (LmsApplicationException& e) diff --git a/src/lms/ui/admin/ProfilerController.cpp b/src/lms/ui/admin/ProfilerController.cpp new file mode 100644 index 00000000..4de5d1a4 --- /dev/null +++ b/src/lms/ui/admin/ProfilerController.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ProfilerController.hpp" + +#include +#include +#include +#include + +#include "utils/IProfiler.hpp" +#include "utils/String.hpp" + +namespace UserInterface +{ + namespace + { + class ReportResource : public Wt::WResource + { + public: + ReportResource(::profiling::IProfiler& profiler) + : _profiler{ profiler } + { + } + + ~ReportResource() + { + beingDeleted(); + } + + void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) + { + suggestFileName(StringUtils::toISO8601String(Wt::WDateTime::currentDateTime()) + "-profiling.json"); + response.setMimeType("application/json"); + _profiler.dumpCurrentBuffer(response.out()); + } + + private: + profiling::IProfiler& _profiler; + }; + } + + ProfilerController::ProfilerController() + : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.ProfilerController.template") } + { + addFunction("tr", &Wt::WTemplate::Functions::tr); + + Wt::WPushButton* dumpBtn{ bindNew("export-btn", Wt::WString::tr("Lms.Admin.ProfilerController.export-current-buffer")) }; + + if (auto profiler{ Service::get() }) + { + Wt::WLink link{ std::make_shared(*profiler) }; + link.setTarget(Wt::LinkTarget::NewWindow); + dumpBtn->setLink(link); + } + else + dumpBtn->setEnabled(false); + } +} // namespace UserInterface diff --git a/src/lms/ui/admin/ProfilerController.hpp b/src/lms/ui/admin/ProfilerController.hpp new file mode 100644 index 00000000..22a92b8c --- /dev/null +++ b/src/lms/ui/admin/ProfilerController.hpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface +{ + class ProfilerController : public Wt::WTemplate + { + public: + ProfilerController(); + }; +} // namespace UserInterface