Skip to content

Fix Patch Manager search-handle leaks causing unbounded CPU growth - #275

Open
kartun83 wants to merge 2 commits into
dsp56300:mainfrom
kartun83:fix/patchmanager-search-leak-upstream
Open

Fix Patch Manager search-handle leaks causing unbounded CPU growth#275
kartun83 wants to merge 2 commits into
dsp56300:mainfrom
kartun83:fix/patchmanager-search-leak-upstream

Conversation

@kartun83

@kartun83 kartun83 commented Jul 4, 2026

Copy link
Copy Markdown

Summary

Two related leaks in the Patch Manager's search-handle bookkeeping (pluginLib::patchDB::DB) that cause plugin host CPU usage to climb after loading multiple banks / switching many patches, and never recover:

  • PatchManager::updateStateAsync() only called cancelSearch() on the "found a datasource match" path; the "no match found" early return skipped it, so the Search stayed in DB::m_searches forever. Since DB::updateSearches() rescans every entry in m_searches (including leaked ones) against every newly loaded patch on each subsequent bank load/patch change, leaked searches make every future load do strictly more work, with cost proportional to cumulative history rather than current state.
  • DB::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 — the common case for essentially every Patch Manager UI interaction (tree/list rebuilds, filtering, patch selection). This is a genuine unbounded memory leak, distinct in character from the first (insert/find/erase only, never iterated, so not itself a CPU driver).

Both were found while investigating Bitwig host CPU climbing to 150-170% and never dropping back after loading several banks and switching patches in Osirus/OsTIrus.

Changes

  • updateStateAsync(): dispatch cancelSearch(handle) on the empty-results path too, mirroring the already-correct success path.
  • cancelSearch(): only track a handle in m_cancelledSearches if the search hasn't completed yet, since a completed search will never be looked at again.

Test plan

  • Builds clean (jucePluginLib, jucePluginEditorLib targets, Xcode/macOS)
  • Reproduced the original symptom (Osirus/OsTIrus CLAP in Bitwig: load several banks, switch many patches, CPU climbs and doesn't recover) and confirmed CPU stays flat after this fix

kartun83 added 2 commits July 4, 2026 00:44
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant