From de062d6c54d48cc53d080fb25ed33a6c72d6e26f Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Tue, 5 Sep 2023 01:33:42 +0200 Subject: [PATCH] Show detailed CPU load information in a tool-tip (#5970) * Profiler rework * Workaround for GCC bug * Rollback QFile removal * Use enum instead of a plain index to describe detailed stats * Use the GCC workaround code for all compilers to avoid redundancy * Update and fix comments * Implement suggestions from review * Split AudioEngine::renderNextBuffer() into separate functions, fix old formatting * Remove QFile include * Revert formatting changes * Apply suggestion from review (remove unnecessary template parameter) * Revert more formatting changes * Convert DetailType to enum class * DetailType enum class improvements suggested in review * Use std::atomic for m_detailLoad * RAII-style profiler probes * Apply suggestions from code review Co-authored-by: Dominic Clark * Fix namespace comment * Improve CPU load widget precision and use floats for load computations * Atomic m_cpuLoad * Add custom step size support for CPULoadWidget * Apply suggestions from review (convert the profiler probe into a nested class, other small changes) * Do not limit stored load averages to 100% --------- Co-authored-by: sakertooth Co-authored-by: Dominic Clark --- data/themes/classic/style.css | 1 + data/themes/default/style.css | 1 + include/AudioEngine.h | 9 ++++++ include/AudioEngineProfiler.h | 48 +++++++++++++++++++++++++++++- include/CPULoadWidget.h | 6 ++++ src/core/AudioEngine.cpp | 49 ++++++++++++++++++++++++------- src/core/AudioEngineProfiler.cpp | 22 ++++++++++++-- src/gui/widgets/CPULoadWidget.cpp | 27 ++++++++++++----- 8 files changed, 140 insertions(+), 23 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index d1f4d0588..ac22a14ec 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -209,6 +209,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 4; } /* scrollbar: trough */ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e88e51e5b..092332eee 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -241,6 +241,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 1; } /* scrollbar: trough */ diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 030c5bce3..f056c22e1 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -275,6 +275,11 @@ public: return m_profiler.cpuLoad(); } + int detailLoad(const AudioEngineProfiler::DetailType type) const + { + return m_profiler.detailLoad(type); + } + const qualitySettings & currentQualitySettings() const { return m_qualitySettings; @@ -401,6 +406,10 @@ private: AudioDevice * tryAudioDevices(); MidiClient * tryMidiClients(); + void renderStageNoteSetup(); + void renderStageInstruments(); + void renderStageEffects(); + void renderStageMix(); const surroundSampleFrame * renderNextBuffer(); diff --git a/include/AudioEngineProfiler.h b/include/AudioEngineProfiler.h index 7b5191e76..b0d62a1dc 100644 --- a/include/AudioEngineProfiler.h +++ b/include/AudioEngineProfiler.h @@ -25,6 +25,8 @@ #ifndef LMMS_AUDIO_ENGINE_PROFILER_H #define LMMS_AUDIO_ENGINE_PROFILER_H +#include +#include #include #include "lmms_basics.h" @@ -53,11 +55,55 @@ public: void setOutputFile( const QString& outputFile ); + enum class DetailType { + NoteSetup, + Instruments, + Effects, + Mixing, + Count + }; + + constexpr static auto DetailCount = static_cast(DetailType::Count); + + int detailLoad(const DetailType type) const + { + return m_detailLoad[static_cast(type)].load(std::memory_order_relaxed); + } + + class Probe + { + public: + Probe(AudioEngineProfiler& profiler, AudioEngineProfiler::DetailType type) + : m_profiler(profiler) + , m_type(type) + { + profiler.startDetail(type); + } + ~Probe() { m_profiler.finishDetail(m_type); } + Probe& operator=(const Probe&) = delete; + Probe(const Probe&) = delete; + Probe(Probe&&) = delete; + + private: + AudioEngineProfiler &m_profiler; + const AudioEngineProfiler::DetailType m_type; + }; private: + void startDetail(const DetailType type) { m_detailTimer[static_cast(type)].reset(); } + void finishDetail(const DetailType type) + { + m_detailTime[static_cast(type)] = m_detailTimer[static_cast(type)].elapsed(); + } + MicroTimer m_periodTimer; - int m_cpuLoad; + std::atomic m_cpuLoad; QFile m_outputFile; + + // Use arrays to avoid dynamic allocations in realtime code + std::array m_detailTimer; + std::array m_detailTime{0}; + std::array, DetailCount> m_detailLoad{0}; }; } // namespace lmms diff --git a/include/CPULoadWidget.h b/include/CPULoadWidget.h index 904445c67..dfa5bac73 100644 --- a/include/CPULoadWidget.h +++ b/include/CPULoadWidget.h @@ -26,6 +26,7 @@ #ifndef LMMS_GUI_CPU_LOAD_WIDGET_H #define LMMS_GUI_CPU_LOAD_WIDGET_H +#include #include #include #include @@ -40,6 +41,7 @@ namespace lmms::gui class CPULoadWidget : public QWidget { Q_OBJECT + Q_PROPERTY(int stepSize MEMBER m_stepSize) public: CPULoadWidget( QWidget * _parent ); ~CPULoadWidget() override = default; @@ -54,6 +56,8 @@ protected slots: private: + int stepSize() const { return std::max(1, m_stepSize); } + int m_currentLoad; QPixmap m_temp; @@ -64,6 +68,8 @@ private: QTimer m_updateTimer; + int m_stepSize; + } ; diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 21a9a3598..2d077babc 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -333,12 +333,9 @@ void AudioEngine::pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ) - -const surroundSampleFrame * AudioEngine::renderNextBuffer() +void AudioEngine::renderStageNoteSetup() { - m_profiler.startPeriod(); - - s_renderingThread = true; + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::NoteSetup); if( m_clearSignal ) { @@ -387,9 +384,15 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() m_newPlayHandles.free( e ); e = next; } +} - // STAGE 1: run and render all play handles - AudioEngineWorkerThread::fillJobQueue( m_playHandles ); + + +void AudioEngine::renderStageInstruments() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Instruments); + + AudioEngineWorkerThread::fillJobQueue(m_playHandles); AudioEngineWorkerThread::startAndWaitForJobs(); // removed all play handles which are done @@ -417,16 +420,28 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() ++it; } } +} + + + +void AudioEngine::renderStageEffects() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Effects); // STAGE 2: process effects of all instrument- and sampletracks AudioEngineWorkerThread::fillJobQueue(m_audioPorts); AudioEngineWorkerThread::startAndWaitForJobs(); +} - // STAGE 3: do master mix in mixer + +void AudioEngine::renderStageMix() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Mixing); + + Mixer *mixer = Engine::mixer(); mixer->masterMix(m_outputBufferWrite); - emit nextAudioBuffer(m_outputBufferRead); runChangesInModel(); @@ -435,10 +450,22 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() EnvelopeAndLfoParameters::instances()->trigger(); Controller::triggerFrameCounter(); AutomatableModel::incrementPeriodCounter(); +} + + + +const surroundSampleFrame *AudioEngine::renderNextBuffer() +{ + m_profiler.startPeriod(); + s_renderingThread = true; + + renderStageNoteSetup(); // STAGE 0: clear old play handles and buffers, setup new play handles + renderStageInstruments(); // STAGE 1: run and render all play handles + renderStageEffects(); // STAGE 2: process effects of all instrument- and sampletracks + renderStageMix(); // STAGE 3: do master mix in mixer s_renderingThread = false; - - m_profiler.finishPeriod( processingSampleRate(), m_framesPerPeriod ); + m_profiler.finishPeriod(processingSampleRate(), m_framesPerPeriod); return m_outputBufferRead; } diff --git a/src/core/AudioEngineProfiler.cpp b/src/core/AudioEngineProfiler.cpp index 9e05ff80a..82a412cbb 100644 --- a/src/core/AudioEngineProfiler.cpp +++ b/src/core/AudioEngineProfiler.cpp @@ -24,6 +24,8 @@ #include "AudioEngineProfiler.h" +#include + namespace lmms { @@ -38,10 +40,24 @@ AudioEngineProfiler::AudioEngineProfiler() : void AudioEngineProfiler::finishPeriod( sample_rate_t sampleRate, fpp_t framesPerPeriod ) { - int periodElapsed = m_periodTimer.elapsed(); + // Time taken to process all data and fill the audio buffer. + const unsigned int periodElapsed = m_periodTimer.elapsed(); + // Maximum time the processing can take before causing buffer underflow. Convert to us. + const uint64_t timeLimit = static_cast(1000000) * framesPerPeriod / sampleRate; - const float newCpuLoad = periodElapsed / 10000.0f * sampleRate / framesPerPeriod; - m_cpuLoad = std::clamp((newCpuLoad * 0.1f + m_cpuLoad * 0.9f), 0, 100); + // Compute new overall CPU load and apply exponential averaging. + // The result is used for overload detection in AudioEngine::criticalXRuns() + // → the weight of a new sample must be high enough to allow relatively fast changes! + const auto newCpuLoad = 100.f * periodElapsed / timeLimit; + m_cpuLoad = newCpuLoad * 0.1f + m_cpuLoad * 0.9f; + + // Compute detailed load analysis. Can use stronger averaging to get more stable readout. + for (std::size_t i = 0; i < DetailCount; i++) + { + const auto newLoad = 100.f * m_detailTime[i] / timeLimit; + const auto oldLoad = m_detailLoad[i].load(std::memory_order_relaxed); + m_detailLoad[i].store(newLoad * 0.05f + oldLoad * 0.95f, std::memory_order_relaxed); + } if( m_outputFile.isOpen() ) { diff --git a/src/gui/widgets/CPULoadWidget.cpp b/src/gui/widgets/CPULoadWidget.cpp index 799e037ef..db1f5cacc 100644 --- a/src/gui/widgets/CPULoadWidget.cpp +++ b/src/gui/widgets/CPULoadWidget.cpp @@ -24,6 +24,7 @@ */ +#include #include #include "AudioEngine.h" @@ -72,10 +73,9 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) QPainter p( &m_temp ); p.drawPixmap( 0, 0, m_background ); - // as load-indicator consists of small 2-pixel wide leds with - // 1 pixel spacing, we have to make sure, only whole leds are - // shown which we achieve by the following formula - int w = ( m_leds.width() * m_currentLoad / 300 ) * 3; + // Normally the CPU load indicator moves smoothly, with 1 pixel resolution. However, some themes may want to + // draw discrete elements (like LEDs), so the stepSize property can be used to specify a larger step size. + int w = (m_leds.width() * std::min(m_currentLoad, 100) / (stepSize() * 100)) * stepSize(); if( w > 0 ) { p.drawPixmap( 23, 3, m_leds, 0, 0, w, @@ -91,10 +91,21 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) void CPULoadWidget::updateCpuLoad() { - // smooth load-values a bit - int new_load = ( m_currentLoad + Engine::audioEngine()->cpuLoad() ) / 2; - if( new_load != m_currentLoad ) + // Additional display smoothing for the main load-value. Stronger averaging + // cannot be used directly in the profiler: cpuLoad() must react fast enough + // to be useful as overload indicator in AudioEngine::criticalXRuns(). + const int new_load = (m_currentLoad + Engine::audioEngine()->cpuLoad()) / 2; + + if (new_load != m_currentLoad) { + auto engine = Engine::audioEngine(); + setToolTip( + tr("DSP total: %1%").arg(new_load) + "\n" + + tr(" - Notes and setup: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::NoteSetup)) + "\n" + + tr(" - Instruments: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Instruments)) + "\n" + + tr(" - Effects: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Effects)) + "\n" + + tr(" - Mixing: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Mixing)) + ); m_currentLoad = new_load; m_changed = true; update(); @@ -102,4 +113,4 @@ void CPULoadWidget::updateCpuLoad() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui