Skip to content

Commit 300659d

Browse files
Search view: performance issues on remove and sort #3735
- Implemented a batch removal of search matches - Search matches are removed from their parent, instead of one by one which causes performance issues - It's also fixing the problem, that all elements are removed instead only the visible elements (while all others are filtered by the limit number) - Replaces the ConcurrentHashMap.newKeySet() with the ConcurrentSkipListSet with the comparator and makes the additional sorting obsolete. - After this change, avoid using `size()` on values, because it is not more constant time operation, see `ConcurrentSkipListSet` javadoc. - Also comparator must be changed to consider different Match elements with same size and offsets in the set - otherwise the `ConcurrentSkipListSet` would lost them. - Added `AbstractTextSearchResult.hasMatches(Object element)` API for cases where match count not needed. - If possible, calls to `size()` changed to `isEmpty()` or omitted. - Implemented Tests Fixes #3735 Co-authored-by: Andrey Loskutov <loskutov@gmx.de>
1 parent b8d7d92 commit 300659d

6 files changed

Lines changed: 372 additions & 20 deletions

File tree

bundles/org.eclipse.search/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: %pluginName
44
Bundle-SymbolicName: org.eclipse.search; singleton:=true
5-
Bundle-Version: 3.18.100.qualifier
5+
Bundle-Version: 3.19.0.qualifier
66
Bundle-Activator: org.eclipse.search.internal.ui.SearchPlugin
77
Bundle-ActivationPolicy: lazy
88
Bundle-Vendor: %providerName

bundles/org.eclipse.search/newsearch/org/eclipse/search/ui/text/AbstractTextSearchResult.java

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
package org.eclipse.search.ui.text;
1515

1616
import java.util.ArrayList;
17-
import java.util.Arrays;
1817
import java.util.Collection;
1918
import java.util.Collections;
2019
import java.util.Enumeration;
@@ -23,8 +22,10 @@
2322
import java.util.Set;
2423
import java.util.concurrent.ConcurrentHashMap;
2524
import java.util.concurrent.ConcurrentMap;
25+
import java.util.concurrent.ConcurrentSkipListSet;
2626
import java.util.concurrent.CopyOnWriteArrayList;
2727
import java.util.concurrent.atomic.AtomicInteger;
28+
import java.util.concurrent.atomic.AtomicLong;
2829

