7
mirror of https://gitlab.com/kicad/code/kicad.git synced 2025-04-20 21:11:43 +00:00

Thread git checks

Push the git checks from the main thread into child threads to prevent
resource contention in some cases where Windows machines might have many
files in git that are _also_ being managed by Google Drive

Fixes https://gitlab.com/kicad/code/kicad/-/issues/20078
This commit is contained in:
Seth Hillbrand 2025-02-25 16:04:53 -08:00
parent 42e448dbf6
commit 2c54ab277b
6 changed files with 229 additions and 99 deletions

View File

@ -126,6 +126,8 @@ static const wxChar MaximumThreads[] = wxT( "MaximumThreads" );
static const wxChar NetInspectorBulkUpdateOptimisationThreshold[] =
wxT( "NetInspectorBulkUpdateOptimisationThreshold" );
static const wxChar ExcludeFromSimulationLineWidth[] = wxT( "ExcludeFromSimulationLineWidth" );
static const wxChar GitIconRefreshInterval[] = wxT( "GitIconRefreshInterval" );
static const wxChar GitProjectStatusRefreshInterval[] = wxT( "GitProjectStatusRefreshInterval" );
} // namespace KEYS
@ -305,6 +307,9 @@ ADVANCED_CFG::ADVANCED_CFG()
m_ExcludeFromSimulationLineWidth = 25;
m_GitIconRefreshInterval = 1000;
m_GitProjectStatusRefreshInterval = 60000;
loadFromConfigFile();
}
@ -586,6 +591,14 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg )
&m_ExcludeFromSimulationLineWidth,
m_ExcludeFromSimulationLineWidth, 1, 100 ) );
configParams.push_back( new PARAM_CFG_INT( true, AC_KEYS::GitIconRefreshInterval,
&m_GitIconRefreshInterval,
m_GitIconRefreshInterval, 0, 100000 ) );
configParams.push_back( new PARAM_CFG_INT( true, AC_KEYS::GitProjectStatusRefreshInterval,
&m_GitProjectStatusRefreshInterval,
m_GitProjectStatusRefreshInterval, 0, 100000 ) );
// Special case for trace mask setting...we just grab them and set them immediately
// Because we even use wxLogTrace inside of advanced config
wxString traceMasks;

View File

@ -76,6 +76,7 @@ public:
GIT_STATUS_BEHIND, // File changed in remote repository but not in local
GIT_STATUS_AHEAD, // File changed in local repository but not in remote
GIT_STATUS_CONFLICTED,
GIT_STATUS_IGNORED,
GIT_STATUS_LAST
};

View File

@ -740,6 +740,23 @@ public:
*/
int m_ExcludeFromSimulationLineWidth;
/**
* The interval in milliseconds to refresh the git icons in the project tree.
*
* Setting name: "GitIconRefreshInterval"
* Default value: 2000
*/
int m_GitIconRefreshInterval;
/**
* The interval in milliseconds to refresh the project status by performing
* a git fetch on the remote project. Set to 0 to disable.
*
* Setting name: "GitProjectStatusRefreshInterval"
* Default value: 60000
*/
int m_GitProjectStatusRefreshInterval;
///@}
private:

View File

