Add THROTTLE class and DrainPendingEvents

Five call sites independently reimplemented the same
time-gated throttle pattern.  This consolidates them all
into a single THROTTLE class backed by steady_clock.

Three call sites duplicated the wxWidgets YieldFor workaround
for timer-event backlog (wxWidgets issue #26192). This replaces them
with a shared DrainPendingEvents free function.
This commit is contained in:
Seth Hillbrand
2026-03-03 11:33:10 -08:00
parent 4836d172c7
commit fff428dcd6
14 changed files with 213 additions and 72 deletions
+6 -1
View File
@@ -24,6 +24,7 @@
#include <wx/version.h>
#include <wx/sizer.h>
#include <widgets/app_progress_dialog.h>
#include <widgets/wx_event_utils.h>
APP_PROGRESS_DIALOG::APP_PROGRESS_DIALOG( const wxString& aTitle, const wxString& aMessage,
int aMaximum, wxWindow* aParent,
@@ -54,5 +55,9 @@ bool APP_PROGRESS_DIALOG::Update( int aValue, const wxString& aNewMsg, bool* aSk
m_appProgressIndicator.SetValue( aValue );
}
return APP_PROGRESS_DIALOG_BASE::Update( aValue, aNewMsg, aSkip );
bool diag = APP_PROGRESS_DIALOG_BASE::Update( aValue, aNewMsg, aSkip );
DrainPendingEvents();
return diag;
}
+3 -7
View File
@@ -24,8 +24,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <wx/evtloop.h>
#include <thread>
#include <widgets/wx_event_utils.h>
#include <widgets/wx_progress_reporters.h>
@@ -96,10 +96,7 @@ bool WX_PROGRESS_REPORTER::updateUI()
// Returns false when cancelled (if it's a cancellable dialog)
bool diag = WX_PROGRESS_REPORTER_BASE::Update( cur, message );
// Prevent wx from queuing timer events and slowing down
// See https://github.com/wxWidgets/wxWidgets/issues/26192
if( wxEventLoopBase* loop = wxEventLoopBase::GetActive() )
loop->YieldFor( wxEVT_CATEGORY_TIMER );
DrainPendingEvents();
return diag;
}
@@ -122,8 +119,7 @@ bool GAUGE_PROGRESS_REPORTER::updateUI()
wxGauge::SetValue( cur );
if( wxEventLoopBase* loop = wxEventLoopBase::GetActive() )
loop->YieldFor( wxEVT_CATEGORY_UI );
DrainPendingEvents( wxEVT_CATEGORY_UI );
return true; // No cancel button on a wxGauge
}
+16 -5
View File
@@ -28,6 +28,7 @@
#include <base_screen.h>
#include <confirm.h>
#include <core/kicad_algo.h>
#include <core/throttle.h>
#include <eeschema_id.h>
#include <eeschema_settings.h>
#include <env_paths.h>
@@ -1348,18 +1349,28 @@ void SYMBOL_EDIT_FRAME::SyncLibraries( bool aShowProgress, bool aPreloadCancelle
APP_PROGRESS_DIALOG progressDlg( _( "Loading Symbol Libraries" ), wxEmptyString,
m_libMgr->GetAdapter()->GetLibrariesCount(), this );
THROTTLE progressThrottle( std::chrono::milliseconds( 350 ) );
int pendingProgress = 0;
bool callbackFired = false;
wxString pendingLibName;
m_libMgr->Sync( aForceRefresh,
[&]( int progress, int max, const wxString& libName )
{
progressDlg.Update( progress, wxString::Format( _( "Loading library '%s'..." ), libName ) );
pendingProgress = progress;
pendingLibName = libName;
callbackFired = true;
if( progressThrottle.Ready() )
progressDlg.Update( progress, wxString::Format( _( "Loading library '%s'..." ), libName ) );
} );
if( callbackFired )
progressDlg.Update( pendingProgress, wxString::Format( _( "Loading library '%s'..." ), pendingLibName ) );
}
else if( !aPreloadCancelled )
{
m_libMgr->Sync( aForceRefresh,
[&]( int progress, int max, const wxString& libName )
{
} );
m_libMgr->Sync( aForceRefresh, [&]( int progress, int max, const wxString& libName ) {} );
}
if( m_treePane )
-2
View File
@@ -37,8 +37,6 @@
#include <trace_helpers.h>
#include <libraries/symbol_library_adapter.h>
#define PROGRESS_INTERVAL_MILLIS 33 // 30 FPS refresh rate
wxObjectDataPtr<LIB_TREE_MODEL_ADAPTER>
SYMBOL_TREE_MODEL_ADAPTER::Create( SCH_BASE_FRAME* aParent, SYMBOL_LIBRARY_ADAPTER* aManager )
+4 -11
View File
@@ -27,6 +27,7 @@
#include <wx/settings.h>
#include <core/throttle.h>
#include <pgm_base.h>
#include <project/project_file.h>
#include <lib_symbol_library_manager.h>
@@ -70,12 +71,10 @@ bool SYMBOL_TREE_SYNCHRONIZING_ADAPTER::IsContainer( const wxDataViewItem& aItem
}
#define PROGRESS_INTERVAL_MILLIS 120
void SYMBOL_TREE_SYNCHRONIZING_ADAPTER::Sync( const wxString& aForceRefresh,
std::function<void( int, int, const wxString& )> aProgressCallback )
{
wxLongLong nextUpdate = wxGetUTCTimeMillis() + (PROGRESS_INTERVAL_MILLIS / 2);
THROTTLE progressThrottle( std::chrono::milliseconds( 120 ) );
m_lastSyncHash = m_libMgr->GetHash();
int i = 0, max = GetLibrariesCount();
@@ -87,11 +86,8 @@ void SYMBOL_TREE_SYNCHRONIZING_ADAPTER::Sync( const wxString& aForceRefresh,
{
const wxString& name = it->get()->m_Name;
if( wxGetUTCTimeMillis() > nextUpdate )
{
if( progressThrottle.Ready() )
aProgressCallback( i, max, name );
nextUpdate = wxGetUTCTimeMillis() + PROGRESS_INTERVAL_MILLIS;
}
// Check the table row directly rather than adapter->HasLibrary(), which requires the
// library to be fully loaded. After table reloads (e.g. adding a new library), all
@@ -137,11 +133,8 @@ void SYMBOL_TREE_SYNCHRONIZING_ADAPTER::Sync( const wxString& aForceRefresh,
if( m_libHashes.count( libName ) == 0 )
{
if( wxGetUTCTimeMillis() > nextUpdate )
{
if( progressThrottle.Ready() )
aProgressCallback( i++, max, libName );
nextUpdate = wxGetUTCTimeMillis() + PROGRESS_INTERVAL_MILLIS;
}
auto optRow = adapter->GetRow( libName );
wxCHECK2( optRow.has_value(), continue );
+64
View File
@@ -0,0 +1,64 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include <chrono>
/**
* Rate-limiter that fires at most once per interval.
*
* The first call to Ready() always returns true so callers can emit an
* initial update immediately.
*/
class THROTTLE
{
public:
explicit THROTTLE( std::chrono::milliseconds aInterval ) :
m_interval( aInterval ),
m_last( std::chrono::steady_clock::time_point::min() )
{
}
/**
* @return true when at least \a m_interval has elapsed since the previous
* true return (or since construction). Resets the internal clock
* on a true return.
*/
bool Ready()
{
auto now = std::chrono::steady_clock::now();
if( now - m_last >= m_interval )
{
m_last = now;
return true;
}
return false;
}
private:
std::chrono::milliseconds m_interval;
std::chrono::steady_clock::time_point m_last;
};
+44
View File
@@ -0,0 +1,44 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include <wx/evtloop.h>
/**
* Drain pending events of the given categories from the active event loop.
*
* During long-running operations that call progress-dialog Update() frequently,
* wxWidgets can accumulate a backlog of timer events that makes the UI sluggish
* after the operation completes. Calling this after each Update() prevents the
* backlog from building up.
*
* See https://github.com/wxWidgets/wxWidgets/issues/26192
*
* @param aCategories Event categories to yield for (default wxEVT_CATEGORY_TIMER).
*/
inline void DrainPendingEvents( long aCategories = wxEVT_CATEGORY_TIMER )
{
if( wxEventLoopBase* loop = wxEventLoopBase::GetActive() )
loop->YieldFor( aCategories );
}
+5 -14
View File
@@ -24,6 +24,7 @@
*/
#include <confirm.h>
#include <core/throttle.h>
#include <dialog_drc.h>
#include <board_design_settings.h>
#include <kiface_base.h>
@@ -81,8 +82,8 @@ DIALOG_DRC::DIALOG_DRC( PCB_EDIT_FRAME* aEditorFrame, wxWindow* aParent ) :
m_markersTreeModel( nullptr ),
m_unconnectedTreeModel( nullptr ),
m_fpWarningsTreeModel( nullptr ),
m_lastUpdateUi( std::chrono::steady_clock::now() ),
m_lastYieldUi( std::chrono::steady_clock::now() )
m_updateThrottle( std::chrono::milliseconds( 100 ) ),
m_yieldThrottle( std::chrono::milliseconds( 2000 ) )
{
SetName( DIALOG_DRC_WINDOW_NAME ); // Set a window name to be able to find it
KIPLATFORM::UI::SetFloatLevel( this );
@@ -228,27 +229,17 @@ bool DIALOG_DRC::updateUI()
m_gauge->SetValue( newValue );
}
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
// Repaint the dialog at ~10Hz using Update() which processes only pending expose/
// draw events without entering the full platform event loop.
if( std::chrono::duration_cast<std::chrono::milliseconds>( now - m_lastUpdateUi ).count()
> 100 )
{
if( m_updateThrottle.Ready() )
Update();
m_lastUpdateUi = now;
}
// Yield to the event loop infrequently so the cancel button remains functional.
// On some Linux systems with glycin-enabled gdk-pixbuf (2.44+), entering the GTK event
// loop triggers heavyweight sandbox process spawning that can add hundreds of milliseconds
// per call, so we keep this interval long.
if( std::chrono::duration_cast<std::chrono::milliseconds>( now - m_lastYieldUi ).count()
> 2000 )
{
if( m_yieldThrottle.Ready() )
Pgm().App().SafeYieldFor( this, wxEVT_CATEGORY_NATIVE_EVENTS );
m_lastYieldUi = now;
}
return !m_cancelled;
}
+3 -6
View File
@@ -26,7 +26,7 @@
#pragma once
#include <chrono>
#include <core/throttle.h>
#include <wx/htmllbox.h>
#include <rc_item.h>
#include <pcb_marker.h>
@@ -136,10 +136,7 @@ private:
RC_TREE_MODEL* m_unconnectedTreeModel; // wx reference-counted ptr
RC_TREE_MODEL* m_fpWarningsTreeModel; // wx reference-counted ptr
/// Used to throttle visual refresh in updateUi()
std::chrono::steady_clock::time_point m_lastUpdateUi;
/// Used to throttle event loop yields in updateUi()
std::chrono::steady_clock::time_point m_lastYieldUi;
THROTTLE m_updateThrottle;
THROTTLE m_yieldThrottle;
};
-2
View File
@@ -71,8 +71,6 @@ bool FP_TREE_SYNCHRONIZING_ADAPTER::IsContainer( const wxDataViewItem& aItem ) c
}
#define PROGRESS_INTERVAL_MILLIS 33 // 30 FPS refresh rate
void FP_TREE_SYNCHRONIZING_ADAPTER::Sync( FOOTPRINT_LIBRARY_ADAPTER* aLibs )
{
m_libs = aLibs;
+4 -12
View File
@@ -24,7 +24,6 @@
#include "allegro_builder.h"
#include <chrono>
#include <cmath>
#include <limits>
#include <set>
@@ -36,6 +35,7 @@
#include <wx/log.h>
#include <core/profile.h>
#include <core/throttle.h>
#include <base_units.h>
#include <board_design_settings.h>
@@ -4585,7 +4585,7 @@ bool BOARD_BUILDER::BuildBoard()
const LL_WALKER fpWalker( m_brdDb.m_Header->m_LL_0x2B, m_brdDb );
std::vector<BOARD_ITEM*> bulkAddedItems;
auto lastRefresh = std::chrono::steady_clock::now();
THROTTLE refreshThrottle( std::chrono::milliseconds( 100 ) );
for( const BLOCK_BASE* fpContainer : fpWalker )
{
@@ -4626,16 +4626,8 @@ bool BOARD_BUILDER::BuildBoard()
}
}
if( m_progressReporter )
{
auto now = std::chrono::steady_clock::now();
if( now - lastRefresh >= std::chrono::milliseconds( 100 ) )
{
m_progressReporter->KeepRefreshing();
lastRefresh = now;
}
}
if( m_progressReporter && refreshThrottle.Ready() )
m_progressReporter->KeepRefreshing();
}
}
}
@@ -25,7 +25,6 @@
#include "convert/allegro_parser.h"
#include <array>
#include <chrono>
#include <cstring>
#include <wx/sstream.h>
@@ -33,6 +32,7 @@
#include <wx/translation.h>
#include <core/profile.h>
#include <core/throttle.h>
#include <core/type_helpers.h>
#include <ki_exception.h>
@@ -2475,7 +2475,7 @@ void ALLEGRO::PARSER::readObjects( BRD_DB& aBoard )
BLOCK_PARSER blockParser( m_stream, ver, aBoard.m_Header->Get_0x27_End() );
auto lastRefresh = std::chrono::steady_clock::now();
THROTTLE refreshThrottle( std::chrono::milliseconds( 100 ) );
while( true )
{
@@ -2577,16 +2577,8 @@ void ALLEGRO::PARSER::readObjects( BRD_DB& aBoard )
{
m_progressReporter->AdvanceProgress();
if( ( aBoard.GetObjectCount() & 0x3F ) == 0 )
{
auto now = std::chrono::steady_clock::now();
if( now - lastRefresh >= std::chrono::milliseconds( 100 ) )
{
m_progressReporter->KeepRefreshing();
lastRefresh = now;
}
}
if( ( aBoard.GetObjectCount() & 0x3F ) == 0 && refreshThrottle.Ready() )
m_progressReporter->KeepRefreshing();
}
}
}
+1
View File
@@ -95,6 +95,7 @@ set( QA_COMMON_SRCS
io/pads/test_pads_common.cpp
io/pads/test_pads_unit_converter.cpp
test_throttle.cpp
view/test_zoom_controller.cpp
test_netname_validator.cpp
+59
View File
@@ -0,0 +1,59 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <core/throttle.h>
#include <thread>
BOOST_AUTO_TEST_SUITE( Throttle )
BOOST_AUTO_TEST_CASE( FirstCallAlwaysReady )
{
THROTTLE t( std::chrono::milliseconds( 500 ) );
BOOST_CHECK( t.Ready() );
}
BOOST_AUTO_TEST_CASE( SecondCallRejectsWithinInterval )
{
THROTTLE t( std::chrono::milliseconds( 200 ) );
BOOST_CHECK( t.Ready() );
BOOST_CHECK( !t.Ready() );
}
BOOST_AUTO_TEST_CASE( ReadyAgainAfterInterval )
{
THROTTLE t( std::chrono::milliseconds( 50 ) );
BOOST_CHECK( t.Ready() );
std::this_thread::sleep_for( std::chrono::milliseconds( 60 ) );
BOOST_CHECK( t.Ready() );
}
BOOST_AUTO_TEST_SUITE_END()