Added a small builtin benchmark to decode files to PCM
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,14 +447,8 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
PathRouter* mainRouter{ main->bindNew<PathRouter>("contents") };
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user