From b8c086efa28748b6c87fe1de9f092433ac84fc87 Mon Sep 17 00:00:00 2001 From: alekseitveritinov Date: Fri, 3 Jul 2026 23:53:44 -0300 Subject: [PATCH 1/2] Fix Patch Manager search handle leak causing unbounded CPU growth updateStateAsync() only cancelled its datasource-lookup search on the match-found path; when no match was found the Search stayed in DB::m_searches forever. Since DB::updateSearches() rescans every entry in m_searches against every newly loaded patch on each bank load/patch change, leaked searches make every subsequent load do strictly more work, causing CPU usage to climb with the number of banks loaded/patches switched and never recover. --- source/jucePluginEditorLib/patchmanager/patchmanager.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/jucePluginEditorLib/patchmanager/patchmanager.cpp b/source/jucePluginEditorLib/patchmanager/patchmanager.cpp index 04b2c1f8a..18db0f1fd 100644 --- a/source/jucePluginEditorLib/patchmanager/patchmanager.cpp +++ b/source/jucePluginEditorLib/patchmanager/patchmanager.cpp @@ -644,7 +644,16 @@ namespace jucePluginEditorLib::patchManager results.assign(_search.results.begin(), _search.results.end()); if(results.empty()) + { + // the search found no match, but it must still be cancelled/removed from the DB's + // search list, otherwise it leaks forever and every subsequent bank load / patch + // update has to scan it in DB::updateSearches, causing CPU usage to grow unbounded + runOnUiThread([this, handle] + { + cancelSearch(handle); + }); return; + } if(results.size() > 1) { From f4f2581fde9cbfe33e734516cb9a6b7a6edc8ad2 Mon Sep 17 00:00:00 2001 From: alekseitveritinov Date: Sat, 4 Jul 2026 00:04:38 -0300 Subject: [PATCH 2/2] Fix DB::m_cancelledSearches unbounded growth cancelSearch() always inserted the handle into m_cancelledSearches, but that set is only ever pruned inside executeSearch's per-datasource cancellation check, which never runs for a search that has already completed. Since practically every Patch Manager UI interaction (tree/list rebuilds, filters, patch selection) calls cancelSearch() on an already-finished search, the handle was leaked permanently. Only track the handle as cancelled when the search hasn't completed yet, so it can still be picked up by the pending-cancellation check. --- source/jucePluginLib/patchdb/db.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source/jucePluginLib/patchdb/db.cpp b/source/jucePluginLib/patchdb/db.cpp index 4d520df1a..1a5211fce 100644 --- a/source/jucePluginLib/patchdb/db.cpp +++ b/source/jucePluginLib/patchdb/db.cpp @@ -480,7 +480,14 @@ namespace pluginLib::patchDB return; std::unique_lock lock(m_searchesMutex); - m_cancelledSearches.insert(_handle); + + // only remember the handle as cancelled if the search may still be picked up by executeSearch + // later (i.e. it's still queued/running). A search that has already completed will never be + // looked at again, so tracking it here would leak the handle in m_cancelledSearches forever. + const auto it = m_searches.find(_handle); + if(it != m_searches.end() && it->second->state != SearchState::Completed) + m_cancelledSearches.insert(_handle); + m_searches.erase(_handle); }