diff --git a/AUTHORS b/AUTHORS index 5b22023..ad8efd1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,4 +2,5 @@ # following the format reported by `git shortlog -sen | cut -f 2`. Sean Moore - +mhoffman +pestophagous diff --git a/src/app/app.pro b/src/app/app.pro index 05c9121..6b452f5 100644 --- a/src/app/app.pro +++ b/src/app/app.pro @@ -2,6 +2,8 @@ QT += core qml quick svg widgets quickcontrols2 +CONFIG += c++17 + SOURCES += \ event_filter.cc \ gui_tests.cc \ diff --git a/src/app/view_model_collection.cc b/src/app/view_model_collection.cc index e8cd9ef..e8ebf36 100644 --- a/src/app/view_model_collection.cc +++ b/src/app/view_model_collection.cc @@ -18,6 +18,11 @@ #include "src/libstyles/resource_helper.h" #include "src/util/qml_message_interceptor.h" #include "src/util/usage_log_t.hpp" +#include "util-assert.h" + +#if !defined( Q_OS_ANDROID ) +# include "src/util/performance_counter.h" +#endif // !defined(Q_OS_ANDROID) namespace project { @@ -66,5 +71,15 @@ void ViewModelCollection::ExportContextPropertiesToQml( QQmlEngine* engine ) void ViewModelCollection::SetRootObject( QObject* object ) { m_eventFilter->FilterEventsDirectedAtThisObject( object ); + +#if !defined( Q_OS_ANDROID ) + if( m_opts->RunningGuiTests() ) + { + auto rootWin = qobject_cast( object ); + FASSERT( rootWin, "the casting must succeed so we get a QQuickWindow" ); + m_perfCounter = std::make_unique( nullptr, rootWin ); + m_perfCounter->StartReport( "GuiTests" ); + } +#endif // !defined(Q_OS_ANDROID) } } // namespace project diff --git a/src/app/view_model_collection.h b/src/app/view_model_collection.h index e148d7e..6ebec98 100644 --- a/src/app/view_model_collection.h +++ b/src/app/view_model_collection.h @@ -20,6 +20,7 @@ class CliOptions; class EventFilter; class GuiTests; class LoggingTags; +class PerformanceCounter; class QmlMessageInterceptor; class ViewModelCollection @@ -43,6 +44,10 @@ class ViewModelCollection std::unique_ptr m_qmlLogger; std::unique_ptr m_logging; +#if !defined( Q_OS_ANDROID ) + std::unique_ptr m_perfCounter; +#endif // !defined(Q_OS_ANDROID) + std::unique_ptr m_guiTests; }; } // namespace project diff --git a/src/util/performance_counter.cc b/src/util/performance_counter.cc new file mode 100644 index 0000000..6762f4c --- /dev/null +++ b/src/util/performance_counter.cc @@ -0,0 +1,198 @@ +#include "performance_counter.h" + +#include + +#include + +namespace project +{ +PerformanceCounter::PerformanceCounter( QObject* parent, QQuickWindow* window ) + : QObject( parent ), m_window( window ) +{ +} + +void PerformanceCounter::ExportContextPropertiesToQml( QQmlEngine* engine ) +{ + engine->rootContext()->setContextProperty( "performanceCounter", this ); +} + +void PerformanceCounter::ConnectToWindowIfNecessary() +{ + if( m_connectedToWindow ) + { + return; + } + connect( m_window, &QQuickWindow::afterAnimating, this, &PerformanceCounter::AfterAnimating ); + connect( m_window, &QQuickWindow::beforeRendering, this, &PerformanceCounter::BeforeRendering ); + connect( m_window, &QQuickWindow::afterRendering, this, &PerformanceCounter::AfterRendering ); +} + +void PerformanceCounter::StartReport( const QString& name ) +{ + for( auto& reportRequest : m_reports ) + { + if( reportRequest.m_name.isEmpty() ) + { + reportRequest = ReportRequest( name ); + return; + } + } +} + +void PerformanceCounter::AfterAnimating() +{ + for( auto& reportRequest : m_reports ) + { + reportRequest.Synchronize(); + if( reportRequest.m_report.has_value() ) + { + reportRequest.m_report->GetFrameReportToWriteInto().m_afterAnimating = static_cast( reportRequest.m_timer.elapsed() ); + } + } +} + +void PerformanceCounter::BeforeRendering() +{ + for( auto& reportRequest : m_reports ) + { + if( reportRequest.m_report.has_value() ) + { + reportRequest.m_report->GetFrameReportToWriteInto().m_beforeRendering = static_cast( reportRequest.m_timer.elapsed() ); + } + } +} + +void PerformanceCounter::AfterRendering() +{ + for( auto& reportRequest : m_reports ) + { + if( reportRequest.m_report.has_value() ) + { + reportRequest.m_report->GetFrameReportToWriteInto().m_beforeRendering = static_cast( reportRequest.m_timer.elapsed() ); + reportRequest.m_report->FinishFrame(); + if( reportRequest.m_requestedToStopRenderThread ) + { + reportRequest.Finalize(); + } + } + } +} + +ReportRequest::ReportRequest() + : m_name(), m_timer() +{ +} +ReportRequest::ReportRequest( QString name ) + : m_name( name ), m_timer() +{ + m_timer.start(); +} + +// Called automatically from the GUI thread while the render thread is waiting +void ReportRequest::Synchronize() +{ + if( m_finalized ) + { + *this = ReportRequest(); + } + else + { + if( ( m_name.data() != nullptr ) && !m_report.has_value() ) + { + m_report.emplace(); + } + m_requestedToStopRenderThread = m_requestedToStopGUIThread; + } +} + +// Called by users from GUI thread +void ReportRequest::RequestStop() +{ + m_requestedToStopGUIThread = true; +} + +// Called automatically from the render thread +void ReportRequest::Finalize() +{ + ExportReport(); + m_finalized = true; +} + +struct MinMax +{ + qint32 m_min = std::numeric_limits::max(); + qint32 m_max = std::numeric_limits::min(); + + void Update( qint32 val ) + { + m_min = std::min( m_min, val ); + m_max = std::max( m_max, val ); + } +}; + +void ReportRequest::ExportReport() +{ + auto log = qDebug(); + log = log << "Exporting report for: " << m_name << Qt::endl; + log = log << Qt::right << qSetFieldWidth( 3 ); + qint32 previousGuiUpdate = 0; + qint32 previousRenderFinish = 0; + + MinMax guiFrameDurationExtents; + MinMax renderFrameDurationExtents; + MinMax synchronizingExtents; + MinMax renderingExtents; + + for( const FrameReport& frame : m_report->m_backloggedFrames ) + { + qint32 guiFrameDuration = frame.m_afterAnimating - previousGuiUpdate; + previousGuiUpdate = frame.m_afterAnimating; + qint32 renderFrameDuration = frame.m_afterRendering - previousRenderFinish; + previousRenderFinish = frame.m_afterRendering; + qint32 synchronizing = frame.m_beforeRendering - frame.m_afterAnimating; + qint32 rendering = frame.m_afterRendering - frame.m_beforeRendering; + log << synchronizing << ", " << rendering << ", " << guiFrameDuration << ", " << renderFrameDuration; + guiFrameDurationExtents.Update( guiFrameDuration ); + renderFrameDurationExtents.Update( renderFrameDuration ); + synchronizingExtents.Update( synchronizing ); + renderingExtents.Update( rendering ); + } + for( size_t i = 0; i < m_report->m_framesFilledCount; i++ ) + { + const FrameReport& frame = m_report->m_frames[ i ]; + qint32 guiFrameDuration = frame.m_afterAnimating - previousGuiUpdate; + previousGuiUpdate = frame.m_afterAnimating; + qint32 renderFrameDuration = frame.m_afterRendering - previousRenderFinish; + previousRenderFinish = frame.m_afterRendering; + qint32 synchronizing = frame.m_beforeRendering - frame.m_afterAnimating; + qint32 rendering = frame.m_afterRendering - frame.m_beforeRendering; + log << synchronizing << ", " << rendering << ", " << guiFrameDuration << ", " << renderFrameDuration; + guiFrameDurationExtents.Update( guiFrameDuration ); + renderFrameDurationExtents.Update( renderFrameDuration ); + synchronizingExtents.Update( synchronizing ); + renderingExtents.Update( rendering ); + } + + log << "GUI Frame Duration Extents " << guiFrameDurationExtents.m_min << ", " << guiFrameDurationExtents.m_max; + log << "Render Frame Duration Extents " << renderFrameDurationExtents.m_min << ", " << renderFrameDurationExtents.m_max; + log << "Synchronizing Extents " << synchronizingExtents.m_min << ", " << synchronizingExtents.m_max; + log << "Rendering Extents " << renderingExtents.m_min << ", " << renderingExtents.m_max; +} + +FrameReport& Report::GetFrameReportToWriteInto() +{ + return m_frames[ m_framesFilledCount ]; +} + +void Report::FinishFrame() +{ + m_framesFilledCount++; + if( m_framesFilledCount == m_frames.size() ) + { + size_t currentBacklogSize = m_backloggedFrames.size(); + m_backloggedFrames.resize( currentBacklogSize + m_frames.size() ); + memcpy( m_frames.data(), m_backloggedFrames.data() + currentBacklogSize, sizeof( FrameReport ) * m_frames.size() ); + m_framesFilledCount = 0; + } +} +} // namespace project diff --git a/src/util/performance_counter.h b/src/util/performance_counter.h new file mode 100644 index 0000000..8997818 --- /dev/null +++ b/src/util/performance_counter.h @@ -0,0 +1,111 @@ +#ifndef PROJ_LIB_UTIL_PERFORMANCE_COUNTER_H +#define PROJ_LIB_UTIL_PERFORMANCE_COUNTER_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace project +{ +struct FrameReport +{ + qint32 m_afterAnimating; + qint32 m_beforeRendering; + qint32 m_afterRendering; +}; + +struct Report +{ + Report() = default; + Report& operator=( const Report& ) = default; + + // Gets the current frame that timing data should be written into. + FrameReport& GetFrameReportToWriteInto(); + // Moves the internal pointer to the next frame to write into, moving the cache into the heap-allocated backlog if space is needed. + void FinishFrame(); + + static constexpr size_t k_maxNumberOfFrames = 15; + + // The number of frame reports that have been fully filled, with all 3 timestamps having been written. + // This means that this is also the index of the FrameReport where new values should be written, + // and that this should be incremented whenever the last timestamp is written. + size_t m_framesFilledCount; + std::array m_frames; + std::vector m_backloggedFrames; +}; + +struct ReportRequest +{ + ReportRequest(); + explicit ReportRequest( QString name ); + + // Called automatically from the GUI thread while the render thread is waiting + void Synchronize(); + + // Called by users from GUI thread + void RequestStop(); + + // Called automatically from the render thread + void Finalize(); + + void ExportReport(); + + // Fully owned by the GUI thread + QString m_name; + // Written by the GUI thread before synchronization, read by the render thread after synchronization. + QElapsedTimer m_timer; + // Written by the GUI thread requesting that the report finish after the next render. + bool m_requestedToStopGUIThread = false; + // Copied based on m_requestedToStopGUIThread during synchronization. + bool m_requestedToStopRenderThread = false; + // Written to on the render thread once the frame that m_requestedToStopRenderThread was set in has finished. + // Once this is true, the report can be cleared out during the next synchronization. + bool m_finalized = false; + std::optional m_report; +}; + +// This object currently sits at 800 bytes, which means it should fit in a single page of memory. +class PerformanceCounter : public QObject +{ + Q_OBJECT + +public: + PerformanceCounter( QObject* parent, QQuickWindow* window ); + + void ExportContextPropertiesToQml( QQmlEngine* engine ); + + // All public methods must be called by the GUI thread + void StartReport( const QString& reportName ); + void StopReport( const QString& reportName ); + + static constexpr size_t k_maxNumberOfReports = 3; + +private slots: + // This happens on the GUI thread and can be used to synchronize between GUI and rendering thread resources. + void AfterAnimating(); + // This happens on the rendering thread + void BeforeRendering(); + // This happens on the rendering thread + void AfterRendering(); + +private: + // This must be called on the rendering thread + void AddReportToInternalStorage( Report report ); + void ConnectToWindowIfNecessary(); + + std::array m_reports; + + // Whether the private slots have been connected to the window's signals. + bool m_connectedToWindow = false; + QQuickWindow* m_window; +}; + +} // namespace project + +#endif // PROJ_LIB_UTIL_PERFORMANCE_COUNTER_H diff --git a/src/util/register_type.h b/src/util/register_type.h new file mode 100644 index 0000000..4ed33c8 --- /dev/null +++ b/src/util/register_type.h @@ -0,0 +1,71 @@ +#include +#include + +// Internal helper function that registers a type with modifier (like being wrapped in a vector). +// aliases is the list of aliased names the underlying class uses, and aliasModifierOrNone is optionally a function that will apply the modifier to the alias. +template +void registerModifiedType( + const std::vector& aliases, std::function aliasModifier = []( const std::string& s ) { return s; } ) +{ + if( aliases.size() == 0 ) + { + qRegisterMetaType(); + } + else + { + for( const std::string& alias : aliases ) + { + qRegisterMetaType( aliasModifier( alias ).c_str() ); + } + } +} + +// Aliases is all aliases that will be registered for the type (such as both type name with and without the namespace). +// If the type is not anonymous, the first alias will be used as the name. +// It is valid to register a type without aliases, but then it must be anonymous. +// Template arguments control which variants on a type (pointer, Qt smart pointer, etc.) are created, as well as const versions (with const-ness binding most tightly to the main type). +template +void registerType( const std::vector& aliases ) +{ + if constexpr( makeAnonymous ) + { + qmlRegisterAnonymousType( "App", 1 ); + } + else + { + qmlRegisterType( "App", 1, 0, aliases[ 0 ].c_str() ); + } + + if constexpr( registerValueType ) + { + registerModifiedType( aliases ); + if constexpr( registerConstVersions ) + { + registerModifiedType( aliases, []( const std::string& s ) { return "const " + s; } ); + } + } + if constexpr( registerPointer ) + { + registerModifiedType( aliases, []( const std::string& s ) { return s + "*"; } ); + if constexpr( registerConstVersions ) + { + registerModifiedType( aliases, []( const std::string& s ) { return "const " + s + "*"; } ); + } + } + if constexpr( registerSharedPointer ) + { + registerModifiedType>( aliases, []( const std::string& s ) { return "QSharedPointer<" + s + ">"; } ); + if constexpr( registerConstVersions ) + { + registerModifiedType>( aliases, []( const std::string& s ) { return "QSharedPointer"; } ); + } + } + if constexpr( registerVectorOfSharedPointers ) + { + registerModifiedType>>( aliases, []( const std::string& s ) { return "QVector>"; } ); + if constexpr( registerConstVersions ) + { + registerModifiedType>>( aliases, []( const std::string& s ) { return "QVector>"; } ); + } + } +} diff --git a/src/util/util.pro b/src/util/util.pro index ad46996..4d21767 100644 --- a/src/util/util.pro +++ b/src/util/util.pro @@ -1,6 +1,6 @@ !include($$top_srcdir/compiler_flags.pri) { error() } -QT += core +QT += core qml quick android { QT += androidextras @@ -30,5 +30,13 @@ HEADERS += \ qml_message_interceptor.h \ usage_log_t.hpp +!android { + SOURCES += \ + performance_counter.cc + + HEADERS += \ + performance_counter.h +} + target.path = $$top_exe_dir INSTALLS += target