Skip to content

File search: don't re-evaluate match filters in the UI thread - #4338

Open
iloveeclipse wants to merge 1 commit into
eclipse-platform:masterfrom
iloveeclipse:ui_freeze_on_search
Open

iloveeclipse wants to merge 1 commit into
eclipse-platform:masterfrom
iloveeclipse:ui_freeze_on_search

Conversation

@iloveeclipse

@iloveeclipse iloveeclipse commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

File search: don't re-evaluate match filters in the UI thread

FileTreeContentProvider#elementsChanged(..) evaluated the active match filters again for every match of every updated file. The filter state of a match is however already computed once per match by AbstractTextSearchResult#didAddMatch(..) (and updated when the filters change) and is cached in Match#isFiltered(), which is also what initialize(..) and AbstractTextSearchViewPage#getDisplayedMatchCount(..)
use.

Re-evaluating the filters is not only redundant, it is expensive: OuterProjectFileFilter calls IWorkspaceRoot#findFilesForLocationURI(..), which iterates over all projects of the workspace. With a search
producing thousands of matches this ran for every match in the UI thread on every batched update and froze the UI. The provider now reads the already computed filter state instead.

The collection of the updated line elements was reworked as well: the matches of a file are enumerated only once, no matter how many lines of that file were updated, only the updated lines are remembered instead of
all lines of the touched files, and the enumeration stops as soon as all updated lines are known to have matches. Since line elements have identity semantics (LineElement doesn't implement equals(..)/hashCode(),
matches and updates refer to the very same instance), identity based sets are used.

OuterProjectFileFilter is still evaluated once per match by the search result, so it now remembers the filter state per file instead of repeating the workspace lookup for every match of that file. The states are kept in a weak map keyed by the file handles the matches hold, so they are collected together with the search result they were computed
for. The state doesn't depend on the filter instance, therefore the map and the resource change listener that invalidates it are static: at most one listener is registered, no matter how many filters are created.