2930
import org.eclipse.search.ui.ISearchResult;
3031
import org.eclipse.search.ui.ISearchResultListener;
@@ -44,6 +45,8 @@ public abstract class AbstractTextSearchResult implements ISearchResult {
4445
private final ConcurrentMap<Object, Set<Match>> fElementsToMatches;
4546
private final List<ISearchResultListener> fListeners;
4647
private final AtomicInteger matchCount;
48+
private final ConcurrentHashMap<Match, Long> collisionOrder;
49+
private final AtomicLong collisionCounter;
4750

4851
private MatchFilter[] fMatchFilters;
4952

@@ -55,6 +58,8 @@ protected AbstractTextSearchResult() {
5558
fListeners = new CopyOnWriteArrayList<>();
5659
matchCount = new AtomicInteger(0);
5760
fMatchFilters= null; // filtering disabled by default
61+
collisionOrder = new ConcurrentHashMap<>();
62+
collisionCounter = new AtomicLong(0);
5863
}
5964

6065
/**
@@ -78,9 +83,7 @@ public Match[] getMatches(Object element) {
7883
}
7984
Set<Match> matches = fElementsToMatches.get(element);
8085
if (matches != null) {
81-
Match[] sortingCopy = matches.toArray(new Match[matches.size()]);
82-
Arrays.sort(sortingCopy, AbstractTextSearchResult::compare);
83-
return sortingCopy;
86+
return matches.toArray(new Match[0]);
8487
}
8588
return EMPTY_ARRAY;
8689
}
@@ -161,15 +164,36 @@ private MatchEvent getSearchResultEvent(Collection<Match> matches, int eventKind
161164
private boolean didAddMatch(Match match) {
162165
matchCount.set(0);
163166
updateFilterState(match);
164-
return fElementsToMatches.computeIfAbsent(match.getElement(), k -> ConcurrentHashMap.newKeySet()).add(match);
167+
return fElementsToMatches.computeIfAbsent(match.getElement(),
168+
k -> new ConcurrentSkipListSet<>(this::compare)).add(match);
165169
}
166170

167-
private static int compare(Match match2, Match match1) {
168-
int diff= match2.getOffset()-match1.getOffset();
171+
private int compare(Match match2, Match match1) {
172+
if (match1 == match2) {
173+
return 0;
174+
}
175+
int diff = Integer.compare(match2.getOffset(), match1.getOffset());
176+
if (diff != 0) {
177+
return diff;
178+
}
179+
diff = Integer.compare(match2.getLength(), match1.getLength());
169180
if (diff != 0) {
170181
return diff;
171182
}
172-
return match2.getLength()-match1.getLength();
183+
diff = Integer.compare(System.identityHashCode(match2), System.identityHashCode(match1));
184+
if (diff != 0) {
185+
return diff;
186+
}
187+
// Identity hash collision for two distinct objects: use a stable
188+
// tiebreaker so the set does not treat them as duplicates.
189+
return resolveCollision(match2, match1);
190+
}
191+
192+
@SuppressWarnings("boxing")
193+
private int resolveCollision(Match a, Match b) {
194+
long orderA = collisionOrder.computeIfAbsent(a, k -> collisionCounter.incrementAndGet());
195+
long orderB = collisionOrder.computeIfAbsent(b, k -> collisionCounter.incrementAndGet());
196+
return Long.compare(orderA, orderB);
173197
}
174198

175199
/**
@@ -185,6 +209,7 @@ public void removeAll() {
185209
private void doRemoveAll() {
186210
matchCount.set(0);
187211
fElementsToMatches.clear();
212+
collisionOrder.clear();
188213
}
189214

190215
/**
@@ -233,6 +258,9 @@ private boolean didRemoveMatch(Match match) {
233258
}
234259
return matches;
235260
});
261+
if (existed[0]) {
262+
collisionOrder.remove(match);
263+
}
236264
return existed[0];
237265
}
238266

@@ -333,6 +361,25 @@ public boolean hasMatches() {
333361
return false;
334362
}
335363

364+
/**
365+
* Returns whether the given element has any matches in this search result.
366+
*
367+
* @param element
368+
* the element to test for matches
369+
* @return {@code true} if the given element has at least one match
370+
* @since 3.19
371+
*/
372+
public boolean hasMatches(Object element) {
373+
if (element == null) {
374+
return false;
375+
}
376+
Set<Match> matches = fElementsToMatches.get(element);
377+
if (matches != null) {
378+
return !matches.isEmpty();
379+
}
380+
return false;
381+
}
382+
336383
/**
337384
* Returns the number of matches reported against a given element. This is
338385
* equivalent to calling <code>getMatches(element).length</code>
@@ -442,4 +489,28 @@ public MatchFilter[] getAllMatchFilters() {
442489
* @see IFileMatchAdapter
443490
*/
444491
public abstract IFileMatchAdapter getFileMatchAdapter();
492+
493+
/**
494+
* Batch removing of matches by using their enclosing elements.
495+
*
496+
* @param elements
497+
* the elements of which matches should be removed.
498+
* @since 3.19
499+
*/
500+
public void removeElements(Collection<?> elements) {
501+
matchCount.set(0);
502+
List<Match> removedMatches = new ArrayList<>();
503+
for (Object object : elements) {
504+
Set<Match> matches = fElementsToMatches.remove(object);
505+
if (matches != null) {
506+
removedMatches.addAll(matches);
507+
}
508+
}
509+
if (!removedMatches.isEmpty()) {
510+
if (!collisionOrder.isEmpty()) {
511+
removedMatches.forEach(collisionOrder::remove);
512+
}
513+
fireChange(getSearchResultEvent(removedMatches, MatchEvent.REMOVED));
514+
}
515+
}
445516
}

bundles/org.eclipse.search/newsearch/org/eclipse/search/ui/text/AbstractTextSearchViewPage.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Collection;
1919
import java.util.Collections;
2020
import java.util.HashSet;
21+
import java.util.List;
2122
import java.util.Objects;
2223
import java.util.Set;
2324
import java.util.concurrent.LinkedBlockingDeque;
@@ -41,7 +42,9 @@
4142
import org.eclipse.core.runtime.SafeRunner;
4243
import org.eclipse.core.runtime.Status;
4344

45+
import org.eclipse.core.resources.IContainer;
4446
import org.eclipse.core.resources.IFile;
47+
import org.eclipse.core.resources.IResource;
4548

