diff --git a/approot/admin-debugtools.xml b/approot/admin-debugtools.xml index 83025d9f..b7d380ea 100644 --- a/approot/admin-debugtools.xml +++ b/approot/admin-debugtools.xml @@ -8,8 +8,23 @@
${tr:Lms.Admin.DebugTools.Db.db} ${db} +
+ ${tr:Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats} + ${pcm-decoding-stats} + +
+
+
+ ${run-btn class="btn btn-primary"} + ${download-btn class="btn btn-secondary ms-2"} + ${status} +
+
+
+
+ diff --git a/approot/main.xml b/approot/main.xml index 1fb31ea5..a3fb4c71 100644 --- a/approot/main.xml +++ b/approot/main.xml @@ -45,10 +45,8 @@
  • ${scanner class="dropdown-item"}
  • ${users class="dropdown-item"}
  • ${about class="dropdown-item"}
  • - ${}
  • ${debug-tools class="dropdown-item"}
  • - ${
    } ${} diff --git a/approot/messages.xml b/approot/messages.xml index 732e805b..146bfdbe 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -169,6 +169,13 @@ Export query profiling data Database + +PCM decoding stats +Run bench +Download results +Running… +PCM decoding benchmark completed in {1} + Export traces Tracing diff --git a/approot/messages_es.xml b/approot/messages_es.xml index c14b8491..21ba6ad6 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -169,6 +169,13 @@ Exportar datos de perfil de consultas Base de datos + +Estadísticas de decodificación PCM +Ejecutar benchmark +Descargar resultados +Ejecutando… +Benchmark de decodificación PCM completado en {1} + Exportar las trazas Trazas diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 97f392b4..44a7f588 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -169,6 +169,13 @@ Exporter les données de profilage des requêtes Base de données + +Statistiques de décodage PCM +Lancer le bench +Télécharger les résultats +En cours… +Benchmark de décodage PCM terminé en {1} + Exporter les traces Traces diff --git a/approot/messages_it.xml b/approot/messages_it.xml index da46dd5d..1ec58f23 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -169,6 +169,13 @@ Esporta i dati di profilazione delle query Database + +Statistiche di decodifica PCM +Avvia benchmark +Scarica risultati +In esecuzione… +Benchmark di decodifica PCM completato in {1} + Esporta tracce Tracing diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 16523ae9..f35671cd 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -186,6 +186,13 @@ Eksportuj dane profilowania zapytań Baza danych + +Statystyki dekodowania PCM +Uruchom benchmark +Pobierz wyniki +Trwa… +Test wydajności dekodowania PCM zakończony w {1} + Eksportuj ślady Śledzenie diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 2da4a425..c0bc9f44 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -167,6 +167,13 @@ 导出查询分析数据 数据库 + +PCM解码统计 +运行基准测试 +下载结果 +运行中… +PCM解码基准测试已在{1}内完成 + 导出跟踪 跟踪 diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 6ac4bc93..13ecb5b8 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -22,6 +22,8 @@ add_executable(lms ui/Utils.cpp ui/admin/AdminView.cpp ui/admin/debug/Database.cpp + ui/admin/debug/PcmDecodingBenchmark.cpp + ui/admin/debug/PcmDecodingStats.cpp ui/admin/debug/Tracing.cpp ui/admin/About.cpp ui/admin/DebugToolsView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 36a27d66..a2523e88 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -40,7 +40,6 @@ #include "database/objects/Release.hpp" #include "database/objects/TrackList.hpp" #include "database/objects/User.hpp" -#include "database/profiling/IQueryProfiler.hpp" #include "services/artwork/IArtworkService.hpp" #include "services/auth/IAuthTokenService.hpp" #include "services/auth/IEnvService.hpp" @@ -448,13 +447,7 @@ 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 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")); - } + navbar->bindNew("debug-tools", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/debug-tools" }, Wt::WString::tr("Lms.Admin.menu-debug-tools")); } PathRouter* mainRouter{ main->bindNew("contents") }; diff --git a/src/lms/ui/admin/DebugToolsView.cpp b/src/lms/ui/admin/DebugToolsView.cpp index 570c97fa..d83f8198 100644 --- a/src/lms/ui/admin/DebugToolsView.cpp +++ b/src/lms/ui/admin/DebugToolsView.cpp @@ -21,6 +21,7 @@ #include "admin/debug/Database.hpp" #include "debug/Database.hpp" +#include "debug/PcmDecodingStats.hpp" #include "debug/Tracing.hpp" namespace lms::ui @@ -32,5 +33,6 @@ namespace lms::ui bindNew("tracing"); bindNew("db"); + bindNew("pcm-decoding-stats"); } } // namespace lms::ui diff --git a/src/lms/ui/admin/debug/PcmDecodingBenchmark.cpp b/src/lms/ui/admin/debug/PcmDecodingBenchmark.cpp new file mode 100644 index 00000000..a34f0ac1 --- /dev/null +++ b/src/lms/ui/admin/debug/PcmDecodingBenchmark.cpp @@ -0,0 +1,272 @@ +/* + * 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 "PcmDecodingBenchmark.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "audio/Exception.hpp" +#include "audio/IPcmDecoder.hpp" +#include "audio/PcmTypes.hpp" +#include "core/String.hpp" +#include "core/media/Codec.hpp" +#include "database/IDb.hpp" +#include "database/Session.hpp" +#include "database/objects/Filters.hpp" +#include "database/objects/Track.hpp" +#include "database/objects/Types.hpp" + +#include "LmsApplication.hpp" + +namespace lms::ui +{ + namespace + { + using Clock = std::chrono::steady_clock; + + struct DecodeResult + { + std::size_t fileSize{}; + std::size_t decodedSampleCount{}; + Clock::duration decodeDuration{}; + }; + + constexpr std::chrono::milliseconds minTrackDuration{ std::chrono::minutes{ 3 } }; + constexpr std::chrono::milliseconds maxTrackDuration{ std::chrono::minutes{ 5 } }; + constexpr std::size_t maxTracksPerCodec{ 10 }; + constexpr std::size_t queryBatchSize{ 100 }; + + struct TrackEntry + { + std::filesystem::path path; + std::chrono::milliseconds duration; + std::size_t fileSize{}; + std::size_t bitrate{}; + }; + + std::vector pickTracksForCodec(db::Session& session, core::media::Codec codec) + { + auto transaction{ session.createReadTransaction() }; + + db::Track::FindParameters params; + params.setFilters(db::Filters{}.setCodec(codec)); + params.setSortMethod(db::TrackSortMethod::Random); + params.setRange(db::Range{ .offset = 0, .size = queryBatchSize }); + + const auto results{ db::Track::find(session, params) }; + + std::vector entries; + for (const db::Track::pointer& track : results.results) + { + const auto trackDuration{ track->getDuration() }; + if (trackDuration >= minTrackDuration && trackDuration <= maxTrackDuration) + { + entries.push_back({ track->getAbsoluteFilePath(), trackDuration, static_cast(track->getFileSize()), track->getBitrate() }); + if (entries.size() == maxTracksPerCodec) + break; + } + } + + return entries; + } + + DecodeResult decodeFile(const std::filesystem::path& path, std::size_t fileSize, const audio::PcmParameters& params) + { + constexpr std::size_t bufferSamples{ 4096 }; + const std::size_t bufferSize{ bufferSamples * params.channelCount * audio::getSampleSize(params.sampleType) }; + + std::vector buffer(bufferSize); + std::array outputBuffers{ audio::IPcmDecoder::WritableBuffer{ buffer } }; + + const auto start{ Clock::now() }; + + auto decoder{ audio::createPcmDecoder(path, std::chrono::microseconds{ 0 }, params) }; + + std::size_t sampleCount{}; + while (!decoder->finished()) + sampleCount += decoder->readSamples(outputBuffers); + + return DecodeResult{ + .fileSize = fileSize, + .decodedSampleCount = sampleCount, + .decodeDuration = Clock::now() - start, + }; + } + + std::vector runBench(db::IDb& db) + { + constexpr audio::PcmParameters params{ + .channelCount = 2, + .sampleRate = 48000, + .sampleType = audio::PcmSampleType::Signed16, + .byteOrder = std::endian::native, + .planar = false, + }; + + std::vector allResults; + + core::media::visitCodecs([&](const core::media::CodecDesc& codecDesc) { + const std::vector tracks{ pickTracksForCodec(db.getTLSSession(), codecDesc.type) }; + if (tracks.empty()) + return; + + std::vector trackResults; + for (const TrackEntry& entry : tracks) + { + try + { + const DecodeResult res{ decodeFile(entry.path, entry.fileSize, params) }; + const float elapsedSeconds{ std::chrono::duration{ res.decodeDuration }.count() }; + if (elapsedSeconds <= 0.F || res.decodedSampleCount == 0) + continue; + + const float decodedAudioSeconds{ static_cast(res.decodedSampleCount) / params.sampleRate }; + trackResults.push_back(PcmDecodingBenchmark::TrackDecodeResult{ + .path = entry.path, + .bitrate = entry.bitrate, + .duration = entry.duration, + .realTimeFactor = decodedAudioSeconds / elapsedSeconds, + .speedKBs = (static_cast(res.fileSize) / 1024.F) / elapsedSeconds, + }); + } + catch (const audio::Exception&) + { + } + } + + if (trackResults.empty()) + return; + + std::vector rtFactors; + rtFactors.reserve(trackResults.size()); + for (const auto& t : trackResults) + rtFactors.push_back(t.realTimeFactor); + + const float mean{ std::accumulate(rtFactors.begin(), rtFactors.end(), 0.F) / static_cast(rtFactors.size()) }; + const float variance{ std::accumulate(rtFactors.begin(), rtFactors.end(), 0.F, [mean](float acc, float v) { + return acc + (v - mean) * (v - mean); + }) / static_cast(rtFactors.size()) }; + + const auto [minIt, maxIt]{ std::minmax_element(rtFactors.begin(), rtFactors.end()) }; + allResults.push_back(PcmDecodingBenchmark::CodecResult{ + .codecName = std::string{ codecDesc.name.str() }, + .tracks = std::move(trackResults), + .minRealTimeFactor = *minIt, + .maxRealTimeFactor = *maxIt, + .meanRealTimeFactor = mean, + .stdDevRealTimeFactor = std::sqrt(variance), + }); + }); + + return allResults; + } + } // namespace + + PcmDecodingBenchmark& PcmDecodingBenchmark::instance() + { + static PcmDecodingBenchmark s_instance; + return s_instance; + } + + PcmDecodingBenchmark::State PcmDecodingBenchmark::getState() const + { + std::scoped_lock lock{ _mutex }; + return _state; + } + + void PcmDecodingBenchmark::start(db::IDb& db) + { + { + State prevState; + { + std::scoped_lock lock{ _mutex }; + + if (_state == State::Running) + return; + + _reportFilename = "LMS_pcm_decoding_stats_" + core::stringUtils::toISO8601String(Wt::WDateTime::currentDateTime()) + ".txt"; + prevState = _state; + _state = State::Running; + } + + postStateToAllSessions(prevState, State::Running); + } + + std::thread{ [this, &db] { + const auto start{ Clock::now() }; + std::vector results{ runBench(db) }; + const auto elapsed{ std::chrono::duration_cast(Clock::now() - start) }; + + State prevState; + { + std::scoped_lock lock{ _mutex }; + + _results = std::move(results); + _elapsed = elapsed; + prevState = _state; + _state = State::Completed; + } + + postStateToAllSessions(prevState, State::Completed); + } }.detach(); + } + + void PcmDecodingBenchmark::registerOnStateChanged(const std::string& sessionId, std::function f) + { + std::scoped_lock lock{ _signalMutex }; + _sessionSignals[sessionId].connect(std::move(f)); + } + + void PcmDecodingBenchmark::unregisterOnStateChanged(const std::string& sessionId) + { + std::scoped_lock lock{ _signalMutex }; + _sessionSignals.erase(sessionId); + } + + void PcmDecodingBenchmark::postStateToAllSessions(State oldState, State newState) + { + Wt::WServer::instance()->postAll([this, oldState, newState] { + if (!LmsApp) + return; + + Wt::Signal* sig{ nullptr }; + { + std::scoped_lock lock{ _signalMutex }; + auto it{ _sessionSignals.find(wApp->sessionId()) }; + if (it != _sessionSignals.end()) + sig = &it->second; + } + + if (sig) + { + sig->emit(oldState, newState); + LmsApp->triggerUpdate(); + } + }); + } +} // namespace lms::ui diff --git a/src/lms/ui/admin/debug/PcmDecodingBenchmark.hpp b/src/lms/ui/admin/debug/PcmDecodingBenchmark.hpp new file mode 100644 index 00000000..3a18e238 --- /dev/null +++ b/src/lms/ui/admin/debug/PcmDecodingBenchmark.hpp @@ -0,0 +1,88 @@ +/* + * 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 +#include +#include +#include + +#include + +namespace lms::db +{ + class IDb; +} + +namespace lms::ui +{ + class PcmDecodingBenchmark + { + public: + enum class State { Idle, Running, Completed }; + + struct TrackDecodeResult + { + std::filesystem::path path; + std::size_t bitrate{}; + std::chrono::milliseconds duration; + float realTimeFactor{}; + float speedKBs{}; + }; + + struct CodecResult + { + std::string codecName; + std::vector tracks; + float minRealTimeFactor{}; + float maxRealTimeFactor{}; + float meanRealTimeFactor{}; + float stdDevRealTimeFactor{}; + }; + + static PcmDecodingBenchmark& instance(); + + State getState() const; + std::span getResults() const { return _results; } + std::chrono::milliseconds getElapsed() const { return _elapsed; } + std::string_view getReportFilename() const { return _reportFilename; } + + void start(db::IDb& db); + + void registerOnStateChanged(const std::string& sessionId, std::function f); + void unregisterOnStateChanged(const std::string& sessionId); + + private: + void postStateToAllSessions(State oldState, State newState); + + State _state{ State::Idle }; + mutable std::mutex _mutex; + std::vector _results; + std::chrono::milliseconds _elapsed{}; + std::string _reportFilename; + + std::mutex _signalMutex; + std::unordered_map> _sessionSignals; + }; +} // namespace lms::ui diff --git a/src/lms/ui/admin/debug/PcmDecodingStats.cpp b/src/lms/ui/admin/debug/PcmDecodingStats.cpp new file mode 100644 index 00000000..6c13eb29 --- /dev/null +++ b/src/lms/ui/admin/debug/PcmDecodingStats.cpp @@ -0,0 +1,200 @@ +/* + * 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 "PcmDecodingStats.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LmsApplication.hpp" +#include "Notification.hpp" +#include "PcmDecodingBenchmark.hpp" + +namespace lms::ui +{ + namespace + { + class PcmDecodingStatsResource : public Wt::WResource + { + public: + PcmDecodingStatsResource(std::span results, std::chrono::milliseconds elapsed, std::string filename) + : _results{ results.begin(), results.end() } + , _elapsed{ elapsed } + , _filename{ std::move(filename) } + { + } + + ~PcmDecodingStatsResource() override + { + beingDeleted(); + } + + PcmDecodingStatsResource(const PcmDecodingStatsResource&) = delete; + PcmDecodingStatsResource& operator=(const PcmDecodingStatsResource&) = delete; + + private: + void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) override + { + response.setMimeType("text/plain; charset=utf-8"); + + auto encodeHttpHeaderField = [](const std::string& fieldName, const std::string& fieldValue) { + return fieldName + "*=UTF-8''" + Wt::Utils::urlEncode(fieldValue); + }; + + const std::string cdp{ encodeHttpHeaderField("filename", _filename) }; + response.addHeader("Content-Disposition", "attachment; " + cdp); + + response.out() << "PCM decoding bench (elapsed: " << std::chrono::duration_cast(_elapsed).count() << "s)\n\n"; + + std::vector allRtFactors; + for (const PcmDecodingBenchmark::CodecResult& r : _results) + { + response.out() << "=== " << r.codecName << " (" << r.tracks.size() << " track(s)) ===\n"; + + for (const PcmDecodingBenchmark::TrackDecodeResult& t : r.tracks) + { + const long durationSec{ static_cast(t.duration.count() / 1000) }; + response.out() << " " << t.path + << " bitrate=" << t.bitrate / 1000 << " kbps" + << " duration=" << durationSec / 60 << "m" << durationSec % 60 << "s" + << " real_time_factor=" << static_cast(t.realTimeFactor) << "x" + << " speed=" << static_cast(t.speedKBs) << " KB/s\n"; + allRtFactors.push_back(t.realTimeFactor); + } + + response.out() << " real_time_factor summary:" + << " min=" << static_cast(r.minRealTimeFactor) << "x" + << " max=" << static_cast(r.maxRealTimeFactor) << "x" + << " mean=" << static_cast(r.meanRealTimeFactor) << "x" + << " stddev=" << static_cast(r.stdDevRealTimeFactor) << "x\n\n"; + } + + if (!allRtFactors.empty()) + { + const float mean{ std::accumulate(allRtFactors.begin(), allRtFactors.end(), 0.F) / static_cast(allRtFactors.size()) }; + const float variance{ std::accumulate(allRtFactors.begin(), allRtFactors.end(), 0.F, [mean](float acc, float v) { + return acc + (v - mean) * (v - mean); + }) / static_cast(allRtFactors.size()) }; + + response.out() << "=== OVERALL (" << allRtFactors.size() << " track(s)) ===\n"; + const auto [minIt, maxIt]{ std::minmax_element(allRtFactors.begin(), allRtFactors.end()) }; + response.out() << " real_time_factor summary:" + << " min=" << static_cast(*minIt) << "x" + << " max=" << static_cast(*maxIt) << "x" + << " mean=" << static_cast(mean) << "x" + << " stddev=" << static_cast(std::sqrtf(variance)) << "x\n"; + } + } + + std::vector _results; + std::chrono::milliseconds _elapsed{}; + std::string _filename; + }; + } // namespace + + PcmDecodingStats::PcmDecodingStats() + : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.template") } + , _db{ LmsApp->getDb() } + { + addFunction("tr", &Wt::WTemplate::Functions::tr); + + _runBtn = bindNew("run-btn", Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.run")); + _downloadBtn = bindNew("download-btn"); + _downloadBtn->setText(Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.download")); + _statusText = bindNew("status"); + + _runBtn->clicked().connect(this, &PcmDecodingStats::onRunClicked); + + processState(PcmDecodingBenchmark::instance().getState()); + + PcmDecodingBenchmark::instance().registerOnStateChanged(wApp->sessionId(), [this](PcmDecodingBenchmark::State oldState, PcmDecodingBenchmark::State newState) { + onStateChanged(oldState, newState); + }); + } + + PcmDecodingStats::~PcmDecodingStats() + { + PcmDecodingBenchmark::instance().unregisterOnStateChanged(wApp->sessionId()); + } + + void PcmDecodingStats::onRunClicked() + { + PcmDecodingBenchmark::instance().start(_db); + } + + void PcmDecodingStats::processState(PcmDecodingBenchmark::State state) + { + switch (state) + { + case PcmDecodingBenchmark::State::Idle: + _runBtn->setEnabled(true); + _downloadBtn->hide(); + _statusText->setText({}); + break; + case PcmDecodingBenchmark::State::Running: + _runBtn->setEnabled(false); + _downloadBtn->hide(); + _statusText->setText(Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.running")); + break; + case PcmDecodingBenchmark::State::Completed: + _runBtn->setEnabled(true); + _statusText->setText({}); + setupDownloadButton(); + break; + } + } + + void PcmDecodingStats::onStateChanged(PcmDecodingBenchmark::State oldState, PcmDecodingBenchmark::State newState) + { + processState(newState); + + if (oldState == PcmDecodingBenchmark::State::Running && newState == PcmDecodingBenchmark::State::Completed) + { + std::ostringstream oss; + oss << std::chrono::duration_cast(PcmDecodingBenchmark::instance().getElapsed()).count() << "s"; + LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed").arg(oss.str())); + } + } + + void PcmDecodingStats::setupDownloadButton() + { + auto& bench{ PcmDecodingBenchmark::instance() }; + auto resource{ std::make_shared(bench.getResults(), bench.getElapsed(), std::string{ bench.getReportFilename() } ) }; + + Wt::WLink link{ resource }; + link.setTarget(Wt::LinkTarget::NewWindow); + _downloadBtn->setLink(link); + _downloadBtn->show(); + } +} // namespace lms::ui diff --git a/src/lms/ui/admin/debug/PcmDecodingStats.hpp b/src/lms/ui/admin/debug/PcmDecodingStats.hpp new file mode 100644 index 00000000..a894fe15 --- /dev/null +++ b/src/lms/ui/admin/debug/PcmDecodingStats.hpp @@ -0,0 +1,53 @@ +/* + * 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 "PcmDecodingBenchmark.hpp" + +namespace lms::db +{ + class IDb; +} + +namespace lms::ui +{ + class PcmDecodingStats : public Wt::WTemplate + { + public: + PcmDecodingStats(); + ~PcmDecodingStats() override; + + private: + void onRunClicked(); + void onStateChanged(PcmDecodingBenchmark::State oldState, PcmDecodingBenchmark::State newState); + void processState(PcmDecodingBenchmark::State state); + void setupDownloadButton(); + + db::IDb& _db; + Wt::WPushButton* _runBtn{}; + Wt::WAnchor* _downloadBtn{}; + Wt::WText* _statusText{}; + }; +} // namespace lms::ui