diff --git a/approot/admin-db.xml b/approot/admin-db.xml new file mode 100644 index 00000000..ca7246ac --- /dev/null +++ b/approot/admin-db.xml @@ -0,0 +1,14 @@ + + + + +
+
+
+ ${export-query-plans-btn class="btn btn-primary"} +
+
+
+
+ +
diff --git a/approot/admin-debugtools.xml b/approot/admin-debugtools.xml new file mode 100644 index 00000000..83025d9f --- /dev/null +++ b/approot/admin-debugtools.xml @@ -0,0 +1,15 @@ + + + + +
+ ${tr:Lms.Admin.DebugTools.Tracing.tracing} + ${tracing} +
+ ${tr:Lms.Admin.DebugTools.Db.db} + ${db} +
+ +
+ +
diff --git a/approot/admin-tracing.xml b/approot/admin-tracing.xml index eeb1ad43..5f5376a2 100644 --- a/approot/admin-tracing.xml +++ b/approot/admin-tracing.xml @@ -1,9 +1,8 @@ - +
- ${tr:Lms.Admin.Tracing.tracing}
${export-btn class="btn btn-primary"} diff --git a/approot/main.xml b/approot/main.xml index 0dea95e8..d9c0fcd4 100644 --- a/approot/main.xml +++ b/approot/main.xml @@ -44,7 +44,10 @@
  • ${scan-settings class="dropdown-item"}
  • ${scanner class="dropdown-item"}
  • ${users class="dropdown-item"}
  • -
  • ${tracing class="dropdown-item"}
  • + ${} + +
  • ${debug-tools class="dropdown-item"}
  • + ${
    } ${} diff --git a/approot/messages.xml b/approot/messages.xml index dd550f6e..070c40f2 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -52,10 +52,10 @@ User not found + Debug tools Libraries Scan settings Scanner - Tracing Users @@ -149,9 +149,16 @@ Step status Updating library fields: {1} entries + +Debug tools + + +Export query plans +Database + -Export traces -Tracing +Export traces +Tracing New user diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 9d90a2f6..0bce3785 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -52,10 +52,10 @@ No existe el usuario + Herramientas de depuración Bibliotecas Opciones de escaneo Escanear - Traces Usuarios @@ -148,9 +148,16 @@ Estado de las etapas Actualizando campos de la biblioteca: {1} entradas + +Herramientas de depuración + + +Exportar planes de consulta +Base de datos + -Exportar las trazas -Trazas +Exportar las trazas +Trazas Añadir diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index b7c94d7a..c8c654b0 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -52,10 +52,10 @@ L'utilisateur n'existe pas + Outils de débogage Bibliothèques Paramètres du scan Scanner - Traces Utilisateurs @@ -148,9 +148,16 @@ Statut de l'étape Mise à jour des champs des bibliothèques: {1} entrées + +Outils de débogage + + +Exporter les plans de requête +Base de données + -Exporter les traces -Traces +Exporter les traces +Traces Ajouter diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 30894331..42a157a5 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -52,10 +52,10 @@ Utente non trovato + Strumenti di debug Librerie Impostazioni di scansione Scanner - Tracing Utenti @@ -148,9 +148,16 @@ Stato passo Aggiornamento dei campi della libreria: {1} voci + +Strumenti di debug + + +Esporta i piani di query +Database + -Esporta tracce -Tracing +Esporta tracce +Tracing Nuovo utente diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 5bd2eed0..5adcdb87 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -53,10 +53,10 @@ Użytkownik nie odnaleziony + Narzędzia debugowania Biblioteki Ustawienia skanowania Skaner - Śledzenie Użytkownicy @@ -165,9 +165,16 @@ Obecny krok Aktualizowanie pól biblioteki: {1} wpisów + +Narzędzia debugowania + + +Eksportuj plany zapytań +Baza danych + -Eksportuj ślady -Śledzenie +Eksportuj ślady +Śledzenie Nowy użytkownik diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index ce6bbebb..6ca9201f 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -145,6 +145,12 @@ 当前步骤状态 + + + + + + diff --git a/conf/lms.conf b/conf/lms.conf index 0ae9ebea..4636f82e 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -17,7 +17,11 @@ log-min-severity = "info"; # Can be "none", "quick", or "full" db-integrity-check = "quick"; # Output db queries on stdout +# Use this only for debugging purpose, as this may impact performance db-show-queries = false; +# Record query plans for database queries. +# Use this only for debugging purposes, as this may impact performance +db-record-query-plans = false; # Listen port/addr of the web server listen-port = 5082; diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 614c6cde..6c91c368 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -28,7 +28,6 @@ extern "C" } #include -#include #include #include "core/ILogger.hpp" diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 8efc57ca..96af316e 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -32,6 +32,7 @@ add_library(lmsdatabase STATIC impl/IdType.cpp impl/Migration.cpp impl/Object.cpp + impl/QueryPlanRecorder.cpp impl/Session.cpp impl/SqlQuery.cpp impl/Transaction.cpp diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index df00596e..daa9fccd 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -206,7 +206,7 @@ namespace lms::db LMS_LOG(DB, INFO, "Performing quick database check..."); // Quick check is just a simple integrity check - bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) { + const bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) { LMS_LOG(DB, ERROR, "Quick check error: " << error); }) }; @@ -222,7 +222,7 @@ namespace lms::db LMS_LOG(DB, INFO, "Checking database integrity..."); - bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) { + const bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) { LMS_LOG(DB, ERROR, "Integrity check error: " << error); }) }; @@ -238,7 +238,7 @@ namespace lms::db LMS_LOG(DB, INFO, "Checking foreign key constraints..."); - bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) { + const bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) { LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'"); }) }; diff --git a/src/libs/database/impl/Db.hpp b/src/libs/database/impl/Db.hpp index 98299b0e..4d39b4b9 100644 --- a/src/libs/database/impl/Db.hpp +++ b/src/libs/database/impl/Db.hpp @@ -36,8 +36,6 @@ namespace lms::db public: Db(const std::filesystem::path& dbPath, std::size_t connectionCount); - Session& getTLSSession() override; - void executeSql(const std::string& sql); private: @@ -46,6 +44,8 @@ namespace lms::db friend class Session; + Session& getTLSSession() override; + core::RecursiveSharedMutex& getMutex() { return _sharedMutex; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } diff --git a/src/libs/database/impl/QueryPlanRecorder.cpp b/src/libs/database/impl/QueryPlanRecorder.cpp new file mode 100644 index 00000000..fef01d1c --- /dev/null +++ b/src/libs/database/impl/QueryPlanRecorder.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 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 "QueryPlanRecorder.hpp" + +#include +#include + +#include +#include + +#include "core/ILogger.hpp" + +namespace lms::db +{ + std::unique_ptr createQueryPlanRecorder() + { + return std::make_unique(); + } + + QueryPlanRecorder::QueryPlanRecorder() + { + LMS_LOG(DB, INFO, "Recording database query plans"); + } + + QueryPlanRecorder::~QueryPlanRecorder() = default; + + void QueryPlanRecorder::visitQueryPlans(const QueryPlanVisitor& visitor) const + { + const std::shared_lock lock{ _mutex }; + + for (const auto& [query, plan] : _queryPlans) + visitor(query, plan); + } + + void QueryPlanRecorder::recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query) + { + { + const std::shared_lock lock{ _mutex }; + + if (_queryPlans.contains(query)) + return; + } + + Wt::Dbo::Transaction transaction{ session }; + + Wt::Dbo::SqlConnection* connection{ transaction.connection() }; + auto statement{ connection->prepareStatement("EXPLAIN QUERY PLAN " + query) }; + statement->execute(); + + std::map entries{ { 0, "" } }; + std::map> relationships; + + std::string detail; + while (statement->nextRow()) + { + detail.clear(); + + int id{}; + int parent{}; + int unused{}; + + if (statement->getResult(0, &id) + && statement->getResult(1, &parent) + && statement->getResult(2, &unused) + && statement->getResult(3, &detail, static_cast(detail.capacity()))) + { + entries.emplace(id, detail); + relationships[parent].push_back(id); + } + } + + // format + std::string result; + std::function formatQuery = [&](int id, unsigned level) -> void { + for (std::size_t i{}; i < level; ++i) + result += '\t'; + + result += entries.at(id); + result += '\n'; + auto itChildren = relationships.find(id); + if (itChildren == relationships.end()) + return; + + for (int child : itChildren->second) + formatQuery(child, level + 1); + }; + + formatQuery(0, 0); + + { + const std::unique_lock lock{ _mutex }; + _queryPlans.try_emplace(query, std::move(result)); + } + } +} // namespace lms::db diff --git a/src/libs/database/impl/QueryPlanRecorder.hpp b/src/libs/database/impl/QueryPlanRecorder.hpp new file mode 100644 index 00000000..6025b7a1 --- /dev/null +++ b/src/libs/database/impl/QueryPlanRecorder.hpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 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 +#include +#include + +#include + +#include "database/IQueryPlanRecorder.hpp" + +namespace lms::db +{ + class QueryPlanRecorder : public IQueryPlanRecorder + { + public: + QueryPlanRecorder(); + ~QueryPlanRecorder() override; + QueryPlanRecorder(const QueryPlanRecorder&) = delete; + QueryPlanRecorder& operator=(const QueryPlanRecorder&) = delete; + + void visitQueryPlans(const QueryPlanVisitor& visitor) const override; + + void recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query); + + private: + mutable std::shared_mutex _mutex; + std::map _queryPlans; + }; +} // namespace lms::db diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 1fe3fc19..7cbd1f03 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -19,22 +19,39 @@ #pragma once -#include #include #include -#include +#include +#include +#include +#include #include #include "core/ITraceLogger.hpp" +#include "core/Service.hpp" #include "database/Types.hpp" +#include "QueryPlanRecorder.hpp" + namespace lms::db::utils { #define ESCAPE_CHAR_STR "\\" static inline constexpr char escapeChar{ '\\' }; std::string escapeLikeKeyword(std::string_view keywords); + Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); + + namespace details + { + template + void recordQueryPlanIfNeeded(const Query& query) + { + if (IQueryPlanRecorder * recorder{ core::Service::get() }) + static_cast(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString()); + } + } // namespace details + template void applyRange(Query& query, std::optional range) { @@ -82,13 +99,18 @@ namespace lms::db::utils template void forEachQueryResult(const Query& query, UnaryFunc&& func) { + details::recordQueryPlanIfNeeded(query); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString()); + forEachResult(query.resultList(), std::forward(func)); } template std::vector fetchQueryResults(const Query& query) { + details::recordQueryPlanIfNeeded(query); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); auto collection{ query.resultList() }; @@ -98,6 +120,8 @@ namespace lms::db::utils template std::vector::type> fetchQueryResults(const Query& query) { + details::recordQueryPlanIfNeeded(query); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); auto collection{ query.resultList() }; @@ -107,6 +131,8 @@ namespace lms::db::utils template auto fetchQuerySingleResult(const Query& query) { + details::recordQueryPlanIfNeeded(query); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString()); return query.resultValue(); } @@ -183,6 +209,4 @@ namespace lms::db::utils call.run(); } } - - Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); } // namespace lms::db::utils \ No newline at end of file diff --git a/src/libs/database/impl/objects/Artwork.cpp b/src/libs/database/impl/objects/Artwork.cpp index 444b8070..2c8a350e 100644 --- a/src/libs/database/impl/objects/Artwork.cpp +++ b/src/libs/database/impl/objects/Artwork.cpp @@ -18,6 +18,7 @@ */ #include "database/objects/Artwork.hpp" + #include #include "database/Session.hpp" diff --git a/src/libs/database/include/database/IQueryPlanRecorder.hpp b/src/libs/database/include/database/IQueryPlanRecorder.hpp new file mode 100644 index 00000000..298634c8 --- /dev/null +++ b/src/libs/database/include/database/IQueryPlanRecorder.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 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 +#include + +namespace lms::db +{ + // Due to technical limitations, query plans are recorded globally across all databases. + // As a result, this class is implemented as a singleton rather than being owned per DB instance. + class IQueryPlanRecorder + { + public: + virtual ~IQueryPlanRecorder() = default; + + using QueryPlanVisitor = std::function; + virtual void visitQueryPlans(const QueryPlanVisitor& visitor) const = 0; + }; + + std::unique_ptr createQueryPlanRecorder(); +} // namespace lms::db diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 9db7d25a..2adef2e1 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -14,10 +14,12 @@ add_executable(lms ui/State.cpp ui/Tooltip.cpp ui/Utils.cpp + ui/admin/debug/Database.cpp + ui/admin/debug/Tracing.cpp + ui/admin/DebugToolsView.cpp ui/admin/InitWizardView.cpp ui/admin/MediaLibrariesView.cpp ui/admin/MediaLibraryModal.cpp - ui/admin/TracingView.cpp ui/admin/ScannerController.cpp ui/admin/ScannerReportResource.cpp ui/admin/ScanSettingsView.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 9110af3e..09befea6 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -33,6 +33,7 @@ #include "core/SystemPaths.hpp" #include "core/WtLogger.hpp" #include "database/IDb.hpp" +#include "database/IQueryPlanRecorder.hpp" #include "database/Session.hpp" #include "image/Image.hpp" #include "services/artwork/IArtworkService.hpp" @@ -315,6 +316,10 @@ namespace lms core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" }; + core::Service queryPlanRecorder; + if (config->getBool("db-record-query-plans", false)) + queryPlanRecorder.assign(db::createQueryPlanRecorder()); + // Connection pool size must be twice the number of threads: we have at least 2 io pools with getThreadCount() each and they all may access the database auto database{ db::createDb(config->getPath("working-dir", "/var/lms") / "lms.db", getThreadCount() * 2) }; { diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 3acf03b2..e6f6034e 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -30,6 +30,7 @@ #include "core/ITraceLogger.hpp" #include "core/Service.hpp" #include "database/IDb.hpp" +#include "database/IQueryPlanRecorder.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Cluster.hpp" @@ -51,11 +52,11 @@ #include "NotificationContainer.hpp" #include "PlayQueue.hpp" #include "SettingsView.hpp" +#include "admin/DebugToolsView.hpp" #include "admin/InitWizardView.hpp" #include "admin/MediaLibrariesView.hpp" #include "admin/ScanSettingsView.hpp" #include "admin/ScannerController.hpp" -#include "admin/TracingView.hpp" #include "admin/UserView.hpp" #include "admin/UsersView.hpp" #include "common/Template.hpp" @@ -75,6 +76,8 @@ namespace lms::ui const std::string appRoot{ Wt::WApplication::appRoot() }; auto res{ std::make_shared() }; + res->use(appRoot + "admin-db"); + res->use(appRoot + "admin-debugtools"); res->use(appRoot + "admin-initwizard"); res->use(appRoot + "admin-medialibraries"); res->use(appRoot + "admin-medialibrary"); @@ -132,7 +135,7 @@ namespace lms::ui IdxAdminScanner, IdxAdminUsers, IdxAdminUser, - IdxAdminTracing, + IdxAdminDebugTools, }; void handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) @@ -158,7 +161,7 @@ namespace lms::ui { "/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/tracing", IdxAdminTracing, true, Wt::WString::tr("Lms.Admin.Tracing.tracing") }, + { "/admin/debug-tools", IdxAdminDebugTools, true, Wt::WString::tr("Lms.Admin.DebugTools.debug-tools") }, }; LMS_LOG(UI, DEBUG, "Internal path changed to '" << wApp->internalPath() << "'"); @@ -461,11 +464,13 @@ namespace lms::ui 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 trace logger is not enabled - if (core::Service::get()) - navbar->bindNew("tracing", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/tracing" }, Wt::WString::tr("Lms.Admin.menu-tracing")); - else - navbar->bindEmpty("tracing"); + // Hide the entry if no debug service is enabled + if (core::Service::get() + || core::Service::get()) + { + navbar->setCondition("if-debug-tools", true); + navbar->bindNew("debug-tools", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/debug-tools" }, Wt::WString::tr("Lms.Admin.menu-debug-tools")); + } } // Contents @@ -486,7 +491,7 @@ namespace lms::ui mainStack->addNew(); mainStack->addNew(); mainStack->addNew(); - mainStack->addNew(); + mainStack->addNew(); } explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity()); diff --git a/src/lms/ui/admin/DebugToolsView.cpp b/src/lms/ui/admin/DebugToolsView.cpp new file mode 100644 index 00000000..570c97fa --- /dev/null +++ b/src/lms/ui/admin/DebugToolsView.cpp @@ -0,0 +1,36 @@ +/* + * 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 "DebugToolsView.hpp" + +#include "admin/debug/Database.hpp" +#include "debug/Database.hpp" +#include "debug/Tracing.hpp" + +namespace lms::ui +{ + DebugToolsView::DebugToolsView() + : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.DebugTools.template") } + { + addFunction("tr", &Wt::WTemplate::Functions::tr); + + bindNew("tracing"); + bindNew("db"); + } +} // namespace lms::ui diff --git a/src/lms/ui/admin/DebugToolsView.hpp b/src/lms/ui/admin/DebugToolsView.hpp new file mode 100644 index 00000000..152f4fd5 --- /dev/null +++ b/src/lms/ui/admin/DebugToolsView.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 lms::ui +{ + class DebugToolsView : public Wt::WTemplate + { + public: + DebugToolsView(); + }; +} // namespace lms::ui diff --git a/src/lms/ui/admin/debug/Database.cpp b/src/lms/ui/admin/debug/Database.cpp new file mode 100644 index 00000000..811afb58 --- /dev/null +++ b/src/lms/ui/admin/debug/Database.cpp @@ -0,0 +1,91 @@ +/* + * 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 "Database.hpp" + +#include +#include +#include +#include +#include + +#include "core/ITraceLogger.hpp" +#include "core/String.hpp" +#include "database/IQueryPlanRecorder.hpp" + +namespace lms::ui +{ + namespace + { + class QueryPlansReportResource : public Wt::WResource + { + public: + QueryPlansReportResource(const db::IQueryPlanRecorder& recorder) + : _recorder{ recorder } + { + } + + ~QueryPlansReportResource() + { + beingDeleted(); + } + QueryPlansReportResource(const QueryPlansReportResource&) = delete; + QueryPlansReportResource& operator=(const QueryPlansReportResource&) = delete; + + private: + void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) + { + response.setMimeType("application/text"); + + auto encodeHttpHeaderField = [](const std::string& fieldName, const std::string& fieldValue) { + // This implements RFC 5987 + return fieldName + "*=UTF-8''" + Wt::Utils::urlEncode(fieldValue); + }; + + const std::string cdp{ encodeHttpHeaderField("filename", "LMS_db_query_plans_" + core::stringUtils::toISO8601String(Wt::WDateTime::currentDateTime()) + ".txt") }; + response.addHeader("Content-Disposition", "attachment; " + cdp); + + _recorder.visitQueryPlans([&](std::string_view query, std::string_view plan) { + response.out() << query << '\n'; + response.out() << plan << "\n-------------------------\n"; + }); + } + + const db::IQueryPlanRecorder& _recorder; + }; + } // namespace + + Database::Database() + : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.DebugTools.Db.template") } + { + addFunction("tr", &Wt::WTemplate::Functions::tr); + + Wt::WPushButton* dumpBtn{ bindNew("export-query-plans-btn", Wt::WString::tr("Lms.Admin.DebugTools.Db.export-query-plans")) }; + + if (const auto* recorder{ core::Service::get() }) + { + Wt::WLink link{ std::make_shared(*recorder) }; + link.setTarget(Wt::LinkTarget::NewWindow); + dumpBtn->setLink(link); + } + else + dumpBtn->setEnabled(false); + } + +} // namespace lms::ui diff --git a/src/lms/ui/admin/TracingView.hpp b/src/lms/ui/admin/debug/Database.hpp similarity index 92% rename from src/lms/ui/admin/TracingView.hpp rename to src/lms/ui/admin/debug/Database.hpp index 95318c9e..131bb017 100644 --- a/src/lms/ui/admin/TracingView.hpp +++ b/src/lms/ui/admin/debug/Database.hpp @@ -23,9 +23,9 @@ namespace lms::ui { - class TracingView : public Wt::WTemplate + class Database : public Wt::WTemplate { public: - TracingView(); + Database(); }; } // namespace lms::ui diff --git a/src/lms/ui/admin/TracingView.cpp b/src/lms/ui/admin/debug/Tracing.cpp similarity index 93% rename from src/lms/ui/admin/TracingView.cpp rename to src/lms/ui/admin/debug/Tracing.cpp index 948e32c6..26af6712 100644 --- a/src/lms/ui/admin/TracingView.cpp +++ b/src/lms/ui/admin/debug/Tracing.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TracingView.hpp" +#include "Tracing.hpp" #include #include @@ -73,12 +73,12 @@ namespace lms::ui }; } // namespace - TracingView::TracingView() - : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.Tracing.template") } + Tracing::Tracing() + : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.DebugTools.Tracing.template") } { addFunction("tr", &Wt::WTemplate::Functions::tr); - Wt::WPushButton* dumpBtn{ bindNew("export-btn", Wt::WString::tr("Lms.Admin.Tracing.export-current-buffer")) }; + Wt::WPushButton* dumpBtn{ bindNew("export-btn", Wt::WString::tr("Lms.Admin.DebugTools.Tracing.export-current-buffer")) }; if (auto traceLogger{ core::Service::get() }) { @@ -89,4 +89,5 @@ namespace lms::ui else dumpBtn->setEnabled(false); } + } // namespace lms::ui diff --git a/src/lms/ui/admin/debug/Tracing.hpp b/src/lms/ui/admin/debug/Tracing.hpp new file mode 100644 index 00000000..60a2964d --- /dev/null +++ b/src/lms/ui/admin/debug/Tracing.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 lms::ui +{ + class Tracing : public Wt::WTemplate + { + public: + Tracing(); + }; +} // namespace lms::ui