cache intermediate computation results thread-locally even if recursion prevention has occurred before (IDEA-70668)

This commit is contained in:
peter
2011-06-17 19:18:06 +02:00
parent f716213c21
commit d1e07155ab
6 changed files with 110 additions and 14 deletions
@@ -0,0 +1,5 @@
import static Foo.*;
class Foo implements <ref>Bar1, Bar2, Bar3, Bar4, Bar5, Bar6, Bar7, Bar8, Bar9, Bar10, Bar11, Bar12, Bar13, Bar14 {
}
@@ -0,0 +1,6 @@
import static Foo1.*;
import static Foo0.*;
import static Foo3.*;
import static Foo.*;
class Foo extends <ref>Bar {}
@@ -11,6 +11,7 @@ import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.ResolveTestCase;
public class ResolveClassTest extends ResolveTestCase {
@@ -154,6 +155,31 @@ public class ResolveClassTest extends ResolveTestCase {
assertNull(target);
}
public void testStaticImportInTheSameClass() throws Exception {
PsiReference ref = configure();
long start = System.currentTimeMillis();
assertNull(ref.resolve());
PlatformTestUtil.assertTiming("exponent?", 20000, System.currentTimeMillis() - start);
}
public void testStaticImportNetwork() throws Exception {
PsiReference ref = configure();
int count = 20;
String imports = "";
for (int i = 0; i < count; i++) {
imports += "import static Foo" + i + ".*;\n";
}
for (int i = 0; i < count; i++) {
createFile(myModule, "Foo" + i + ".java", imports + "class Foo" + i + " extends Bar1, Bar2, Bar3 {}");
}
long start = System.currentTimeMillis();
assertNull(ref.resolve());
PlatformTestUtil.assertTiming("exponent?", 20000, System.currentTimeMillis() - start);
}
@SuppressWarnings({"ConstantConditions"})
private void configureDependency() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@@ -120,7 +120,7 @@ public class ResolveCache {
};
RecursionGuard.StackStamp stamp = myGuard.markStack();
result = needToPreventRecursion ? myGuard.doPreventingRecursion(ref, computable) : computable.compute();
result = needToPreventRecursion ? myGuard.doPreventingRecursion(ref, true, computable) : computable.compute();
if (stamp.mayCacheNow()) {
cache(ref, result, maps, physical, incompleteCode, clearCountOnStart);
}
@@ -24,15 +24,28 @@ import java.util.List;
*
* @author peter
*/
public interface RecursionGuard {
public abstract class RecursionGuard {
/**
* See {@link #doPreventingRecursion(Object, boolean, Computable)} with memoization disabled
*/
@SuppressWarnings("JavaDoc")
@Deprecated
@Nullable
public <T> T doPreventingRecursion(Object key, Computable<T> computation) {
return doPreventingRecursion(key, false, computation);
}
/**
* @param key an id of the computation. Is stored internally to ensure that a recursive calls with the same key won't lead to endless recursion.
* @param memoize whether the result of the computation may me cached thread-locally until the last currently active doPreventingRecursion call
* completes. May be used to speedup things when recursion re-entrance happens: otherwise nothing would be cached at all and
* in some cases exponential performance may be observed.
* @param computation a piece of code to compute.
* @return the result of the computation or null if we're entering a computation with this key on this thread recursively,
*/
@Nullable
<T> T doPreventingRecursion(Object key, Computable<T> computation);
public abstract <T> T doPreventingRecursion(Object key, boolean memoize, Computable<T> computation);
/**
* Used in pair with {@link com.intellij.openapi.util.RecursionGuard.StackStamp#mayCacheNow()} to ensure that cached are only the reliable values,
@@ -51,12 +64,12 @@ public interface RecursionGuard {
* @return an object representing the current stack state, managed by {@link RecursionManager}
*/
StackStamp markStack();
public abstract StackStamp markStack();
/**
* @return the current thread-local stack of keys passed to {@link #doPreventingRecursion(Object, Computable)}
*/
List<Object> currentStack();
public abstract List<Object> currentStack();
/**
* Makes {@link com.intellij.openapi.util.RecursionGuard.StackStamp#mayCacheNow()} return false for all stamps created since a computation with
@@ -65,11 +78,13 @@ public interface RecursionGuard {
* Used to prevent caching of results that are non-reliable NOT due to recursion prevention: for example, too deep recursion
* ({@link #currentStack()} may help in determining the recursion depth)
*
* Also disables thread-local memoization (see the second parameter of {@link #doPreventingRecursion(Object, boolean, Computable)}.
*
* @param since the id of a computation whose result is safe to cache whilst for more nested ones it's not.
*/
void prohibitResultCaching(Object since);
public abstract void prohibitResultCaching(Object since);
interface StackStamp {
public interface StackStamp {
/**
* @return whether a computation that started at the moment of this {@link StackStamp} instance creation does not depend on any re-entrant recursive
@@ -15,9 +15,14 @@
*/
package com.intellij.openapi.util;
import com.intellij.reference.SoftReference;
import com.intellij.util.containers.SoftHashMap;
import org.jetbrains.annotations.NonNls;
import java.util.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* There are moments when a computation A requires the result of computation B, which in turn requires C, which (unexpectedly) requires A.
@@ -28,7 +33,7 @@ import java.util.*;
* incorrect to cache the results of B and C, because they all are based on the default incomplete result of the A calculation. If the actual
* computation sequence were C->A->B->C, the result of the outer C most probably wouldn't be the same as in A->B->C->A, where it depends on
* the null A result directly. The natural wish is that the program with cache enabled has the same results as the one without cache. In the above
* situation the result of C would depend on the order of invocations of C and A, which can be hardly predictable in multithreaded environments.
* situation the result of C would depend on the order of invocations of C and A, which can be hardly predictable in multi-threaded environments.
*
* Therefore if you use any kind of cache, it probably would make your program safer to cache only when it's safe to do this. See
* {@link com.intellij.openapi.util.RecursionGuard#markStack()} and {@link com.intellij.openapi.util.RecursionGuard.StackStamp#mayCacheNow()}
@@ -40,18 +45,31 @@ import java.util.*;
*/
@SuppressWarnings({"UtilityClassWithoutPrivateConstructor"})
public class RecursionManager {
private static final Object NULL = new Object();
private static final ThreadLocal<Integer> ourStamp = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
private static final ThreadLocal<Integer> ourMemoizationStamp = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
private static final ThreadLocal<LinkedHashMap<MyKey, Integer>> ourProgress = new ThreadLocal<LinkedHashMap<MyKey, Integer>>() {
@Override
protected LinkedHashMap<MyKey, Integer> initialValue() {
return new LinkedHashMap<MyKey, Integer>();
}
};
private static final ThreadLocal<Map<MyKey, SoftReference>> ourIntermediateCache = new ThreadLocal<Map<MyKey, SoftReference>>() {
@Override
protected Map<MyKey, SoftReference> initialValue() {
return new SoftHashMap<MyKey, SoftReference>();
}
};
/**
* @param id just some string to separate different recursion prevention policies from each other
@@ -60,23 +78,44 @@ public class RecursionManager {
public static RecursionGuard createGuard(@NonNls final String id) {
return new RecursionGuard() {
@Override
public <T> T doPreventingRecursion(Object key, Computable<T> computation) {
public <T> T doPreventingRecursion(Object key, boolean memoize, Computable<T> computation) {
MyKey realKey = new MyKey(id, key);
LinkedHashMap<MyKey, Integer> progressMap = ourProgress.get();
if (progressMap.containsKey(realKey)) {
//todo cache only here
prohibitResultCaching(key);
_prohibitResultCaching(key);
return null;
}
if (memoize) {
SoftReference reference = ourIntermediateCache.get().get(realKey);
if (reference != null) {
Object o = reference.get();
if (o != null) {
//noinspection unchecked
return o == NULL ? null : (T)o;
}
}
}
progressMap.put(realKey, ourStamp.get());
int startStamp = ourMemoizationStamp.get();
try {
return computation.compute();
T result = computation.compute();
if (memoize && ourMemoizationStamp.get() == startStamp) {
ourIntermediateCache.get().put(realKey, new SoftReference<Object>(result == null ? NULL : result));
}
return result;
}
finally {
ourStamp.set(progressMap.remove(realKey));
Integer value = progressMap.remove(realKey);
ourStamp.set(value);
if (value == 0) {
ourIntermediateCache.get().clear();
}
}
}
@@ -105,6 +144,10 @@ public class RecursionManager {
@Override
public void prohibitResultCaching(Object since) {
ourMemoizationStamp.set(_prohibitResultCaching(since));
}
private int _prohibitResultCaching(Object since) {
int stamp = ourStamp.get() + 1;
ourStamp.set(stamp);
@@ -117,6 +160,7 @@ public class RecursionManager {
inLoop = true;
}
}
return stamp;
}
};
}