rewritten to explicit classesProcessed sets instead of global class user data, extracted LazyConcurrentCollection into separate class

This commit is contained in:
Alexey Kudravtsev
2016-06-08 11:52:53 +03:00
parent 6225f7a4c8
commit 927d11c391
2 changed files with 250 additions and 178 deletions
@@ -21,7 +21,7 @@ import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
@@ -33,14 +33,12 @@ import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.Function;
import com.intellij.util.Processor;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.HashSetQueue;
import com.intellij.util.containers.Predicate;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ForkJoinPool;
public class JavaClassInheritorsSearcher extends QueryExecutorBase<PsiClass, ClassInheritorsSearch.SearchParameters> {
@Override
@@ -106,7 +104,22 @@ public class JavaClassInheritorsSearcher extends QueryExecutorBase<PsiClass, Cla
Iterable<PsiClass> cached = map.get(baseClass);
if (cached == null) {
// returns lazy collection of subclasses. Each call to next() leads to calculation of next batch of subclasses.
Iterable<PsiClass> computed = new AllSubClassesLazyCollection(project, baseClass); // it's empty now, no big deal
Function<PsiAnchor, PsiClass> converter =
anchor -> ApplicationManager.getApplication().runReadAction((Computable<PsiClass>)() -> (PsiClass)anchor.retrieve());
Predicate<PsiClass> applicableFilter =
candidate -> !(candidate instanceof PsiAnonymousClass) && candidate != null && !candidate.hasModifierProperty(PsiModifier.FINAL);
LazyConcurrentCollection.MoreElementsGenerator<PsiAnchor, PsiClass> generator = (candidate, processor) -> {
DirectClassInheritorsSearch.search(candidate, GlobalSearchScope.allScope(project)).forEach(subClass -> {
ProgressManager.checkCanceled();
PsiAnchor pointer = PsiAnchor.create(subClass);
// append found result to subClasses as early as possible to allow other waiting threads to continue
processor.consume(pointer);
return true;
});
};
PsiAnchor seed = ApplicationManager.getApplication().runReadAction((Computable<PsiAnchor>)() -> PsiAnchor.create(baseClass));
// lazy collection: store underlying queue as PsiAnchors, generate new elements by running direct inheritors
Iterable<PsiClass> computed = new LazyConcurrentCollection<>(seed, converter, applicableFilter, generator);
// for non-physical elements ignore the cache completely because non-physical elements created so often/unpredictably so I can't figure out when to clear caches in this case
boolean isPhysical = ApplicationManager.getApplication().runReadAction((Computable<Boolean>)baseClass::isPhysical);
// make sure concurrent calls of this method always return the same collection to avoid expensive duplicate work
@@ -183,176 +196,4 @@ public class JavaClassInheritorsSearcher extends QueryExecutorBase<PsiClass, Cla
private static boolean isFinal(@NotNull final PsiClass baseClass) {
return ApplicationManager.getApplication().runReadAction((Computable<Boolean>)() -> baseClass.hasModifierProperty(PsiModifier.FINAL));
}
private static class AllSubClassesLazyCollection implements Iterable<PsiClass> {
// Computes all sub classes of the 'baseClass' transitively by calling DirectClassInheritorsSearch repeatedly.
// Already computed subclasses are stored in this collection.
// There are two iterators maintained for this collection:
// - 'candidatesToFindSubclassesIterator' points to the next element for which direct inheritors haven't been searched yet.
// - 'subClassIterator' created in AllSubClassesLazyCollection.iterator() maintains state of the AllSubClassesLazyCollection iterator in a lazy fashion.
// If more elements requested for this iterator, the processMoreSubclasses() is called which tries to populate 'subClasses' with more inheritors.
private final HashSetQueue<PsiAnchor> subClasses = new HashSetQueue<>(); // guarded by lock
private final Object lock = new Object(); // MUST NOT acquire read action inside this lock
private final GlobalSearchScope projectScope;
private final Semaphore currentlyProcessingClasses = new Semaphore();
private final HashSetQueue.PositionalIterator<PsiAnchor> candidatesToFindSubclassesIterator = subClasses.iterator(); // guarded by lock
AllSubClassesLazyCollection(@NotNull Project project, @NotNull PsiClass baseClass) {
projectScope = GlobalSearchScope.allScope(project);
subClasses.add(ApplicationManager.getApplication().runReadAction((Computable<PsiAnchor>)() -> PsiAnchor.create(baseClass)));
}
@NotNull
@Override
public Iterator<PsiClass> iterator() {
return new Iterator<PsiClass>() {
private final Iterator<PsiAnchor> subClassIterator = subClasses.iterator(); // guarded by lock
{
synchronized (lock) {
subClassIterator.next(); //skip the baseClass which stored in the subClasses first element
}
}
@Override
public boolean hasNext() {
synchronized (lock) {
if (subClassIterator.hasNext()) return true;
}
processMoreSubclasses(subClassIterator);
synchronized (lock) {
return subClassIterator.hasNext();
}
}
@Override
public PsiClass next() {
PsiAnchor next;
synchronized (lock) {
next = subClassIterator.next();
}
return ApplicationManager.getApplication().runReadAction((Computable<PsiClass>)() -> (PsiClass)next.retrieve());
}
};
}
private PsiClass findNextClassInQueue(@NotNull HashSetQueue.PositionalIterator.IteratorPosition<PsiAnchor> position) {
// find the first class which is fit (not anonymous and not final and retrievable from PsiAnchor) and not already processed (flag PROCESSING_SUBCLASSES_STATUS in class user data)
PsiClass candidate = null;
boolean foundClassBeingProcessed = false;
// couldn't call iterator.next() until class is processed, so use position.peek()/position.next() which don't advance iterator
while (position != null) {
ProgressManager.checkCanceled();
PsiAnchor anchor = position.peek();
candidate = (PsiClass)anchor.retrieve();
if (candidate instanceof PsiAnonymousClass || candidate != null && candidate.hasModifierProperty(PsiModifier.FINAL)) {
candidate = null;
}
if (candidate != null) {
ClassProcessingStatus status = candidate.getUserData(PROCESSING_SUBCLASSES_STATUS);
if (status == null) {
candidate.putUserData(PROCESSING_SUBCLASSES_STATUS, ClassProcessingStatus.PROCESSING_SUBCLASSES);
break;
}
foundClassBeingProcessed |= status == ClassProcessingStatus.PROCESSING_SUBCLASSES;
}
if (!foundClassBeingProcessed) {
candidatesToFindSubclassesIterator.next(); // this class and all previous are either unfit (anonymous or final or un-retrievable) or already processed, skip iterator to help other threads
if (candidate != null) {
candidate.putUserData(PROCESSING_SUBCLASSES_STATUS, null); // this flag isn't needed anymore, free some memory
}
}
// the candidate is already being processed in the other thread, try the next one (not advancing iterator!)
candidate = null;
position = position.next();
}
return candidate;
}
enum ClassProcessingStatus {
PROCESSING_SUBCLASSES, PROCESSING_FINISHED
}
private static final Key<ClassProcessingStatus> PROCESSING_SUBCLASSES_STATUS = Key.create("PROCESSING_SUBCLASSES_STATUS");
// polls 'subClasses' for more sub classes and call DirectClassInheritorsSearch for them
private void processMoreSubclasses(@NotNull Iterator<PsiAnchor> subClassIterator) {
while (true) {
ProgressManager.checkCanceled();
PsiClass candidate = ApplicationManager.getApplication().runReadAction(new Computable<PsiClass>() {
@Override
public PsiClass compute() {
synchronized (lock) {
// Find the classes in subClasses collection to operate on
// (without advancing the candidatesToFindSubclassesIterator iterator - it will be moved after the class successfully handled - to protect against PCE, INRE, etc)
// The found class will be marked as being analyzed - with PROCESSING_SUBCLASSES_STATUS flag in its user data
HashSetQueue.PositionalIterator.IteratorPosition<PsiAnchor> startPosition = candidatesToFindSubclassesIterator.position().next();
PsiClass candidate = startPosition == null ? null : findNextClassInQueue(startPosition);
if (candidate != null) {
currentlyProcessingClasses.down();
}
return candidate;
}
}
});
if (candidate == null) {
// no candidates left in queue, exit
// but first, wait for other threads to process their candidates
break;
}
try {
DirectClassInheritorsSearch.search(candidate, projectScope).forEach(subClass -> {
ProgressManager.checkCanceled();
PsiAnchor pointer = PsiAnchor.create(subClass);
// append found result to subClasses as early as possible to allow other waiting threads to continue
synchronized (lock) {
subClasses.add(pointer);
}
return true;
});
}
finally {
candidate.putUserData(PROCESSING_SUBCLASSES_STATUS, ClassProcessingStatus.PROCESSING_FINISHED);
currentlyProcessingClasses.up();
}
synchronized (lock) {
if (subClassIterator.hasNext()) {
// we've added something to subClasses so we can return and the iterator can move forward at least once;
// more elements will be added on the subsequent call to .next()
return;
}
}
}
// Found nothing, have to wait for other threads because:
// The first thread comes and takes a class off the queue to search for inheritors,
// the second thread comes and sees there is no classes in the queue.
// The second thread should not return nothing, it should wait for the first thread to finish.
//
// Wait within managedBlock to signal FJP this thread is locked (to avoid thread starvation and deadlocks)
try {
ForkJoinPool.managedBlock(new ForkJoinPool.ManagedBlocker() {
@Override
public boolean block() throws InterruptedException {
currentlyProcessingClasses.waitFor(); // wait until other threads process their classes before giving up
return isReleasable();
}
@Override
public boolean isReleasable() {
synchronized (lock) {
return !currentlyProcessingClasses.isDown() || subClassIterator.hasNext();
}
}
});
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.psi.impl.search;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.HashSetQueue;
import com.intellij.util.containers.Predicate;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
/**
* Collection of elements of type V which is
* - lazy (computes its elements on demand, when the corresponding .iterator().next() method is called)
* - thread safe (multiple threads can iterate this collection concurrently. Already computed elements are shared between these threads. If more elements need to be computed, these computations happen concurrently; different threads can process different elements helping each other)
* - uses some other type T internally to store already computed elements, e.g. for reducing memory.
*
* When more elements needed, this collection iterates not yet processed elements, calls applicable filter on each, calls generator on applicable elements,
* adds generated elements back to the collection.
* Clients must provide:
* -- convertor T->V
* -- filter on V to use to find the applicable elements in the collection to compute more elements
* -- generator which for applicable element:V produces more elements:T
*/
class LazyConcurrentCollection<T,V> implements Iterable<V> {
// Computes all sub classes of the 'baseClass' transitively by calling DirectClassInheritorsSearch repeatedly.
// Already computed subclasses are stored in this collection.
// There are two iterators maintained for this collection:
// - 'candidatesToFindSubclassesIterator' points to the next element for which direct inheritors haven't been searched yet.
// - 'subClassIterator' created in AllSubClassesLazyCollection.iterator() maintains state of the AllSubClassesLazyCollection iterator in a lazy fashion.
// If more elements requested for this iterator, the processMoreSubclasses() is called which tries to populate 'subClasses' with more inheritors.
private final HashSetQueue<T> subClasses; // guarded by lock
private final Object lock = new Object(); // MUST NOT acquire read action inside this lock
@NotNull private final Function<T, V> myConvertor;
@NotNull private final MoreElementsGenerator<T,V> myGenerator;
@NotNull private final Predicate<? super V> myApplicableFilter;
private final Semaphore currentlyProcessingClasses = new Semaphore();
private final HashSetQueue.PositionalIterator<T> candidatesToFindSubclassesIterator; // guarded by lock
// classes for which DirectClassInheritorsSearch is running
private final Set<T> classesBeingProcessed = new THashSet<>(); // guarded by lock
// Classes for which DirectClassInheritorsSearch has already run (maybe in the other thread),
// but candidatesToFindSubclassesIterator hasn't caught them up yet. Elements from this set are removed as the iterator moves.
private final Set<T> classesProcessed = new THashSet<>(); // guarded by lock
LazyConcurrentCollection(@NotNull T seedElement,
@NotNull Function<T, V> convertor,
@NotNull Predicate<? super V> applicableFilter,
@NotNull MoreElementsGenerator<T, V> generator) {
subClasses = new HashSetQueue<>();
subClasses.add(seedElement);
myConvertor = convertor;
myGenerator = generator;
myApplicableFilter = applicableFilter;
candidatesToFindSubclassesIterator = subClasses.iterator();
}
@FunctionalInterface
interface MoreElementsGenerator<T,V> {
void generateMoreElementsFor(@NotNull V element, @NotNull Consumer<T> processor);
}
@NotNull
@Override
public Iterator<V> iterator() {
return new Iterator<V>() {
private final Iterator<T> subClassIterator = subClasses.iterator(); // guarded by lock
{
synchronized (lock) {
subClassIterator.next(); //skip the baseClass which stored in the subClasses first element
}
}
@Override
public boolean hasNext() {
synchronized (lock) {
if (subClassIterator.hasNext()) return true;
}
processMoreSubclasses(subClassIterator);
synchronized (lock) {
return subClassIterator.hasNext();
}
}
@Override
public V next() {
T next;
synchronized (lock) {
next = subClassIterator.next();
}
return myConvertor.fun(next);
}
};
}
private Pair.NonNull<T,V> findNextClassInQueue(@NotNull HashSetQueue.PositionalIterator.IteratorPosition<T> position) {
// find the first class which is fit (not anonymous and not final and retrievable from PsiAnchor) and not already processed (flag PROCESSING_SUBCLASSES_STATUS in class user data)
// couldn't call iterator.next() until class is processed, so use position.peek()/position.next() which don't advance iterator
while (position != null) {
ProgressManager.checkCanceled();
T anchor = position.peek();
V value = myConvertor.fun(anchor);
boolean isAccepted = value != null && myApplicableFilter.apply(value);
if (isAccepted && !classesProcessed.contains(anchor) && classesBeingProcessed.add(anchor)) {
return Pair.createNonNull(anchor, value);
}
// the candidate is already being processed in the other thread, try the next one (not advancing iterator!)
position = position.next();
}
return null;
}
// polls 'subClasses' for more sub classes and call DirectClassInheritorsSearch for them
private void processMoreSubclasses(@NotNull Iterator<T> subClassIterator) {
while (true) {
ProgressManager.checkCanceled();
Pair.NonNull<T,V> pair =
ApplicationManager.getApplication().runReadAction(new Computable<Pair.NonNull<T,V>>() {
@Override
public Pair.NonNull<T,V> compute() {
synchronized (lock) {
// Find the classes in subClasses collection to operate on
// (without advancing the candidatesToFindSubclassesIterator iterator - it will be moved after the class successfully handled - to protect against PCE, INRE, etc)
// The found class will be marked as being analyzed - placed in classesBeingProcessed collection
HashSetQueue.PositionalIterator.IteratorPosition<T> startPosition = candidatesToFindSubclassesIterator.position().next();
Pair.NonNull<T,V> pair = startPosition == null ? null : findNextClassInQueue(startPosition);
if (pair != null) {
currentlyProcessingClasses.down();
}
return pair;
}
}
});
if (pair == null) {
// no candidates left in queue, exit
// but first, wait for other threads to process their candidates
break;
}
V candidate = pair.getSecond();
T anchor = pair.getFirst();
try {
myGenerator.generateMoreElementsFor(candidate, generatedElement -> {
synchronized (lock) {
subClasses.add(generatedElement);
}
});
}
finally {
currentlyProcessingClasses.up();
}
synchronized (lock) {
classesBeingProcessed.remove(anchor);
classesProcessed.add(anchor);
advanceIteratorOnSuccess();
if (subClassIterator.hasNext()) {
// we've added something to subClasses so we can return and the iterator can move forward at least once;
// more elements will be added on the subsequent call to .next()
return;
}
}
}
// Found nothing, have to wait for other threads because:
// The first thread comes and takes a class off the queue to search for inheritors,
// the second thread comes and sees there is no classes in the queue.
// The second thread should not return nothing, it should wait for the first thread to finish.
//
// Wait within managedBlock to signal FJP this thread is locked (to avoid thread starvation and deadlocks)
try {
ForkJoinPool.managedBlock(new ForkJoinPool.ManagedBlocker() {
@Override
public boolean block() throws InterruptedException {
currentlyProcessingClasses.waitFor(); // wait until other threads process their classes before giving up
return isReleasable();
}
@Override
public boolean isReleasable() {
synchronized (lock) {
return !currentlyProcessingClasses.isDown() || subClassIterator.hasNext();
}
}
});
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void advanceIteratorOnSuccess() {
HashSetQueue.PositionalIterator.IteratorPosition<T> position = candidatesToFindSubclassesIterator.position().next();
while (position != null) {
T next = position.peek();
if (classesProcessed.contains(next)) {
candidatesToFindSubclassesIterator.next();
classesProcessed.remove(next);
}
else {
break;
}
position = position.next();
}
}
}