@ -114,6 +114,7 @@ void PROJECT_TREE::LoadIcons()
stateImages.push_back( KiBitmapBundle( BITMAPS::git_out_of_date ) ); // GIT_STATUS_BEHIND
stateImages.push_back( KiBitmapBundle( BITMAPS::git_changed_ahead ) ); // GIT_STATUS_AHEAD
stateImages.push_back( KiBitmapBundle( BITMAPS::git_conflict ) ); // GIT_STATUS_CONFLICTED
stateImages.push_back( wxBitmapBundle( wxBitmap( 16, 16 ) ) ); // GIT_STATUS_IGNORED
SetStateImages( stateImages );
#else
@ -190,14 +191,15 @@ void PROJECT_TREE::LoadIcons()
m_statusImageList = new wxImageList( size, size, true,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_LAST ) );
m_statusImageList->Add( blank_bitmap ); // GIT_STATUS_UNTRACKED
m_statusImageList->Add( KiBitmap( BITMAPS::git_good_check, size ) ); // GIT_STATUS_CURRENT
m_statusImageList->Add( KiBitmap( BITMAPS::git_modified, size ) ); // GIT_STATUS_MODIFIED
m_statusImageList->Add( KiBitmap( BITMAPS::git_add, size ) ); // GIT_STATUS_ADDED
m_statusImageList->Add( KiBitmap( BITMAPS::git_delete, size ) ); // GIT_STATUS_DELETED
m_statusImageList->Add( blank_bitmap ); // GIT_STATUS_UNTRACKED
m_statusImageList->Add( KiBitmap( BITMAPS::git_good_check, size ) ); // GIT_STATUS_CURRENT
m_statusImageList->Add( KiBitmap( BITMAPS::git_modified, size ) ); // GIT_STATUS_MODIFIED
m_statusImageList->Add( KiBitmap( BITMAPS::git_add, size ) ); // GIT_STATUS_ADDED
m_statusImageList->Add( KiBitmap( BITMAPS::git_delete, size ) ); // GIT_STATUS_DELETED
m_statusImageList->Add( KiBitmap( BITMAPS::git_out_of_date, size ) ); // GIT_STATUS_BEHIND
m_statusImageList->Add( KiBitmap( BITMAPS::git_changed_ahead, size ) ); // GIT_STATUS_AHEAD
m_statusImageList->Add( KiBitmap( BITMAPS::git_conflict, size ) ); // GIT_STATUS_CONFLICTED
m_statusImageList->Add( KiBitmap( BITMAPS::git_conflict, size ) ); // GIT_STATUS_CONFLICTED
m_statusImageList->Add( blank_bitmap ); // GIT_STATUS_IGNORED
SetStateImageList( m_statusImageList );
#endif

View File

