/*
* Copyright 2000-2015 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.semantic;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.openapi.util.RecursionGuard;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.patterns.ElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.PsiManagerEx;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.NullableFunction;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ConcurrentIntObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.messages.MessageBusConnection;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author peter
*/
@SuppressWarnings({"unchecked"})
public class SemServiceImpl extends SemService{
private final ConcurrentMap<PsiElement, SemCacheChunk> myCache = ContainerUtil.createConcurrentWeakKeySoftValueMap();
private volatile MultiMap<SemKey, NullableFunction<PsiElement, ? extends SemElement>> myProducers;
private final Project myProject;
private boolean myBulkChange = false;
private final AtomicInteger myCreatingSem = new AtomicInteger(0);
public SemServiceImpl(Project project, PsiManager psiManager) {
myProject = project;
final MessageBusConnection connection = project.getMessageBus().connect();
connection.subscribe(PsiModificationTracker.TOPIC, new PsiModificationTracker.Listener() {
@Override
public void modificationCountChanged() {
if (!isInsideAtomicChange()) {
clearCache();
}
}
});
((PsiManagerEx)psiManager).registerRunnableToRunOnChange(() -> {
if (!isInsideAtomicChange()) {
clearCache();
}
});
LowMemoryWatcher.register(() -> {
if (myCreatingSem.get() == 0) {
clearCache();
}
//System.out.println("SemService cache flushed");
}, project);
}
private MultiMap<SemKey, NullableFunction<PsiElement, ? extends SemElement>> collectProducers() {
final MultiMap<SemKey, NullableFunction<PsiElement, ? extends SemElement>> map = MultiMap.createSmart();
final SemRegistrar registrar = new SemRegistrar() {
@Override
public <T extends SemElement, V extends PsiElement> void registerSemElementProvider(SemKey<T> key,
final ElementPattern<? extends V> place,
final NullableFunction<V, T> provider) {
map.putValue(key, element -> {
if (place.accepts(element)) {
return provider.fun((V)element);
}
return null;
});
}
};
for (SemContributorEP contributor : myProject.getExtensions(SemContributor.EP_NAME)) {
contributor.registerSemProviders(myProject.getPicoContainer(), registrar);
}
return map;
}
@Override
public void clearCache() {
myCache.clear();
}
@Override
public void performAtomicChange(@NotNull Runnable change) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
final boolean oldValue = myBulkChange;
myBulkChange = true;
try {
change.run();
}
finally {
myBulkChange = oldValue;
if (!oldValue) {
clearCache();
}
}
}
@Override
public boolean isInsideAtomicChange() {
return myBulkChange;
}
@Override
@Nullable
public <T extends SemElement> List<T> getSemElements(final SemKey<T> key, @NotNull final PsiElement psi) {
List<T> cached = _getCachedSemElements(key, true, psi);
if (cached != null) {
return cached;
}
ensureInitialized();
RecursionGuard.StackStamp stamp = RecursionManager.createGuard("semService").markStack();
LinkedHashSet<T> result = new LinkedHashSet<>();
final Map<SemKey, List<SemElement>> map = new THashMap<>();
for (final SemKey each : key.getInheritors()) {
List<SemElement> list = createSemElements(each, psi);
map.put(each, list);
result.addAll((List<T>)list);
}
if (stamp.mayCacheNow()) {
final SemCacheChunk persistent = getOrCreateChunk(psi);
for (SemKey semKey : map.keySet()) {
persistent.putSemElements(semKey, map.get(semKey));
}
}
return new ArrayList<>(result);
}
private void ensureInitialized() {
if (myProducers == null) {
myProducers = collectProducers();
}
}
@NotNull
private List<SemElement> createSemElements(SemKey key, PsiElement psi) {
List<SemElement> result = null;
final Collection<NullableFunction<PsiElement, ? extends SemElement>> producers = myProducers.get(key);
if (!producers.isEmpty()) {
for (final NullableFunction<PsiElement, ? extends SemElement> producer : producers) {
myCreatingSem.incrementAndGet();
try {
final SemElement element = producer.fun(psi);
if (element != null) {
if (result == null) result = new SmartList<>();
result.add(element);
}
}
finally {
myCreatingSem.decrementAndGet();
}
}
}
return result == null ? Collections.emptyList() : Collections.unmodifiableList(result);
}
@Override
@Nullable
public <T extends SemElement> List<T> getCachedSemElements(SemKey<T> key, @NotNull PsiElement psi) {
return _getCachedSemElements(key, false, psi);
}
@Nullable
private <T extends SemElement> List<T> _getCachedSemElements(SemKey<T> key, boolean paranoid, final PsiElement element) {
final SemCacheChunk chunk = obtainChunk(element);
if (chunk == null) return null;
List<T> singleList = null;
LinkedHashSet<T> result = null;
final List<SemKey> inheritors = key.getInheritors();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < inheritors.size(); i++) {
List<T> cached = (List<T>)chunk.getSemElements(inheritors.get(i));
if (cached == null && paranoid) {
return null;
}
if (cached != null && cached != Collections.<T>emptyList()) {
if (singleList == null) {
singleList = cached;
continue;
}
if (result == null) {
result = new LinkedHashSet<>(singleList);
}
result.addAll(cached);
}
}
if (result == null) {
if (singleList != null) {
return singleList;
}
return Collections.emptyList();
}
return new ArrayList<>(result);
}
@Nullable
private SemCacheChunk obtainChunk(@Nullable PsiElement root) {
return myCache.get(root);
}
@Override
public <T extends SemElement> void setCachedSemElement(SemKey<T> key, @NotNull PsiElement psi, @Nullable T semElement) {
getOrCreateChunk(psi).putSemElements(key, ContainerUtil.createMaybeSingletonList(semElement));
}
@Override
public void clearCachedSemElements(@NotNull PsiElement psi) {
myCache.remove(psi);
}
private SemCacheChunk getOrCreateChunk(final PsiElement element) {
SemCacheChunk chunk = obtainChunk(element);
if (chunk == null) {
chunk = ConcurrencyUtil.cacheOrGet(myCache, element, new SemCacheChunk());
}
return chunk;
}
private static class SemCacheChunk {
private final ConcurrentIntObjectMap<List<SemElement>> map = ContainerUtil.createConcurrentIntObjectMap();
public List<SemElement> getSemElements(SemKey<?> key) {
return map.get(key.getUniqueId());
}
public void putSemElements(SemKey<?> key, List<SemElement> elements) {
map.put(key.getUniqueId(), elements);
}
@Override
public int hashCode() {
return 0; // ConcurrentWeakKeySoftValueHashMap.SoftValue requires hashCode, and this is faster than identityHashCode
}
}
}