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

File search: don't re-evaluate match filters in the UI thread#4338
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

   858 files  ± 0     858 suites  ±0   50m 2s ⏱️ + 5m 27s
 8 326 tests +10   8 083 ✅ +10  243 💤 ±0  0 ❌ ±0 
20 847 runs  +30  20 177 ✅ +30  670 💤 ±0  0 ❌ ±0 

Results for commit 7746bbe. ± Comparison against base commit 37001ff.

♻️ 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

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.

Since the file handles are equal for all searches, a remembered state
must not outlive the search it was computed for: a change the listener
cannot detect, like a file added or removed below a linked folder, 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.

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.

🟡 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

Comment on lines +279 to +280
innerProject.close(null);
NewSearchUI.runQueryInForeground(null, query);
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