@ -24,6 +24,7 @@
*/
#include <stack>
#include <thread>
#include <git2.h>
#include <wx/regex.h>
@ -31,6 +32,7 @@
#include <wx/string.h>
#include <wx/msgdlg.h>
#include <wx/textdlg.h>
#include <wx/timer.h>
#include <advanced_config.h>
#include <bitmaps.h>
@ -48,6 +50,7 @@
#include <project/project_local_settings.h>
#include <scoped_set_reset.h>
#include <string_utils.h>
#include <thread_pool.h>
#include <launch_ext.h>
#include <wx/dcclient.h>
#include <wx/settings.h>
@ -187,16 +190,19 @@ PROJECT_TREE_PANE::PROJECT_TREE_PANE( KICAD_MANAGER_FRAME* parent ) :
m_isRenaming = false;
m_selectedItem = nullptr;
m_watcherNeedReset = false;
m_lastGitStatusUpdate = wxDateTime::Now();
m_gitLastError = GIT_ERROR_NONE;
m_watcher = nullptr;
Connect( wxEVT_FSWATCHER,
wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
Bind( wxEVT_FSWATCHER,
wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
Bind( wxEVT_SYS_COLOUR_CHANGED,
wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
m_gitSyncTimer.SetOwner( this );
m_gitStatusTimer.SetOwner( this );
Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
/*
* Filtering is now inverted: the filters are actually used to _enable_ support
* for a given file type.
@ -208,15 +214,28 @@ PROJECT_TREE_PANE::PROJECT_TREE_PANE( KICAD_MANAGER_FRAME* parent ) :
ReCreateTreePrj();
// If we are asking for refresh, run one right at the beginning
if( ADVANCED_CFG::GetCfg().m_GitProjectStatusRefreshInterval > 0 )
m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
if( ADVANCED_CFG::GetCfg().m_GitIconRefreshInterval > 0 )
m_gitStatusTimer.Start( 150, wxTIMER_ONE_SHOT );
}
PROJECT_TREE_PANE::~PROJECT_TREE_PANE()
{
Disconnect( wxEVT_FSWATCHER,
wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
Unbind( wxEVT_FSWATCHER,
wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
Unbind( wxEVT_SYS_COLOUR_CHANGED,
wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
m_gitSyncTimer.Stop();
m_gitStatusTimer.Stop();
Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this,
m_gitSyncTimer.GetId() );
Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this,
m_gitStatusTimer.GetId() );
shutdownFileWatcher();
}
@ -605,6 +624,16 @@ wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
void PROJECT_TREE_PANE::ReCreateTreePrj()
{
std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
thread_pool& tp = GetKiCadThreadPool();
tp.wait_for_tasks();
m_gitStatusTimer.Stop();
m_gitSyncTimer.Stop();
m_gitTreeCache.clear();
m_gitStatusIcons.clear();
wxString pro_dir = m_Parent->GetProjectFileName();
if( !m_TreeProject )
@ -706,7 +735,13 @@ void PROJECT_TREE_PANE::ReCreateTreePrj()
// Sort filenames by alphabetic order
m_TreeProject->SortChildren( m_root );
updateGitStatusIcons();
CallAfter( [this] ()
{
updateTreeCache();
m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
m_gitStatusTimer.Start( 150, wxTIMER_ONE_SHOT );
} );
}
@ -1123,7 +1158,6 @@ void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
item->Activate( this );
}
// Inside this routine, we rate limit to once per 2 seconds
updateGitStatusIcons();
}
@ -1299,22 +1333,6 @@ void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
fn = fn.BeforeLast( '/' );
}
switch( event.GetChangeType() )
{
case wxFSW_EVENT_DELETE:
case wxFSW_EVENT_CREATE:
case wxFSW_EVENT_RENAME:
CallAfter( &PROJECT_TREE_PANE::updateGitStatusIcons );
break;
case wxFSW_EVENT_MODIFY:
CallAfter( &PROJECT_TREE_PANE::updateGitStatusIcons );
KI_FALLTHROUGH;
case wxFSW_EVENT_ACCESS:
default:
return;
}
wxTreeItemId root_id = findSubdirTreeItem( subdir );
if( !root_id.IsOk() )
@ -1944,31 +1962,25 @@ void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
void PROJECT_TREE_PANE::updateGitStatusIcons()
{
if( ADVANCED_CFG::GetCfg().m_EnableGit == false )
if( ADVANCED_CFG::GetCfg().m_EnableGit == false || !m_TreeProject )
return;
if( !m_TreeProject )
std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
if( !lock.owns_lock() )
return;
wxTimeSpan timeSinceLastUpdate = wxDateTime::Now() - m_lastGitStatusUpdate;
if( timeSinceLastUpdate.Abs() < wxTimeSpan::Seconds( 2 ) )
return;
m_lastGitStatusUpdate = wxDateTime::Now();
wxTreeItemId kid = m_TreeProject->GetRootItem();
if( !kid.IsOk() )
return;
// Note that this function is called from the idle event, so we need to be careful about
// accessing the tree control from a different thread.
for( auto&[ item, status ] : m_gitStatusIcons )
m_TreeProject->SetItemState( item, static_cast<int>( status ) );
wxTreeItemId kid = m_TreeProject->GetRootItem();
git_repository* repo = m_TreeProject->GetGitRepo();
if( !repo )
return;
// Get Current Branch
git_reference* currentBranchReference = nullptr;
int rc = git_repository_head( &currentBranchReference, repo );
@ -1993,40 +2005,81 @@ void PROJECT_TREE_PANE::updateGitStatusIcons()
else
{
if( giterr_last()->klass != m_gitLastError )
wxLogError( "Failed to lookup current branch: %s", giterr_last()->message );
wxLogTrace( "git", "Failed to lookup current branch: %s", giterr_last()->message );
m_gitLastError = giterr_last()->klass;
}
}
void PROJECT_TREE_PANE::updateTreeCache()
{
std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
if( !lock.owns_lock() || !m_TreeProject )
return;
wxTreeItemId kid = m_TreeProject->GetRootItem();
if( !kid.IsOk() )
return;
// Collect a map to easily set the state of each item
std::map<wxString, wxTreeItemId> branchMap;
std::stack<wxTreeItemId> items;
items.push( kid );
while( !items.empty() )
{
std::stack<wxTreeItemId> items;
items.push( kid );
kid = items.top();
items.pop();
while( !items.empty() )
{
kid = items.top();
items.pop();
PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
wxString gitAbsPath = nextItem->GetFileName();
wxString gitAbsPath = nextItem->GetFileName();
#ifdef _WIN32
gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
#endif
branchMap[gitAbsPath] = kid;
m_gitTreeCache[gitAbsPath] = kid;
wxTreeItemIdValue cookie;
wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
wxTreeItemIdValue cookie;
wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
while( child.IsOk() )
{
items.push( child );
child = m_TreeProject->GetNextChild( kid, cookie );
}
while( child.IsOk() )
{
items.push( child );
child = m_TreeProject->GetNextChild( kid, cookie );
}
}
}
void PROJECT_TREE_PANE::updateGitStatusIconMap()
{
if( ADVANCED_CFG::GetCfg().m_EnableGit == false || !m_TreeProject )
return;
int refresh = ADVANCED_CFG::GetCfg().m_GitIconRefreshInterval;
if( refresh != 0 )
{
CallAfter(
[this, refresh]()
{
m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
} );
}
std::unique_lock<std::mutex> lock1( m_gitStatusMutex );
std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex );
git_repository* repo = m_TreeProject->GetGitRepo();
if( !repo )
return;
// Get Current Branch
PROJECT_TREE_ITEM* rootItem = GetItemIdData( m_TreeProject->GetRootItem() );
wxFileName rootFilename( rootItem->GetFileName() );
wxString repoWorkDir( git_repository_workdir( repo ) );
wxFileName relative = rootFilename;
relative.MakeRelativeTo( repoWorkDir );
@ -2075,63 +2128,41 @@ void PROJECT_TREE_PANE::updateGitStatusIcons()
wxString absPath = repoWorkDir;
absPath << path;
auto iter = branchMap.find( absPath );
auto iter = m_gitTreeCache.find( absPath );
if( iter == branchMap.end() )
if( iter == m_gitTreeCache.end() )
continue;
// If we are current, don't continue because we still need to check to see if the
// current commit is ahead/behind the remote. If the file is modified/added/deleted,
// that is the main status we want to show.
if( entry->status == GIT_STATUS_CURRENT )
if( entry->status & GIT_STATUS_IGNORED )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) );
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED;
}
else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED;
}
else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED;
}
else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED;
}
// Check if file is up to date with the remote
if( localChanges.count( path ) )
else if( localChanges.count( path ) )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD;
}
else if( remoteChanges.count( path ) )
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND;
}
else
{
m_TreeProject->SetItemState(
iter->second,
static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) );
continue;
m_gitStatusIcons[iter->second] = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT;
}
}
@ -2525,3 +2556,42 @@ void PROJECT_TREE_PANE::onRunSelectedJobsFile(wxCommandEvent& event)
{
}
void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
{
if( ADVANCED_CFG::GetCfg().m_EnableGit == false || !m_TreeProject )
return;
thread_pool& tp = GetKiCadThreadPool();
tp.push_task( [this]()
{
KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
if( !gitCommon )
return;
GIT_PULL_HANDLER handler( gitCommon );
handler.PerformFetch();
} );
if( ADVANCED_CFG::GetCfg().m_GitProjectStatusRefreshInterval > 0 )
{
m_gitSyncTimer.Start( ADVANCED_CFG::GetCfg().m_GitProjectStatusRefreshInterval,
wxTIMER_ONE_SHOT );
}
}
void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
{
if( ADVANCED_CFG::GetCfg().m_EnableGit == false || !m_TreeProject )
return;
thread_pool& tp = GetKiCadThreadPool();
tp.push_task( [this]()
{
updateGitStatusIconMap();
} );
}

View File

@ -30,12 +30,17 @@
#ifndef TREEPRJ_FRAME_H
#define TREEPRJ_FRAME_H
#include <map>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <wx/datetime.h>
#include <wx/fswatcher.h>
#include <wx/laywin.h>
#include <wx/timer.h>
#include <wx/treebase.h>
#include <git/kicad_git_common.h>
#include "tree_file_type.h"
@ -230,6 +235,17 @@ private:
*/
void updateGitStatusIcons();
/**
* This is a threaded call that will change the map of git status icons for use in
* the main thread
*/
void updateGitStatusIconMap();
/**
* Updates the map of the wxtreeitemid to the name of each file for use in the thread
*/
void updateTreeCache();
/**
* Returns true if the current project has any uncommitted changes
*/
@ -283,6 +299,10 @@ private:
*/
bool canFileBeAddedToVCS( const wxString& aFilePath );
void onGitSyncTimer( wxTimerEvent& event );
void onGitStatusTimer( wxTimerEvent& event );
public:
KICAD_MANAGER_FRAME* m_Parent;
PROJECT_TREE* m_TreeProject;
@ -296,9 +316,16 @@ private:
bool m_watcherNeedReset; // true if FileWatcherReset() must be called
// (during an idle time for instance) after
// the main loop event handler is started
wxDateTime m_lastGitStatusUpdate;
int m_gitLastError;
wxTimer m_gitSyncTimer;
wxTimer m_gitStatusTimer;
std::mutex m_gitTreeCacheMutex;
std::unordered_map<wxString, wxTreeItemId> m_gitTreeCache;
std::mutex m_gitStatusMutex;
std::map<wxTreeItemId, KIGIT_COMMON::GIT_STATUS> m_gitStatusIcons;
DECLARE_EVENT_TABLE()
};