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 @@
+
+
+
+
+
+
+
+
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 @@
+
@@ -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 @@
+
@@ -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 @@
+
@@ -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