4649
import org.eclipse.jface.action.Action;
4750
import org.eclipse.jface.action.IAction;
@@ -61,6 +64,7 @@
6164
import org.eclipse.jface.viewers.ITreeContentProvider;
6265
import org.eclipse.jface.viewers.OpenEvent;
6366
import org.eclipse.jface.viewers.SelectionChangedEvent;
67+
import org.eclipse.jface.viewers.StructuredSelection;
6468
import org.eclipse.jface.viewers.StructuredViewer;
6569
import org.eclipse.jface.viewers.TableViewer;
6670
import org.eclipse.jface.viewers.TreeViewer;
@@ -645,7 +649,7 @@ protected void postEnsureSelection() {
645649

646650
private void updateBusyLabel() {
647651
AbstractTextSearchResult result = getInput();
648-
boolean shouldShowBusy = result != null && NewSearchUI.isQueryRunning(result.getQuery()) && result.getMatchCount() == 0;
652+
boolean shouldShowBusy = result != null && NewSearchUI.isQueryRunning(result.getQuery()) && !result.hasMatches();
649653
if (shouldShowBusy == fIsBusyShown) {
650654
return;
651655
}
@@ -1387,6 +1391,10 @@ public void internalRemoveSelected() {
13871391
StructuredViewer viewer = getViewer();
13881392
IStructuredSelection selection = viewer.getStructuredSelection();
13891393

1394+
selection = handleRemovalOfResources(result, selection);
1395+
if (selection.isEmpty()) {
1396+
return;
1397+
}
13901398
HashSet<Match> set = new HashSet<>();
13911399
if (viewer instanceof TreeViewer) {
13921400
ITreeContentProvider cp = (ITreeContentProvider) viewer.getContentProvider();
@@ -1401,6 +1409,35 @@ public void internalRemoveSelected() {
14011409
result.removeMatches(matches);
14021410
}
14031411

1412+
private IStructuredSelection handleRemovalOfResources(AbstractTextSearchResult result,
1413+
IStructuredSelection selection) {
1414+
Set<IResource> elementsToRemove = new HashSet<>();
1415+
List<Object> others = new ArrayList<>();
1416+
Object[] resultElements = result.getElements();
1417+
for (Object selectedObj : selection) {
1418+
if (selectedObj instanceof IFile resource) {
1419+
elementsToRemove.add(resource);
1420+
} else if (selectedObj instanceof IContainer container) {
1421+
for (Object resElement : resultElements) {
1422+
if (resElement instanceof IResource res && container.getFullPath().isPrefixOf(res.getFullPath())) {
1423+
elementsToRemove.add(res);
1424+
}
1425+
}
1426+
} else {
1427+
others.add(selectedObj);
1428+
}
1429+
}
1430+
if (!elementsToRemove.isEmpty()) {
1431+
result.removeElements(elementsToRemove);
1432+
if (others.isEmpty()) {
1433+
navigateNext(true);
1434+
return StructuredSelection.EMPTY;
1435+
}
1436+
selection = new StructuredSelection(others);
1437+
}
1438+
return selection;
1439+
}
1440+
14041441
private void collectAllMatches(HashSet<Match> set, Object[] elements) {
14051442
for (Object element : elements) {
14061443
Match[] matches = getDisplayedMatches(element);

tests/org.eclipse.search.tests/META-INF/MANIFEST.MF

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: %pluginName
44
Bundle-SymbolicName: org.eclipse.search.tests;singleton:=true
5-
Bundle-Version: 3.12.0.qualifier
5+
Bundle-Version: 3.12.100.qualifier
66
Bundle-Vendor: %providerName
77
Bundle-Localization: plugin
88
Export-Package: org.eclipse.search.core.tests;x-internal:=true,
99
org.eclipse.search.tests;x-internal:=true,
1010
org.eclipse.search.tests.filesearch;x-internal:=true
1111
Require-Bundle:
1212
org.eclipse.ui;bundle-version="[3.208.0,4.0.0)",
13-
org.eclipse.ui.ide;bundle-version="[3.21.200,4.0.0)",
14-
org.eclipse.search;bundle-version="[3.16.0,4.0.0)",
15-
org.eclipse.core.runtime;bundle-version="[3.29.100,4.0.0)",
16-
org.eclipse.core.resources;bundle-version="[3.19.200,4.0.0)",
17-
org.eclipse.ui.workbench.texteditor;bundle-version="[3.17.200,4.0.0)",
18-
org.eclipse.jface.text;bundle-version="[3.24.200,4.0.0)",
19-
org.eclipse.ui.editors;bundle-version="[3.17.100,4.0.0)",
20-
org.eclipse.ltk.core.refactoring;bundle-version="[3.14.100,4.0.0)",
13+
org.eclipse.ui.ide;bundle-version="[3.23.0,4.0.0)",
14+
org.eclipse.search;bundle-version="[3.19.0,4.0.0)",
15+
org.eclipse.core.runtime;bundle-version="[3.34.200,4.0.0)",
16+
org.eclipse.core.resources;bundle-version="[3.23.200,4.0.0)",
17+
org.eclipse.ui.workbench.texteditor;bundle-version="[3.20.0,4.0.0)",
18+
org.eclipse.jface.text;bundle-version="[3.30.0,4.0.0)",
19+
org.eclipse.ui.editors;bundle-version="[3.21.0,4.0.0)",
20+
org.eclipse.ltk.core.refactoring;bundle-version="[3.15.100,4.0.0)",
2121
org.hamcrest;bundle-version="3.0.0"
2222
Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)",
2323
org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)",

0 commit comments

Comments
 (0)