The listener discards all remembered states if projects are added, removed, opened, closed or moved, and if the description file of a project has changed. Linked resources, resource filters and virtual folders change which files represent a location, but they are not reported by the flags of a project delta (the platform's own AliasManager uses internal lifecycle events for them); they are however stored in the project description, which is written whenever they change. Looking for that single file in the delta is much cheaper than visiting the whole delta in the notification thread, and much less disruptive than discarding the states for every added or removed file.

The listener is registered before the state of the workspace is read, and the registration is published only after it is done, so that a state is never computed and remembered while the changes that would invalidate
it are not reported yet. Outdated states are discarded by replacing the whole map: a state that is computed while the map is replaced is put into the replaced map and is therefore never seen again, so an invalidation cannot be lost.

Added tests for OuterProjectFileFilter (missed in the original eclipse-platform/eclipse.platform.text#144), including the invalidation of the remembered states, and a test that the
tree of the search view doesn't evaluate the match filters again.

Fixes #4337

Assisted-by: Github Copilot (Claude Opus 5)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cache publication can race with project-change invalidation and retain stale filter states.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Optimizes file-search filtering to prevent UI freezes caused by redundant match-filter evaluation.

Changes:

  • Reuses cached match filter state during tree updates.
  • Caches outer-project filter results with resource-change invalidation.
  • Adds nested-project filtering tests.
File summaries
File Description
FileTreeContentProvider.java Optimizes incremental tree updates.
OuterProjectFileFilter.java Adds per-file filter caching and invalidation.
NestedProjectFilterTest.java Tests nested-project filtering behavior.
AllFileSearchTests.java Registers the new test class.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   861 files  ± 0     861 suites  ±0   57m 12s ⏱️ + 2m 48s
 8 369 tests +12   8 127 ✅ +12  242 💤 ±0  0 ❌ ±0 
20 976 runs  +36  20 309 ✅ +36  667 💤 ±0  0 ❌ ±0 

Results for commit 23b7afc. ± Comparison against base commit 79a7d8e.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cache invalidation, listener lifecycle, and multi-file enumeration still have correctness and performance issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The cache can remain stale after linked-folder content changes, and its permanent workspace listener lacks lifecycle cleanup.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/OuterProjectFileFilter.java:95

  • Ignoring additions/removals below linked folders leaves this new static cache stale even after a search is rerun. New matches are recomputed, but filtering them still looks up the same equal IFile key in filterStates, so computeIsFiltered is skipped and the pre-change answer can keep showing a duplicate (or hiding the remaining representation). Invalidate these topology-changing deltas, or scope the cache to a search-result lifetime so a rerun starts with fresh states.
	 * Not reported is the creation or deletion of a file below a linked folder,
	 * although such a file can be represented by more than one project as well.
	 * Those changes are not reflected by the filter state cached per match by
	 * {@link org.eclipse.search.ui.text.AbstractTextSearchResult} either, the
	 * matches of the affected files are recomputed when the search is run again.
	 * Invalidating the states for every added or removed file would discard them
	 * whenever a build or a refresh touches the workspace, which would defeat the
	 * purpose of remembering them.
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Linked-folder child changes can leave static cached filter states stale across subsequent searches.

Review details

Suppressed comments (1)

bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/OuterProjectFileFilter.java:92

  • This exception leaves the static cache stale across later searches. If a file was cached before an add/remove below a linked folder changes the set returned by findFilesForLocationURI, rerunning the search still calls this filter with the same IFile key and receives the old map entry instead of recomputing it; a weak key does not help while an older search result still retains that file. Thus the comment's claimed recovery on rerun does not occur, and the wrong project representation can remain visible/hidden until some unrelated project-level invalidation. Please invalidate for these alias-changing deltas or scope/version the cache so a new search cannot reuse such entries.
	 * Not reported is the creation or deletion of a file below a linked folder,
	 * although such a file can be represented by more than one project as well.
	 * Those changes are not reflected by the filter state cached per match by
	 * {@link org.eclipse.search.ui.text.AbstractTextSearchResult} either, the
	 * matches of the affected files are recomputed when the search is run again.
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The workspace listener lacks lifecycle cleanup, and the cache-reset test is invalidated by a different mechanism.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/OuterProjectFileFilter.java:148

  • This listener is registered against the workspace for the lifetime of the JVM but is never removed when org.eclipse.search stops. Because the workspace keeps a strong reference to the lambda (and therefore this bundle's class loader), updating/restarting the bundle leaks the old bundle and can leave multiple listeners active. Register/unregister it with SearchPlugin's lifecycle (or add a shutdown hook called from SearchPlugin.stop) instead.
				ResourcesPlugin.getWorkspace().addResourceChangeListener(PROJECT_CHANGE_LISTENER,
						IResourceChangeEvent.POST_CHANGE);
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Concurrent filter changes can leave newly inserted matches with stale cached visibility state.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)

FileTreeContentProvider#elementsChanged(..) evaluated the active match
filters again for every match of every updated file. The filter state of
a match is however already computed once per match by
AbstractTextSearchResult#didAddMatch(..) (and updated when the filters
change) and is cached in Match#isFiltered(), which is also what
initialize(..) and AbstractTextSearchViewPage#getDisplayedMatchCount(..)
use.

Re-evaluating the filters is not only redundant, it is expensive:
OuterProjectFileFilter calls IWorkspaceRoot#findFilesForLocationURI(..),
which iterates over all projects of the workspace. With a search
producing thousands of matches this ran for every match in the UI thread
on every batched update and froze the UI. The provider now reads the
already computed filter state instead.

Relying on the computed filter state required fixing a race in
AbstractTextSearchResult: didAddMatch(..) evaluated the filters for a
new match before adding it, while setActiveMatchFilters(..) updates only
the matches that are already added. A match that was evaluated with the
previous filters but added after the filters had been changed kept an
outdated filter state. The re-evaluation in the UI thread masked that
for the updates of the tree, but not for its initialization, the table
or the displayed match counts.

Changing the filters and adding a match are now mutually exclusive. The
filters are changed while the write lock of a StampedLock is held, which
also increments a filter generation. Matches are added while its read
lock is held, so the search threads don't block each other. A match is
only added if the generation hasn't changed since its filter state was
evaluated, otherwise it is evaluated again with the new filters. The
(potentially expensive) filters are evaluated without holding the lock,
and no filter or other overridable method is called while it is held, so
subclasses synchronizing their accessors (like FileSearchResult) can't
deadlock with it. updateFilterStateForAllMatches() now reads the active
filters once instead of once per match.

The collection of the updated line elements was reworked as well: the
matches of a file are enumerated only once, no matter how many lines of
that file were updated, only the updated lines are remembered instead of
all lines of the touched files, and the enumeration stops as soon as all
updated lines are known to have matches. Since line elements have
identity semantics (LineElement doesn't implement equals(..)/hashCode(),
matches and updates refer to the very same instance), identity based
sets are used.

OuterProjectFileFilter is still evaluated once per match by the search
result, so it now remembers the filter state per file instead of
repeating the workspace lookup for every match of that file. The states
are kept in a weak map keyed by the file handles the matches hold, so
they are collected together with the search result they were computed
for. The state doesn't depend on the filter instance, therefore the map
and the resource change listener that invalidates it are static: at most
one listener is registered, no matter how many filters are created.

The listener discards all remembered states if projects are added,
removed, opened, closed or moved, and if the description file of a
project has changed. Linked resources, resource filters and virtual
folders change which files represent a location, but they are not
reported by the flags of a project delta (the platform's own
AliasManager uses internal lifecycle events for them); they are however
stored in the project description, which is written whenever they
change. Looking for that single file in the delta is much cheaper than
visiting the whole delta in the notification thread, and much less
disruptive than discarding the states for every added or removed file.

Since the file handles are equal for all searches, a remembered state
must not outlive the search it was computed for: changes the listener
cannot detect - a file added or removed below a linked folder, or a
resource turned hidden or team private, which IResource#setHidden(..)
and #setTeamPrivateMember(..) deliberately don't report in a delta -
would otherwise still be answered from the map when the search is run
again. FileSearchQuery#run(..) therefore discards the remembered states
when a search is started.

The listener is registered before the state of the workspace is read,
and the registration is published only after it is done, so that a state
is never computed and remembered while the changes that would invalidate
it are not reported yet. Outdated states are discarded by replacing the
whole map: a state that is computed while the map is replaced is put
into the replaced map and is therefore never seen again, so an
invalidation cannot be lost.

Added tests for OuterProjectFileFilter (missed in the original
eclipse-platform/eclipse.platform.text#144),
including the invalidation of the remembered states, and a test that the
tree of the search view doesn't evaluate the match filters again. The
test for the discarded states makes an undetectable change (a file
turned team private) and asserts that the stale state survives it, so
that it really verifies the discarding done by FileSearchQuery#run(..)
and not the resource change listener. MatchFilterStateTest
deterministically changes the filters while a match is being evaluated
and verifies that the added match gets the filter state of the new
filters, and that a match is evaluated only once if the filters don't
change.

Fixes eclipse-platform#4337

Assisted-by: Github Copilot (Claude Opus 5)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The static workspace listener must be removed during plug-in shutdown to prevent listener and class-loader leaks.

Review effort: Balanced
Findings: None

Resolved since last review (1)

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.

Search freezes UI if "Show only most nested match" filter is enabled

2 participants