VFS: add/delete many children in the same directory must not be quadratic

This commit is contained in:
Alexey Kudravtsev
2017-06-07 13:02:41 +03:00
parent e5eca56a78
commit c9773ffcd1
7 changed files with 417 additions and 91 deletions
@@ -162,6 +162,12 @@ public class TempFileSystem extends LocalFileSystemBase {
return fsItem.list();
}
@NotNull
@Override
public String getCanonicallyCasedName(@NotNull VirtualFile file) {
return file.getName();
}
@Override
public boolean isDirectory(@NotNull final VirtualFile file) {
return convert(file) instanceof FSDir;
@@ -26,17 +26,16 @@ import com.intellij.util.containers.ConcurrentBitSet;
import com.intellij.util.containers.ConcurrentIntObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.keyFMap.KeyFMap;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import com.intellij.util.text.CaseInsensitiveCharSequenceHashingStrategy;
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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -270,8 +269,8 @@ public class VfsData {
public static class DirectoryData {
private static final AtomicFieldUpdater<DirectoryData, KeyFMap> updater = AtomicFieldUpdater.forFieldOfType(DirectoryData.class, KeyFMap.class);
@NotNull volatile KeyFMap myUserMap = KeyFMap.EMPTY_MAP;
@NotNull int[] myChildrenIds = ArrayUtil.EMPTY_INT_ARRAY;
private Set<String> myAdoptedNames;
@NotNull int[] myChildrenIds = ArrayUtil.EMPTY_INT_ARRAY; // guarded by this
private Set<CharSequence> myAdoptedNames; // guarded by this
@NotNull
VirtualFileSystemEntry[] getFileChildren(int fileId, VirtualDirectoryImpl parent) {
@@ -287,11 +286,11 @@ public class VfsData {
return updater.compareAndSet(this, oldMap, newMap);
}
boolean isAdoptedName(String name) {
boolean isAdoptedName(CharSequence name) {
return myAdoptedNames != null && myAdoptedNames.contains(name);
}
void removeAdoptedName(String name) {
void removeAdoptedName(CharSequence name) {
if (myAdoptedNames != null) {
myAdoptedNames.remove(name);
if (myAdoptedNames.isEmpty()) {
@@ -299,16 +298,16 @@ public class VfsData {
}
}
}
void addAdoptedName(String name, boolean caseSensitive) {
void addAdoptedName(CharSequence name, boolean caseSensitive) {
if (myAdoptedNames == null) {
//noinspection unchecked
myAdoptedNames = new THashSet<>(0, caseSensitive ? TObjectHashingStrategy.CANONICAL : CaseInsensitiveStringHashingStrategy.INSTANCE);
myAdoptedNames = new THashSet<>(0, caseSensitive ? ContainerUtil.canonicalStrategy() : CaseInsensitiveCharSequenceHashingStrategy.INSTANCE);
}
myAdoptedNames.add(name);
}
List<String> getAdoptedNames() {
return myAdoptedNames == null ? Collections.emptyList() : ContainerUtil.newArrayList(myAdoptedNames);
@NotNull
Collection<CharSequence> getAdoptedNames() {
return myAdoptedNames == null ? Collections.emptyList() : myAdoptedNames;
}
void clearAdoptedNames() {
@@ -19,6 +19,7 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtilRt;
@@ -26,6 +27,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.InvalidVirtualFileAccessException;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWithId;
import com.intellij.openapi.vfs.impl.win32.Win32LocalFileSystem;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
@@ -36,9 +38,11 @@ import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
import com.intellij.openapi.vfs.newvfs.persistent.PersistentFSImpl;
import com.intellij.psi.impl.PsiCachedValue;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.UriUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.keyFMap.KeyFMap;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -46,15 +50,13 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
/**
* @author max
*/
public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl");
private static final Logger LOG = Logger.getInstance(VirtualDirectoryImpl.class);
private static final boolean CHECK = ApplicationManager.getApplication().isUnitTestMode();
@@ -90,8 +92,8 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
boolean doRefresh,
boolean ensureCanonicalName,
@NotNull NewVirtualFileSystem delegate) {
boolean ignoreCase = !delegate.isCaseSensitive();
VirtualFileSystemEntry result = doFindChild(name, ensureCanonicalName, delegate, ignoreCase);
boolean caseSensitive = delegate.isCaseSensitive();
VirtualFileSystemEntry result = doFindChild(name, ensureCanonicalName, delegate, caseSensitive);
//noinspection UseVirtualFileEquals
if (result == NULL_VIRTUAL_FILE) {
@@ -105,28 +107,28 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
return result;
}
private void addToAdoptedChildren(final boolean ignoreCase, @NotNull final String name) {
private void removeFromRealAndAddToAdopted(final boolean caseSensitive, @NotNull final String name) {
if (myData.isAdoptedName(name)) return; //already added
if (!allChildrenLoaded()) {
myData.addAdoptedName(name, getFileSystem().isCaseSensitive());
myData.addAdoptedName(name, caseSensitive);
}
int indexInReal = findIndex(myData.myChildrenIds, name, ignoreCase);
int indexInReal = findIndex(myData.myChildrenIds, name, caseSensitive);
if (indexInReal >= 0) {
// there suddenly can be that we ask to add name to adopted whereas it already contains in the real part
// there suddenly can be that we ask to add name to adopted whereas it already contained in the real part
// in this case we should remove it from there
removeFromArray(indexInReal);
}
assertConsistency(ignoreCase, name);
assertConsistency(caseSensitive, name);
}
@Nullable // null if there can't be a child with this name, NULL_VIRTUAL_FILE
private VirtualFileSystemEntry doFindChildInArray(@NotNull String name, boolean ignoreCase) {
private VirtualFileSystemEntry doFindChildInArray(@NotNull String name, boolean caseSensitive) {
synchronized (myData) {
if (myData.isAdoptedName(name)) return NULL_VIRTUAL_FILE;
int[] array = myData.myChildrenIds;
int indexInReal = findIndex(array, name, ignoreCase);
int indexInReal = findIndex(array, name, caseSensitive);
if (indexInReal >= 0) {
return VfsData.getFileById(array[indexInReal], this);
}
@@ -138,7 +140,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
private VirtualFileSystemEntry doFindChild(@NotNull String name,
boolean ensureCanonicalName,
@NotNull NewVirtualFileSystem delegate,
boolean ignoreCase) {
boolean caseSensitive) {
if (name.isEmpty()) {
return null;
}
@@ -146,7 +148,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
throw new InvalidVirtualFileAccessException(this);
}
VirtualFileSystemEntry found = doFindChildInArray(name, ignoreCase);
VirtualFileSystemEntry found = doFindChildInArray(name, caseSensitive);
if (found != null) return found;
if (ensureCanonicalName) {
@@ -154,7 +156,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
if (trimmedName.indexOf('/') != -1) return null; // name must not contain slashes in the middle
if (trimmedName.isEmpty()) return null;
if (!trimmedName.equals(name)) {
found = doFindChildInArray(trimmedName, ignoreCase);
found = doFindChildInArray(trimmedName, caseSensitive);
if (found != null) return found;
name = trimmedName;
}
@@ -176,7 +178,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
if (myData.isAdoptedName(name)) return NULL_VIRTUAL_FILE;
int[] array = myData.myChildrenIds;
int indexInReal = findIndex(array, name, ignoreCase);
int indexInReal = findIndex(array, name, caseSensitive);
// double check
if (indexInReal >= 0) {
return VfsData.getFileById(array[indexInReal], this);
@@ -188,13 +190,13 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
// do not extract getId outside the synchronized block since it will cause a concurrency problem.
int id = ourPersistence.getId(this, name, delegate);
if (id <= 0) {
myData.addAdoptedName(name, !ignoreCase);
myData.addAdoptedName(name, caseSensitive);
return null;
}
child = createChild(FileNameCache.storeName(name), id, delegate);
insertChildAt(child, indexInReal);
assertConsistency(!delegate.isCaseSensitive(), name);
assertConsistency(caseSensitive, name);
}
if (!child.isDirectory()) {
@@ -271,8 +273,8 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
@Override
@Nullable
public NewVirtualFile findChildIfCached(@NotNull String name) {
final boolean ignoreCase = !getFileSystem().isCaseSensitive();
VirtualFileSystemEntry found = doFindChildInArray(name, ignoreCase);
final boolean caseSensitive = getFileSystem().isCaseSensitive();
VirtualFileSystemEntry found = doFindChildInArray(name, caseSensitive);
//noinspection UseVirtualFileEquals
return found == NULL_VIRTUAL_FILE ? null : found;
}
@@ -301,10 +303,10 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
throw new InvalidVirtualFileAccessException(this);
}
NewVirtualFileSystem delegate = getFileSystem();
final boolean ignoreCase = !delegate.isCaseSensitive();
final boolean caseSensitive = delegate.isCaseSensitive();
synchronized (myData) {
if (allChildrenLoaded()) {
assertConsistency(ignoreCase, "");
assertConsistency(caseSensitive, "");
return getArraySafely();
}
@@ -318,10 +320,10 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
Arrays.sort(childrenIds, (o1, o2) -> {
CharSequence name1 = o1.name;
CharSequence name2 = o2.name;
int cmp = compareNames(name1, name2, ignoreCase);
int cmp = compareNames(name1, name2, caseSensitive);
if (cmp == 0 && name1 != name2) {
LOG.error(ourPersistence + " returned duplicate file names(" + name1 + "," + name2 + ")" +
" ignoreCase: " + ignoreCase +
" caseSensitive: " + caseSensitive +
" SystemInfo.isFileSystemCaseSensitive: " + SystemInfo.isFileSystemCaseSensitive +
" SystemInfo.OS: " + SystemInfo.OS_NAME + " " + SystemInfo.OS_VERSION +
" wasChildrenLoaded: " + wasChildrenLoaded +
@@ -351,7 +353,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
if (getId() > 0) {
myData.myChildrenIds = result;
if (CHECK) {
assertConsistency(ignoreCase, Arrays.asList(childrenIds));
assertConsistency(caseSensitive, Arrays.asList(childrenIds));
}
setChildrenLoaded();
}
@@ -360,7 +362,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
}
}
private void assertConsistency(boolean ignoreCase, @NotNull Object details) {
private void assertConsistency(boolean caseSensitive, @NotNull Object details) {
if (!CHECK || ApplicationInfoImpl.isInStressTest()) return;
int[] childrenIds = myData.myChildrenIds;
for (int i = 1; i < childrenIds.length; i++) {
@@ -368,7 +370,7 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
int prev = childrenIds[i - 1];
CharSequence name = VfsData.getNameByFileId(id);
CharSequence prevName = VfsData.getNameByFileId(prev);
int cmp = compareNames(name, prevName, ignoreCase);
int cmp = compareNames(name, prevName, caseSensitive);
if (cmp <= 0) {
error(verboseToString(VfsData.getFileById(prev, this)) +
" is wrongly placed before " +
@@ -420,18 +422,56 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
throw new IOException("Cannot get content of directory: " + this);
}
// optimisation: works faster than added.forEach(this::addChild)
public void addChildren(@NotNull List<Pair<VirtualFile,CharSequence>> added) {
if (added.size()<=3) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < added.size(); i++) {
VirtualFile file = added.get(i).getFirst();
addChild((VirtualFileSystemEntry)file);
}
return;
}
// merge sorted added and existing lists just like in merge sort
final boolean caseSensitive = getFileSystem().isCaseSensitive();
Comparator<Pair<VirtualFile, CharSequence>> pairComparator = (p1, p2) -> compareNames(p1.getSecond(), p2.getSecond(), caseSensitive);
added.sort(pairComparator);
TIntArrayList mergedIds = new TIntArrayList(myData.myChildrenIds.length + added.size());
synchronized (myData) {
ContainerUtil.processSortedListsInOrder(added, new AbstractList<Pair<VirtualFile,CharSequence>>() {
@Override
public Pair<VirtualFile,CharSequence> get(int index) {
VirtualFileSystemEntry file = VfsData.getFileById(myData.myChildrenIds[index], VirtualDirectoryImpl.this);
return Pair.create(file, file.getNameSequence());
}
@Override
public int size() {
return myData.myChildrenIds.length;
}
}, pairComparator, true, pair -> {
mergedIds.add(((VirtualFileWithId)pair.getFirst()).getId());
return true;
});
myData.myChildrenIds = mergedIds.toNativeArray();
((PersistentFSImpl)PersistentFS.getInstance()).incStructuralModificationCount();
assertConsistency(caseSensitive, added);
}
}
public void addChild(@NotNull VirtualFileSystemEntry child) {
final String childName = child.getName();
final boolean ignoreCase = !getFileSystem().isCaseSensitive();
final boolean caseSensitive = getFileSystem().isCaseSensitive();
synchronized (myData) {
int indexInReal = findIndex(myData.myChildrenIds, childName, ignoreCase);
int indexInReal = findIndex(myData.myChildrenIds, childName, caseSensitive);
myData.removeAdoptedName(childName);
if (indexInReal < 0) {
insertChildAt(child, indexInReal);
}
// else already stored
assertConsistency(ignoreCase, child);
assertConsistency(caseSensitive, child);
}
}
@@ -448,14 +488,79 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
}
public void removeChild(@NotNull VirtualFile file) {
boolean ignoreCase = !getFileSystem().isCaseSensitive();
boolean caseSensitive = getFileSystem().isCaseSensitive();
String name = file.getName();
synchronized (myData) {
addToAdoptedChildren(ignoreCase, name);
assertConsistency(ignoreCase, file);
removeFromRealAndAddToAdopted(caseSensitive, name);
assertConsistency(caseSensitive, file);
}
}
// faster than names.forEach(this::removeChild)
public void removeChildren(@NotNull List<CharSequence> names) {
boolean caseSensitive = getFileSystem().isCaseSensitive();
synchronized (myData) {
Comparator<CharSequence> comparator = (n1, n2) -> compareNames(n1, n2, caseSensitive);
names.sort(comparator);
boolean allChildrenLoaded = allChildrenLoaded();
if (!allChildrenLoaded) {
for (CharSequence name : names) {
myData.addAdoptedName(name, caseSensitive);
}
}
// remove from array by merging two sorted lists
int[] newIds = new int[myData.myChildrenIds.length];
int length = processSortedListsAndRemoveFirstFromTheSecond(names, myData.myChildrenIds, comparator, newIds);
if (length != newIds.length) {
newIds = length == 0 ? ArrayUtil.EMPTY_INT_ARRAY : Arrays.copyOf(newIds, length);
}
myData.myChildrenIds = newIds;
((PersistentFSImpl)PersistentFS.getInstance()).incStructuralModificationCount();
assertConsistency(caseSensitive, names);
}
}
// returns output length
private static int processSortedListsAndRemoveFirstFromTheSecond(@NotNull List<CharSequence> namesToRemove,
@NotNull int[] ids,
@NotNull Comparator<CharSequence> comparator,
@NotNull int[] output) {
int index1 = 0;
int index2 = 0;
int o = 0;
while (index2 < ids.length) {
int e;
if (index1 >= namesToRemove.size()) {
e = ids[index2++];
}
else {
CharSequence nameToRemove = namesToRemove.get(index1);
int id = ids[index2];
CharSequence element2 = VfsData.getNameByFileId(id);
int c = comparator.compare(nameToRemove, element2);
if (c == 0) {
// remove
index2++;
continue;
}
if (c < 0) {
// next name to remove
index1++;
continue;
}
e = id;
index2++;
}
output[o++] = e;
}
return o;
}
private void removeFromArray(int index) {
myData.myChildrenIds = ArrayUtil.remove(myData.myChildrenIds, index);
((PersistentFSImpl)PersistentFS.getInstance()).incStructuralModificationCount();
@@ -472,18 +577,20 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
@NotNull
public List<String> getSuspiciousNames() {
synchronized (myData) {
return myData.getAdoptedNames();
Collection<CharSequence> names = myData.getAdoptedNames();
//noinspection unchecked
return ContainerUtil.map(names, Function.TO_STRING);
}
}
@SuppressWarnings("Duplicates")
private static int findIndex(final int[] array, @NotNull CharSequence name, boolean ignoreCase) {
private static int findIndex(final int[] array, @NotNull CharSequence name, boolean caseSensitive) {
int low = 0;
int high = array.length - 1;
while (low <= high) {
int mid = low + high >>> 1;
int cmp = -compareNames(VfsData.getNameByFileId(array[mid]), name, ignoreCase);
int cmp = compareNames(name, VfsData.getNameByFileId(array[mid]), caseSensitive);
if (cmp > 0) low = mid + 1;
else if (cmp < 0) high = mid - 1;
else return mid;
@@ -492,12 +599,12 @@ public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
return -(low + 1);
}
private static int compareNames(@NotNull CharSequence name1, @NotNull CharSequence name2, boolean ignoreCase) {
private static int compareNames(@NotNull CharSequence name1, @NotNull CharSequence name2, boolean caseSensitive) {
int d = name1.length() - name2.length();
if (d != 0) return d;
for (int i = 0; i < name1.length(); i++) {
// com.intellij.openapi.util.text.StringUtil.compare(String,String,boolean) inconsistent
d = StringUtil.compare(name1.charAt(i), name2.charAt(i), ignoreCase);
d = StringUtil.compare(name1.charAt(i), name2.charAt(i), !caseSensitive);
if (d != 0) return d;
}
return 0;
@@ -53,7 +53,7 @@ import java.util.Collections;
public class VirtualFileImpl extends VirtualFileSystemEntry {
VirtualFileImpl(int id, VfsData.Segment segment, VirtualDirectoryImpl parent) {
public VirtualFileImpl(int id, VfsData.Segment segment, VirtualDirectoryImpl parent) {
super(id, segment, parent);
}
@@ -23,6 +23,7 @@ import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.ShutDownTracker;
import com.intellij.openapi.util.io.*;
import com.intellij.openapi.util.text.StringUtil;
@@ -391,15 +392,9 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
@Override
public long getLength(@NotNull VirtualFile file) {
long len;
if (mustReloadContent(file)) {
len = reloadLengthFromDelegate(file, getDelegate(file));
}
else {
len = getLastRecordedLength(file);
}
return len;
return mustReloadContent(file) ?
reloadLengthFromDelegate(file, getDelegate(file)) :
getLastRecordedLength(file);
}
@Override
@@ -688,7 +683,7 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
@NotNull
private static List<VFileEvent> validateEvents(@NotNull List<VFileEvent> events) {
final List<EventWrapper> deletionEvents = ContainerUtil.newArrayList();
final List<EventWrapper> deletionEvents = new ArrayList<>();
for (int i = 0, size = events.size(); i < size; i++) {
final VFileEvent event = events.get(i);
if (event instanceof VFileDeleteEvent && event.isValid()) {
@@ -776,14 +771,17 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
private void applyChildrenChangeEvents(@NotNull VirtualFile parent, @NotNull List<VFileEvent> events) {
final NewVirtualFileSystem delegate = getDelegate(parent);
TIntArrayList childrenIdsUpdated = new TIntArrayList();
final int parentId = getFileId(parent);
assert parentId != 0;
TIntHashSet parentChildrenIds = new TIntHashSet(FSRecords.list(parentId));
int[] oldIds = FSRecords.list(parentId);
TIntHashSet parentChildrenIds = new TIntHashSet(Math.max(events.size(), oldIds.length));
parentChildrenIds.addAll(oldIds);
boolean hasRemovedChildren = false;
List<VirtualFile> childrenToBeUpdated = new SmartList<>();
List<Pair<VirtualFile,CharSequence>> childrenAdded = new SmartList<>();
List<VirtualFile> childrenDeleted = new SmartList<>();
List<CharSequence> childrenNamesDeleted = new SmartList<>();
for (VFileEvent event : events) {
if (event instanceof VFileCreateEvent) {
String name = ((VFileCreateEvent)event).getChildName();
@@ -795,8 +793,7 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
assert parent instanceof VirtualDirectoryImpl : parent;
final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
childrenToBeUpdated.add(child);
childrenIdsUpdated.add(childId);
childrenAdded.add(Pair.create(child, name));
parentChildrenIds.add(childId);
}
}
@@ -810,8 +807,8 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
hasRemovedChildren = true;
int id = getFileId(file);
childrenToBeUpdated.add(file);
childrenIdsUpdated.add(-id);
childrenDeleted.add(file);
childrenNamesDeleted.add(file.getNameSequence());
parentChildrenIds.remove(id);
}
}
@@ -821,18 +818,15 @@ public class PersistentFSImpl extends PersistentFS implements ApplicationCompone
if (hasRemovedChildren) clearIdCache();
VirtualDirectoryImpl parentImpl = (VirtualDirectoryImpl)parent;
for (int i = 0, len = childrenIdsUpdated.size(); i < len; ++i) {
final int childId = childrenIdsUpdated.get(i);
final VirtualFile childFile = childrenToBeUpdated.get(i);
if (childId > 0) {
parentImpl.addChild((VirtualFileSystemEntry)childFile);
}
else {
FSRecords.deleteRecordRecursively(-childId);
parentImpl.removeChild(childFile);
if (!childrenDeleted.isEmpty()) {
for (final VirtualFile childFile : childrenDeleted) {
FSRecords.deleteRecordRecursively(getFileId(childFile));
invalidateSubtree(childFile);
}
parentImpl.removeChildren(childrenNamesDeleted);
}
if (!childrenAdded.isEmpty()) {
parentImpl.addChildren(childrenAdded);
}
}
@@ -17,35 +17,52 @@ package com.intellij.openapi.vfs;
import com.intellij.concurrency.JobLauncher;
import com.intellij.concurrency.JobSchedulerImpl;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.IoTestUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.testFramework.EdtTestUtil;
import com.intellij.testFramework.JITSensitive;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.SkipSlowTestLocally;
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.openapi.vfs.newvfs.impl.VfsData;
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileImpl;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.openapi.vfs.newvfs.persistent.FSRecords;
import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
import com.intellij.testFramework.*;
import com.intellij.testFramework.fixtures.BareTestFixtureTestCase;
import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl;
import com.intellij.testFramework.rules.TempDirectory;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.ThrowableRunnable;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.FileBasedIndexImpl;
import com.intellij.util.ref.GCUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.Assert.*;
@@ -76,7 +93,7 @@ public class VfsUtilPerformanceTest extends BareTestFixtureTestCase {
UIUtil.pump(); // wait for all event handlers to calm down
System.out.println("Start searching...");
PlatformTestUtil.startPerformanceTest("find child is slow", 1000, () -> {
PlatformTestUtil.startPerformanceTest("finding child", 1000, () -> {
for (int i = 0; i < 1000000; i++) {
VirtualFile child = vDir.findChild("5111.txt");
assertEquals(theChild, child);
@@ -103,7 +120,7 @@ public class VfsUtilPerformanceTest extends BareTestFixtureTestCase {
String path = jar.getPath() + "!/";
NewVirtualFile root = ManagingFS.getInstance().findRoot(path, fs);
PlatformTestUtil.startPerformanceTest(
"find root is slow", 5000,
"finding root", 5000,
() -> JobLauncher.getInstance().invokeConcurrentlyUnderProgress(
Collections.nCopies(500, null), null, false, false,
o -> {
@@ -154,13 +171,13 @@ public class VfsUtilPerformanceTest extends BareTestFixtureTestCase {
}
};
int time = 1200;
PlatformTestUtil.startPerformanceTest("getParent is slow before movement", time, checkPerformance).useLegacyScaling().assertTiming();
PlatformTestUtil.startPerformanceTest("getParent before movement", time, checkPerformance).useLegacyScaling().assertTiming();
VirtualFile dir1 = vDir.createChildDirectory(this, "dir1");
VirtualFile dir2 = vDir.createChildDirectory(this, "dir2");
for (int i = 0; i < 13; i++) { /*13 is max length with THashMap capacity of 17, we get plenty collisions then*/
dir1.createChildData(this, "a" + i + ".txt").move(this, dir2);
}
PlatformTestUtil.startPerformanceTest("getParent is slow after movement", time, checkPerformance).useLegacyScaling().assertTiming();
PlatformTestUtil.startPerformanceTest("getParent after movement", time, checkPerformance).useLegacyScaling().assertTiming();
}
}.execute();
}
@@ -185,7 +202,7 @@ public class VfsUtilPerformanceTest extends BareTestFixtureTestCase {
"fff.txt";
VirtualFile file = fixture.findOrCreateDir(path);
PlatformTestUtil.startPerformanceTest("VF.getPath() performance failed", 4000, () -> {
PlatformTestUtil.startPerformanceTest("VF.getPath()", 4000, () -> {
for (int i = 0; i < 1000000; ++i) {
file.getPath();
}
@@ -268,4 +285,170 @@ public class VfsUtilPerformanceTest extends BareTestFixtureTestCase {
IoTestUtil.assertTimestampsEqual(timestamp[i], child.getTimeStamp());
}
}
}
@Test
public void addingManyChildrenToTheSameDirectoryMustNotBeQuadratic() throws IOException {
int N = 1_000_000;
// measures create N children inside one directory.
// to avoid slow local file system try to use MyFakeVirtualFile which doesn't actually query disk
// then call PersistentFS.getInstance().processEvents(events); directly instead of agonizingly slow refresh.
ApplicationInfoImpl.setInStressTest(true); //wtf wrong with you fixtures?
Disposer.register(getTestRootDisposable(), ()-> ApplicationInfoImpl.setInStressTest(false));
List<VirtualFile> toDelete = new ArrayList<>();
try {
UIUtil.invokeLaterIfNeeded(()->{
PlatformTestUtil.startPerformanceTest("adding many children", 15000, () -> {
VirtualFile validVTemp = new MyFakeDirectory("vtemp");
toDelete.add(validVTemp);
List<VFileEvent> events = IntStream.range(0, N)
.mapToObj(i -> new VFileCreateEvent(this, validVTemp, i + ".txt", false, false))
.collect(Collectors.toList());
WriteCommandAction.runWriteCommandAction(null, () -> {
PersistentFS.getInstance().processEvents(events);
});
assertEquals(N, validVTemp.getChildren().length);
}).assertTiming();
});
}
finally {
toDelete.forEach(VfsTestUtil::deleteFile);
}
}
@Test
public void deleteManyChildrenFromTheSameDirectoryMustNotBeQuadratic() throws IOException {
int N = 1_000_000;
// measures delete N children inside one directory.
// to avoid slow local file system try to use MyFakeVirtualFile which doesn't actually query disk
// then call PersistentFS.getInstance().processEvents(events); directly instead of agonizingly slow refresh.
ApplicationInfoImpl.setInStressTest(true); //wtf wrong with you fixtures?
//TranslatingCompilerFilesMonitor.getInstance().disable(getTestRootDisposable());
Disposer.register(getTestRootDisposable(), ()-> ApplicationInfoImpl.setInStressTest(false));
List<VirtualFile> toDelete = new ArrayList<>();
try {
UIUtil.invokeLaterIfNeeded(()->{
final VirtualDirectoryImpl[] validVTemp = new VirtualDirectoryImpl[1];
List<VFileEvent> deleteEvents = new ArrayList<>();
PlatformTestUtil.startPerformanceTest("deleting many children", 30000, () -> {
WriteCommandAction.runWriteCommandAction(null, () -> {
PersistentFS.getInstance().processEvents(deleteEvents);
});
assertEquals(0, validVTemp[0].getChildren().length);
}).setup(()-> {
// prepare fake dir with N fake children
((FileBasedIndexImpl) FileBasedIndex.getInstance()).cleanupForNextTest();
GCUtil.tryForceGC();
validVTemp[0] = new MyFakeDirectory("vtemp");
validVTemp[0].getChildren();
toDelete.add(validVTemp[0]);
List<VFileEvent> createEvents = IntStream.range(0, N)
.mapToObj(i -> new VFileCreateEvent(this, validVTemp[0], i + ".txt", false, false))
.collect(Collectors.toList());
WriteCommandAction.runWriteCommandAction(null, () -> {
PersistentFS.getInstance().processEvents(createEvents);
});
assertEquals(N, validVTemp[0].getChildren().length);
deleteEvents.clear();
deleteEvents.addAll(Arrays.stream(validVTemp[0].getChildren())
.map(v -> new VFileDeleteEvent(this, new MyFakeFile(v.getName(), validVTemp[0]), false))
.collect(Collectors.toList()));
}).assertTiming();
});
}
finally {
toDelete.forEach(VfsTestUtil::deleteFile);
}
}
private static class MyFakeFile extends VirtualFileImpl {
private static final VfsData.Segment SEGMENT = new VfsData.Segment();
private final String name;
MyFakeFile(String name, VirtualDirectoryImpl parent) {
super(FSRecords.createRecord(), SEGMENT, parent);
this.name = name;
}
@NotNull
@Override
public CharSequence getNameSequence() {
return name;
}
@NotNull
@Override
public String getUrl() {
return getName();
}
@NotNull
@Override
public String getPath() {
return getName();
}
UserDataHolder data;
@Override
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
if (data == null) data = new UserDataHolderBase();
data.putUserData(key, value);
}
@Override
public <T> T getUserData(@NotNull Key<T> key) {
try {
return data.getUserData(key);
}
finally {
data.putUserData(key, null);
if (((UserDataHolderBase)data).isUserDataEmpty()) data = null;
}
}
}
private static class MyFakeDirectory extends VirtualDirectoryImpl {
private final String name;
MyFakeDirectory(String name) {
super(FSRecords.createRecord(), new VfsData.Segment(), new VfsData.DirectoryData(), null, new TempFileSystem(){
@Override
public FileAttributes getAttributes(@NotNull VirtualFile file) {
return new FileAttributes(false, false, false, false, 0, 1, true);
}
});
this.name = name;
}
@Nullable
@Override
public VirtualFileSystemEntry findChild(@NotNull String name) {
return null; // hack for VCreateEvent.isValid()
}
@NotNull
@Override
public String getPath() {
return getName();
}
@NotNull
@Override
public CharSequence getNameSequence() {
return name;
}
@NotNull
@Override
public String getUrl() {
return getPath();
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2000-2017 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.text;
import com.intellij.openapi.util.text.StringUtil;
import gnu.trove.TObjectHashingStrategy;
/**
* @author max
*/
public class CaseInsensitiveCharSequenceHashingStrategy implements TObjectHashingStrategy<CharSequence> {
public static final CaseInsensitiveCharSequenceHashingStrategy INSTANCE = new CaseInsensitiveCharSequenceHashingStrategy();
@Override
public int computeHashCode(final CharSequence s) {
return StringUtil.stringHashCodeInsensitive(s);
}
@Override
public boolean equals(final CharSequence s1, final CharSequence s2) {
return StringUtil.equalsIgnoreCase(s1, s2);
}
}