blob: ceb3a779f596c4d785b4f1ff48082c49a66163f9 [file] [log] [blame]
/*
* Copyright 2000-2014 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.openapi.vfs.newvfs.impl;
import com.intellij.openapi.application.ApplicationAdapter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.vfs.InvalidVirtualFileAccessException;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartFMap;
import com.intellij.util.concurrency.AtomicFieldUpdater;
import com.intellij.util.containers.ConcurrentBitSet;
import com.intellij.util.containers.ConcurrentIntObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.StripedLockIntObjectConcurrentHashMap;
import com.intellij.util.keyFMap.KeyFMap;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import gnu.trove.THashSet;
import gnu.trove.TIntHashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry.ALL_FLAGS_MASK;
import static com.intellij.util.ObjectUtils.assertNotNull;
/**
* The place where all the data is stored for VFS parts loaded into a memory: name-ids, flags, user data, children.
*
* The purpose is to avoid holding this data in separate immortal file/directory objects because that involves space overhead, significant
* when there are hundreds of thousands of files.
*
* The data is stored per-id in blocks of {@link #SEGMENT_SIZE}. File ids in one project tend to cluster together,
* so the overhead for non-loaded id should not be large in most cases.
*
* File objects are still created if needed. There might be several objects for the same file, so equals() should be used instead of ==.
*
* The lifecycle of a file object is as follows:
*
* 1. The file has not been instantiated yet, so {@link #getFileById} returns null.
*
* 2. A file is explicitly requested by calling getChildren or findChild on its parent. The parent initializes all the necessary data (in a thread-safe context)
* and creates the file instance. See {@link #initFile}
*
* 3. After that the file is live, an object representing it can be retrieved any time from its parent. File system roots are
* kept on hard references in {@link com.intellij.openapi.vfs.newvfs.persistent.PersistentFS}
*
* 4. If a file is deleted (invalidated), then its data is not needed anymore, and should be removed. But this can only happen after
* all the listener have been notified about the file deletion and have had their chance to look at the data the last time. See {@link #killInvalidatedFiles()}
*
* 5. The file with removed data is marked as "dead" (see {@link #ourDeadMarker}, any access to it will throw {@link com.intellij.openapi.vfs.InvalidVirtualFileAccessException}
* Dead ids won't be reused in the same session of the IDE.
*
* @author peter
*/
public class VfsData {
private static final int SEGMENT_BITS = 9;
private static final int SEGMENT_SIZE = 1 << SEGMENT_BITS;
private static final int OFFSET_MASK = SEGMENT_SIZE - 1;
private static final Object ourDeadMarker = new String("dead file");
private static final ConcurrentIntObjectMap<Segment> ourSegments = new StripedLockIntObjectConcurrentHashMap<Segment>();
private static final ConcurrentBitSet ourInvalidatedIds = new ConcurrentBitSet();
private static TIntHashSet ourDyingIds = new TIntHashSet();
private static volatile SmartFMap<VirtualFileSystemEntry, VirtualDirectoryImpl> ourChangedParents = SmartFMap.emptyMap();
static {
ApplicationManager.getApplication().addApplicationListener(new ApplicationAdapter() {
@Override
public void writeActionFinished(Object action) {
// after top-level write action is finished, all the deletion listeners should have processed the deleted files
// and their data is considered safe to remove. From this point on accessing a removed file will result in an exception.
if (!ApplicationManager.getApplication().isWriteAccessAllowed()) {
killInvalidatedFiles();
}
}
});
}
private static void killInvalidatedFiles() {
synchronized (ourDeadMarker) {
if (!ourDyingIds.isEmpty()) {
for (int id : ourDyingIds.toArray()) {
assertNotNull(getSegment(id, false)).myObjectArray.set(getOffset(id), ourDeadMarker);
ourChangedParents = ourChangedParents.minus(new VirtualFileImpl(id, null, null));
}
ourDyingIds = new TIntHashSet();
}
}
}
@Nullable
public static VirtualFileSystemEntry getFileById(int id, VirtualDirectoryImpl parent) {
Segment segment = getSegment(id, false);
if (segment == null) return null;
int offset = getOffset(id);
Object o = segment.myObjectArray.get(offset);
if (o == null) return null;
if (o == ourDeadMarker) {
throw reportDeadFileAccess(new VirtualFileImpl(id, segment, parent));
}
assert segment.getNameId(id) > 0;
return o instanceof DirectoryData ? new VirtualDirectoryImpl(id, segment, (DirectoryData)o, parent, parent.getFileSystem())
: new VirtualFileImpl(id, segment, parent);
}
private static InvalidVirtualFileAccessException reportDeadFileAccess(VirtualFileSystemEntry file) {
return new InvalidVirtualFileAccessException("Accessing dead virtual file: " + file.getUrl());
}
private static int getOffset(int id) {
return id & OFFSET_MASK;
}
@Nullable @Contract("_,true->!null")
public static Segment getSegment(int id, boolean create) {
int key = id >>> SEGMENT_BITS;
Segment segment = ourSegments.get(key);
if (segment != null || !create) return segment;
return ourSegments.cacheOrGet(key, new Segment());
}
public static void initFile(int id, Segment segment, int nameId, @NotNull Object data) {
assert id > 0;
int offset = getOffset(id);
segment.setNameId(id, nameId);
if (segment.myObjectArray.get(offset) != null) {
throw new AssertionError("File already created");
}
segment.myObjectArray.set(offset, data);
}
static CharSequence getNameByFileId(int id) {
return FileNameCache.getVFileName(assertNotNull(getSegment(id, false)).getNameId(id));
}
static boolean isFileValid(int id) {
return !ourInvalidatedIds.get(id);
}
@Nullable
static VirtualDirectoryImpl getChangedParent(VirtualFileSystemEntry child) {
SmartFMap<VirtualFileSystemEntry, VirtualDirectoryImpl> map = ourChangedParents;
return map == (SmartFMap)SmartFMap.emptyMap() ? null : map.get(child);
}
static void changeParent(VirtualFileSystemEntry child, VirtualDirectoryImpl parent) {
synchronized (ourDeadMarker) {
ourChangedParents = ourChangedParents.plus(child, parent);
}
}
static void invalidateFile(int id) {
ourInvalidatedIds.set(id);
synchronized (ourDeadMarker) {
ourDyingIds.add(id);
}
}
public static class Segment {
// user data for files, DirectoryData for folders
final AtomicReferenceArray<Object> myObjectArray = new AtomicReferenceArray<Object>(SEGMENT_SIZE);
// <nameId, flags> pairs, "flags" part containing flags per se and modification stamp
private final AtomicIntegerArray myIntArray = new AtomicIntegerArray(SEGMENT_SIZE * 2);
int getNameId(int fileId) {
return myIntArray.get(getOffset(fileId) * 2);
}
void setNameId(int fileId, int nameId) {
myIntArray.set(getOffset(fileId) * 2, nameId);
}
void setUserMap(int fileId, KeyFMap map) {
myObjectArray.set(getOffset(fileId), map);
}
KeyFMap getUserMap(VirtualFileSystemEntry file) {
Object o = myObjectArray.get(getOffset(Math.abs(file.getId())));
if (!(o instanceof KeyFMap)) {
throw reportDeadFileAccess(file);
}
return (KeyFMap)o;
}
boolean changeUserMap(int fileId, KeyFMap oldMap, KeyFMap newMap) {
return myObjectArray.compareAndSet(getOffset(fileId), oldMap, newMap);
}
boolean getFlag(int id, int mask) {
assert (mask & ~ALL_FLAGS_MASK) == 0 : "Unexpected flag";
return (myIntArray.get(getOffset(id) * 2 + 1) & mask) != 0;
}
void setFlag(int id, int mask, boolean value) {
assert (mask & ~ALL_FLAGS_MASK) == 0 : "Unexpected flag";
int offset = getOffset(id) * 2 + 1;
while (true) {
int oldInt = myIntArray.get(offset);
int updated = value ? (oldInt | mask) : (oldInt & ~mask);
if (myIntArray.compareAndSet(offset, oldInt, updated)) {
return;
}
}
}
long getModificationStamp(int id) {
return myIntArray.get(getOffset(id) * 2 + 1) & ~ALL_FLAGS_MASK;
}
void setModificationStamp(int id, long stamp) {
int offset = getOffset(id) * 2 + 1;
while (true) {
int oldInt = myIntArray.get(offset);
int updated = (oldInt & ALL_FLAGS_MASK) | ((int)stamp & ~ALL_FLAGS_MASK);
if (myIntArray.compareAndSet(offset, oldInt, updated)) {
return;
}
}
}
}
// non-final field accesses are synchronized on this instance, but this happens in VirtualDirectoryImpl
public static class DirectoryData {
private static final AtomicFieldUpdater<DirectoryData, KeyFMap> updater = AtomicFieldUpdater.forFieldOfType(DirectoryData.class, KeyFMap.class);
volatile KeyFMap myUserMap = KeyFMap.EMPTY_MAP;
int[] myChildrenIds = ArrayUtil.EMPTY_INT_ARRAY;
private THashSet<String> myAdoptedNames;
VirtualFileSystemEntry[] getFileChildren(int fileId, VirtualDirectoryImpl parent) {
assert fileId > 0;
VirtualFileSystemEntry[] children = new VirtualFileSystemEntry[myChildrenIds.length];
for (int i = 0; i < myChildrenIds.length; i++) {
children[i] = assertNotNull(getFileById(myChildrenIds[i], parent));
}
return children;
}
boolean changeUserMap(KeyFMap oldMap, KeyFMap newMap) {
return updater.compareAndSet(this, oldMap, newMap);
}
boolean isAdoptedName(String name) {
return myAdoptedNames != null && myAdoptedNames.contains(name);
}
void removeAdoptedName(String name) {
if (myAdoptedNames != null) {
myAdoptedNames.remove(name);
if (myAdoptedNames.isEmpty()) {
myAdoptedNames = null;
}
}
}
void addAdoptedName(String name, boolean caseSensitive) {
if (myAdoptedNames == null) {
//noinspection unchecked
myAdoptedNames = new THashSet<String>(0, caseSensitive ? TObjectHashingStrategy.CANONICAL : CaseInsensitiveStringHashingStrategy.INSTANCE);
}
myAdoptedNames.add(name);
}
List<String> getAdoptedNames() {
return myAdoptedNames == null ? Collections.<String>emptyList() : ContainerUtil.newArrayList(myAdoptedNames);
}
}
}