/*
* 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.util.indexing;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectAndLibrariesScope;
import com.intellij.psi.search.ProjectScopeImpl;
import com.intellij.util.Processor;
import com.intellij.util.SystemProperties;
import com.intellij.util.ThrowableRunnable;
import com.intellij.util.containers.ConcurrentIntObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.impl.MapIndexStorage;
import com.intellij.util.io.*;
import com.intellij.util.io.DataOutputStream;
import gnu.trove.TIntHashSet;
import gnu.trove.TIntProcedure;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
/**
* @author Eugene Zhuravlev
* Date: Dec 20, 2007
*/
public final class VfsAwareMapIndexStorage<Key, Value> extends MapIndexStorage<Key, Value> implements VfsAwareIndexStorage<Key, Value> {
private static final Logger LOG = Logger.getInstance("#com.intellij.util.indexing.impl.MapIndexStorage");
private static final boolean ENABLE_CACHED_HASH_IDS = SystemProperties.getBooleanProperty("idea.index.no.cashed.hashids", true);
private final boolean myBuildKeyHashToVirtualFileMapping;
private AppendableStorageBackedByResizableMappedFile myKeyHashToVirtualFileMapping;
private volatile int myLastScannedId;
private static final ConcurrentIntObjectMap<Boolean> ourInvalidatedSessionIds = ContainerUtil.createConcurrentIntObjectMap();
public VfsAwareMapIndexStorage(@NotNull File storageFile,
@NotNull KeyDescriptor<Key> keyDescriptor,
@NotNull DataExternalizer<Value> valueExternalizer,
final int cacheSize
) throws IOException {
this(storageFile, keyDescriptor, valueExternalizer, cacheSize, false, false);
}
public VfsAwareMapIndexStorage(@NotNull File storageFile,
@NotNull KeyDescriptor<Key> keyDescriptor,
@NotNull DataExternalizer<Value> valueExternalizer,
final int cacheSize,
boolean keyIsUniqueForIndexedFile,
boolean buildKeyHashToVirtualFileMapping) throws IOException {
super(storageFile, keyDescriptor, valueExternalizer, cacheSize, keyIsUniqueForIndexedFile, false);
myBuildKeyHashToVirtualFileMapping = buildKeyHashToVirtualFileMapping && FileBasedIndex.ourEnableTracingOfKeyHashToVirtualFileMapping;
initMapAndCache();
}
@Override
protected void initMapAndCache() throws IOException {
super.initMapAndCache();
myKeyHashToVirtualFileMapping = myBuildKeyHashToVirtualFileMapping ?
new AppendableStorageBackedByResizableMappedFile(getProjectFile(), 4096, null, PagedFileStorage.MB, true) : null;
}
@Override
protected void checkCanceled() {
ProgressManager.checkCanceled();
}
@NotNull
private File getProjectFile() {
return new File(myBaseStorageFile.getPath() + ".project");
}
private <T extends Throwable> void withLock(ThrowableRunnable<T> r) throws T {
myKeyHashToVirtualFileMapping.getPagedFileStorage().lock();
try {
r.run();
} finally {
myKeyHashToVirtualFileMapping.getPagedFileStorage().unlock();
}
}
@Override
public void flush() {
l.lock();
try {
super.flush();
if (myKeyHashToVirtualFileMapping != null && myKeyHashToVirtualFileMapping.isDirty()) {
withLock(() -> myKeyHashToVirtualFileMapping.force());
}
}
finally {
l.unlock();
}
}
@Override
public void close() throws StorageException {
super.close();
try {
if (myKeyHashToVirtualFileMapping != null) {
withLock(() -> myKeyHashToVirtualFileMapping.close());
}
}
catch (RuntimeException e) {
unwrapCauseAndRethrow(e);
}
}
@Override
public void clear() throws StorageException{
try {
if (myKeyHashToVirtualFileMapping != null) {
withLock(() -> myKeyHashToVirtualFileMapping.close());
}
}
catch (RuntimeException e) {
LOG.error(e);
}
try {
if (myKeyHashToVirtualFileMapping != null) IOUtil.deleteAllFilesStartingWith(getProjectFile());
}
catch (RuntimeException e) {
unwrapCauseAndRethrow(e);
}
super.clear();
}
@Override
public boolean processKeys(@NotNull final Processor<Key> processor, GlobalSearchScope scope, final IdFilter idFilter) throws StorageException {
l.lock();
try {
myCache.clear(); // this will ensure that all new keys are made into the map
if (myBuildKeyHashToVirtualFileMapping && idFilter != null) {
TIntHashSet hashMaskSet = null;
long l = System.currentTimeMillis();
File fileWithCaches = getSavedProjectFileValueIds(myLastScannedId, scope);
final boolean useCachedHashIds = ENABLE_CACHED_HASH_IDS &&
(scope instanceof ProjectScopeImpl || scope instanceof ProjectAndLibrariesScope) &&
fileWithCaches != null;
int id = myKeyHashToVirtualFileMapping.getCurrentLength();
if (useCachedHashIds && id == myLastScannedId) {
if (ourInvalidatedSessionIds.remove(id) == null) {
try {
hashMaskSet = loadHashedIds(fileWithCaches);
}
catch (IOException ignored) {
}
}
}
if (hashMaskSet == null) {
if (useCachedHashIds && myLastScannedId != 0) {
FileUtil.asyncDelete(fileWithCaches);
}
hashMaskSet = new TIntHashSet(1000);
final TIntHashSet finalHashMaskSet = hashMaskSet;
withLock(() -> {
myKeyHashToVirtualFileMapping.force();
ProgressManager.checkCanceled();
myKeyHashToVirtualFileMapping.processAll(key -> {
if (!idFilter.containsFileId(key[1])) return true;
finalHashMaskSet.add(key[0]);
ProgressManager.checkCanceled();
return true;
}, IntPairInArrayKeyDescriptor.INSTANCE);
});
if (useCachedHashIds) {
saveHashedIds(hashMaskSet, id, scope);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Scanned keyHashToVirtualFileMapping of " + myBaseStorageFile + " for " + (System.currentTimeMillis() - l));
}
final TIntHashSet finalHashMaskSet = hashMaskSet;
return myMap.processKeys(key -> {
if (!finalHashMaskSet.contains(myKeyDescriptor.getHashCode(key))) return true;
return processor.process(key);
});
}
return myMap.processKeys(processor);
}
catch (IOException e) {
throw new StorageException(e);
}
catch (RuntimeException e) {
return unwrapCauseAndRethrow(e);
}
finally {
l.unlock();
}
}
@NotNull
private static TIntHashSet loadHashedIds(@NotNull File fileWithCaches) throws IOException {
DataInputStream inputStream = null;
try {
inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(fileWithCaches)));
int capacity = DataInputOutputUtil.readINT(inputStream);
TIntHashSet hashMaskSet = new TIntHashSet(capacity);
while(capacity > 0) {
hashMaskSet.add(DataInputOutputUtil.readINT(inputStream));
--capacity;
}
inputStream.close();
return hashMaskSet;
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException ignored) {}
}
}
}
private void saveHashedIds(@NotNull TIntHashSet hashMaskSet, int largestId, @NotNull GlobalSearchScope scope) {
File newFileWithCaches = getSavedProjectFileValueIds(largestId, scope);
assert newFileWithCaches != null;
DataOutputStream stream = null;
boolean savedSuccessfully = false;
try {
stream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(newFileWithCaches)));
DataInputOutputUtil.writeINT(stream, hashMaskSet.size());
final DataOutputStream finalStream = stream;
savedSuccessfully = hashMaskSet.forEach(new TIntProcedure() {
@Override
public boolean execute(int value) {
try {
DataInputOutputUtil.writeINT(finalStream, value);
return true;
} catch (IOException ex) {
return false;
}
}
});
}
catch (IOException ignored) {
}
finally {
if (stream != null) {
try {
stream.close();
if (savedSuccessfully) myLastScannedId = largestId;
}
catch (IOException ignored) {}
}
}
}
private static volatile File mySessionDirectory;
private static File getSessionDir() {
File sessionDirectory = mySessionDirectory;
if (sessionDirectory == null) {
synchronized (VfsAwareMapIndexStorage.class) {
sessionDirectory = mySessionDirectory;
if (sessionDirectory == null) {
try {
mySessionDirectory = sessionDirectory = FileUtil.createTempDirectory(new File(PathManager.getTempPath()), Long.toString(System.currentTimeMillis()), "", true);
} catch (IOException ex) {
throw new RuntimeException("Can not create temp directory", ex);
}
}
}
}
return sessionDirectory;
}
@Nullable
private File getSavedProjectFileValueIds(int id, @NotNull GlobalSearchScope scope) {
Project project = scope.getProject();
if (project == null) return null;
return new File(getSessionDir(), getProjectFile().getName() + "." + project.hashCode() + "." + id + "." + scope.isSearchInLibraries());
}
@Override
public void addValue(final Key key, final int inputId, final Value value) throws StorageException {
try {
if (myKeyHashToVirtualFileMapping != null) {
withLock(() -> myKeyHashToVirtualFileMapping.append(new int[] { myKeyDescriptor.getHashCode(key), inputId }, IntPairInArrayKeyDescriptor.INSTANCE));
int lastScannedId = myLastScannedId;
if (lastScannedId != 0) { // we have write lock
ourInvalidatedSessionIds.cacheOrGet(lastScannedId, Boolean.TRUE);
myLastScannedId = 0;
}
}
super.addValue(key, inputId, value);
}
catch (IOException e) {
throw new StorageException(e);
}
}
private static class IntPairInArrayKeyDescriptor implements KeyDescriptor<int[]>, DifferentSerializableBytesImplyNonEqualityPolicy {
private static final IntPairInArrayKeyDescriptor INSTANCE = new IntPairInArrayKeyDescriptor();
@Override
public void save(@NotNull DataOutput out, int[] value) throws IOException {
DataInputOutputUtil.writeINT(out, value[0]);
DataInputOutputUtil.writeINT(out, value[1]);
}
@Override
public int[] read(@NotNull DataInput in) throws IOException {
return new int[] {DataInputOutputUtil.readINT(in), DataInputOutputUtil.readINT(in)};
}
@Override
public int getHashCode(int[] value) {
return value[0] * 31 + value[1];
}
@Override
public boolean isEqual(int[] val1, int[] val2) {
return val1[0] == val2[0] && val1[1] == val2[1];
}
}
}