Added a small builtin benchmark to decode files to PCM

This commit is contained in:
emeric
2026-06-20 10:24:58 +02:00
parent 8fa58f1775
commit 62f4766f82
15 changed files with 675 additions and 10 deletions
+15
View File
@@ -8,8 +8,23 @@
<hr/>
<legend>${tr:Lms.Admin.DebugTools.Db.db}</legend>
${db}
<hr/>
<legend>${tr:Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats}</legend>
${pcm-decoding-stats}
</form>
</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.template">
<form>
<div class="row g-3">
<div class="col-12">
${run-btn class="btn btn-primary"}
${download-btn class="btn btn-secondary ms-2"}
${status}
</div>
</div>
</form>
</message>
</messages>
-2
View File
@@ -45,10 +45,8 @@
<li>${scanner class="dropdown-item"}</li>
<li>${users class="dropdown-item"}</li>
<li>${about class="dropdown-item"}</li>
${<if-debug-tools>}
<li class="dropdown-divider"></li>
<li>${debug-tools class="dropdown-item"}</li>
${</if-debug-tools>}
</ul>
</li>
${</if-is-admin>}
+7
View File
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Export query profiling data</message>
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">PCM decoding stats</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Run bench</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Download results</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Running…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">PCM decoding benchmark completed in {1}</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Export traces</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message>
+7
View File
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exportar datos de perfil de consultas</message>
<message id="Lms.Admin.DebugTools.Db.db">Base de datos</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Estadísticas de decodificación PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Ejecutar benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Descargar resultados</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Ejecutando…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark de decodificación PCM completado en {1}</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exportar las trazas</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Trazas</message>
+7
View File
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exporter les données de profilage des requêtes</message>
<message id="Lms.Admin.DebugTools.Db.db">Base de données</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statistiques de décodage PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Lancer le bench</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Télécharger les résultats</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">En cours…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark de décodage PCM terminé en {1}</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exporter les traces</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Traces</message>
+7
View File
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Esporta i dati di profilazione delle query</message>
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statistiche di decodifica PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Avvia benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Scarica risultati</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">In esecuzione…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark di decodifica PCM completato in {1}</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Esporta tracce</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message>
+7
View File
@@ -186,6 +186,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Eksportuj dane profilowania zapytań</message>
<message id="Lms.Admin.DebugTools.Db.db">Baza danych</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statystyki dekodowania PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Uruchom benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Pobierz wyniki</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Trwa…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Test wydajności dekodowania PCM zakończony w {1}</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Eksportuj ślady</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Śledzenie</message>
+7
View File
@@ -167,6 +167,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">导出查询分析数据</message>
<message id="Lms.Admin.DebugTools.Db.db">数据库</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">PCM解码统计</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">运行基准测试</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">下载结果</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">运行中…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">PCM解码基准测试已在{1}内完成</message>
<!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">导出跟踪</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">跟踪</message>
+2
View File
@@ -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
+1 -8
View File
@@ -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<Wt::WAnchor>("scan-settings", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scan-settings" }, Wt::WString::tr("Lms.Admin.menu-scan-settings"));
navbar->bindNew<Wt::WAnchor>("scanner", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scanner" }, Wt::WString::tr("Lms.Admin.menu-scanner"));
navbar->bindNew<Wt::WAnchor>("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<core::tracing::ITraceLogger>::get()
|| core::Service<db::IQueryProfiler>::get())
{
navbar->setCondition("if-debug-tools", true);
navbar->bindNew<Wt::WAnchor>("debug-tools", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/debug-tools" }, Wt::WString::tr("Lms.Admin.menu-debug-tools"));
}
navbar->bindNew<Wt::WAnchor>("debug-tools", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/debug-tools" }, Wt::WString::tr("Lms.Admin.menu-debug-tools"));
}
PathRouter* mainRouter{ main->bindNew<PathRouter>("contents") };
+2
View File
@@ -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>("tracing");
bindNew<Database>("db");
bindNew<PcmDecodingStats>("pcm-decoding-stats");
}
} // namespace lms::ui
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PcmDecodingBenchmark.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <numeric>
#include <thread>
#include <vector>
#include <Wt/WApplication.h>
#include <Wt/WDateTime.h>
#include <Wt/WServer.h>
#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<TrackEntry> 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<TrackEntry> 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<std::size_t>(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<std::byte> buffer(bufferSize);
std::array<audio::IPcmDecoder::WritableBuffer, 1> 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<PcmDecodingBenchmark::CodecResult> runBench(db::IDb& db)
{
constexpr audio::PcmParameters params{
.channelCount = 2,
.sampleRate = 48000,
.sampleType = audio::PcmSampleType::Signed16,
.byteOrder = std::endian::native,
.planar = false,
};
std::vector<PcmDecodingBenchmark::CodecResult> allResults;
core::media::visitCodecs([&](const core::media::CodecDesc& codecDesc) {
const std::vector<TrackEntry> tracks{ pickTracksForCodec(db.getTLSSession(), codecDesc.type) };
if (tracks.empty())
return;
std::vector<PcmDecodingBenchmark::TrackDecodeResult> trackResults;
for (const TrackEntry& entry : tracks)
{
try
{
const DecodeResult res{ decodeFile(entry.path, entry.fileSize, params) };
const float elapsedSeconds{ std::chrono::duration<float>{ res.decodeDuration }.count() };
if (elapsedSeconds <= 0.F || res.decodedSampleCount == 0)
continue;
const float decodedAudioSeconds{ static_cast<float>(res.decodedSampleCount) / params.sampleRate };
trackResults.push_back(PcmDecodingBenchmark::TrackDecodeResult{
.path = entry.path,
.bitrate = entry.bitrate,
.duration = entry.duration,
.realTimeFactor = decodedAudioSeconds / elapsedSeconds,
.speedKBs = (static_cast<float>(res.fileSize) / 1024.F) / elapsedSeconds,
});
}
catch (const audio::Exception&)
{
}
}
if (trackResults.empty())
return;
std::vector<float> 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<float>(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<float>(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<CodecResult> results{ runBench(db) };
const auto elapsed{ std::chrono::duration_cast<std::chrono::milliseconds>(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<void(State, State)> 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<State, State>* 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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <mutex>
#include <span>
#include <string>
#include <unordered_map>
#include <vector>
#include <Wt/WSignal.h>
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<TrackDecodeResult> tracks;
float minRealTimeFactor{};
float maxRealTimeFactor{};
float meanRealTimeFactor{};
float stdDevRealTimeFactor{};
};
static PcmDecodingBenchmark& instance();
State getState() const;
std::span<const CodecResult> 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<void(State oldState, State newState)> f);
void unregisterOnStateChanged(const std::string& sessionId);
private:
void postStateToAllSessions(State oldState, State newState);
State _state{ State::Idle };
mutable std::mutex _mutex;
std::vector<CodecResult> _results;
std::chrono::milliseconds _elapsed{};
std::string _reportFilename;
std::mutex _signalMutex;
std::unordered_map<std::string, Wt::Signal<State, State>> _sessionSignals;
};
} // namespace lms::ui
+200
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PcmDecodingStats.hpp"
#include <algorithm>
#include <chrono>
#include <span>
#include <cmath>
#include <numeric>
#include <sstream>
#include <vector>
#include <Wt/Http/Response.h>
#include <Wt/Utils.h>
#include <Wt/WAnchor.h>
#include <Wt/WApplication.h>
#include <Wt/WLink.h>
#include <Wt/WPushButton.h>
#include <Wt/WResource.h>
#include <Wt/WString.h>
#include <Wt/WText.h>
#include "LmsApplication.hpp"
#include "Notification.hpp"
#include "PcmDecodingBenchmark.hpp"
namespace lms::ui
{
namespace
{
class PcmDecodingStatsResource : public Wt::WResource
{
public:
PcmDecodingStatsResource(std::span<const PcmDecodingBenchmark::CodecResult> 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<std::chrono::seconds>(_elapsed).count() << "s)\n\n";
std::vector<float> 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<long>(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<long>(t.realTimeFactor) << "x"
<< " speed=" << static_cast<long>(t.speedKBs) << " KB/s\n";
allRtFactors.push_back(t.realTimeFactor);
}
response.out() << " real_time_factor summary:"
<< " min=" << static_cast<long>(r.minRealTimeFactor) << "x"
<< " max=" << static_cast<long>(r.maxRealTimeFactor) << "x"
<< " mean=" << static_cast<long>(r.meanRealTimeFactor) << "x"
<< " stddev=" << static_cast<long>(r.stdDevRealTimeFactor) << "x\n\n";
}
if (!allRtFactors.empty())
{
const float mean{ std::accumulate(allRtFactors.begin(), allRtFactors.end(), 0.F) / static_cast<float>(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<float>(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<long>(*minIt) << "x"
<< " max=" << static_cast<long>(*maxIt) << "x"
<< " mean=" << static_cast<long>(mean) << "x"
<< " stddev=" << static_cast<long>(std::sqrtf(variance)) << "x\n";
}
}
std::vector<PcmDecodingBenchmark::CodecResult> _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<Wt::WPushButton>("run-btn", Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.run"));
_downloadBtn = bindNew<Wt::WAnchor>("download-btn");
_downloadBtn->setText(Wt::WString::tr("Lms.Admin.DebugTools.PcmDecodingStats.download"));
_statusText = bindNew<Wt::WText>("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<std::chrono::seconds>(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<PcmDecodingStatsResource>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WAnchor.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h>
#include <Wt/WText.h>
#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