refactor virtual file pointer performance to fix IDEA-218393 Freezes in VirtualFilePointerManagerImpl.addRelevantPointers

- instead of maintaining modCount and lazily updating file pointer inplace on demand in FilePartNode.update(), update all VirtualFilePointers once during VFS.after() events and just return stored file afterwards, to speedup file pointer.getFile()/.getUrl()
- update VirtualFilePointers before sending VFS.after() events to everyone else because updated virtual file pointers are needed in after() listeners (e.g. DirectoryIndex)
- simplify trie structure to store jar-based and local pointers under one root to speedup finding relevant pointers on VFS change
- FilePartNode.update() now just does "parentFile.findChildByName()" in the common case, avoiding expensive findFileByUrl() which traverses the file hierarchy from the root

GitOrigin-RevId: 147835f790986e079a6d420aa2ac2375f6cb11df
This commit is contained in:
Alexey Kudravtsev
2020-06-08 09:53:36 +00:00
committed by intellij-monorepo-bot
parent d5b36ec026
commit 334e3e7d9f
14 changed files with 1098 additions and 852 deletions
@@ -198,8 +198,8 @@ public class VirtualFileManagerImpl extends VirtualFileManagerEx implements Disp
}
@ApiStatus.Internal
public List<AsyncFileListener> getAsyncFileListeners() {
return Collections.unmodifiableList(myAsyncFileListeners);
public void addAsyncFileListenersTo(@NotNull List<? super AsyncFileListener> listeners) {
listeners.addAll(myAsyncFileListeners);
}
@Override
@@ -0,0 +1,459 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.io.URLUtil;
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.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Trie data structure for succinct storage and fast retrieval of file pointers.
* File pointer "a/b/x.txt" is stored in the tree with nodes a->b->x.txt
*/
class FilePartNode {
private static final FilePartNode[] EMPTY_ARRAY = new FilePartNode[0];
static final int JAR_SEPARATOR_NAME_ID = -2;
private final int nameId; // name id of the VirtualFile corresponding to this node
FilePartNode @NotNull [] children = EMPTY_ARRAY; // sorted by this.getName(). elements never updated inplace
// file pointers for this exact path (i.e. concatenation of all getName() down from the root).
// Either VirtualFilePointerImpl or VirtualFilePointerImpl[] (when it so happened that several pointers merged into one node - e.g. after file rename onto existing pointer)
private Object leaves;
@NotNull
volatile Object myFileOrUrl;
final NewVirtualFileSystem myFS; // the file system of this particular component. E.g. for path "/x.jar!/foo.txt" the node "x.jar" fs is LocalFileSystem, the node "foo.txt" fs is JarFileSystem
FilePartNode(int nameId,
@NotNull Object fileOrUrl,
@NotNull NewVirtualFileSystem fs) {
myFS = fs;
assert nameId > 0 || nameId == JAR_SEPARATOR_NAME_ID : nameId + "; " + getClass();
this.nameId = nameId;
myFileOrUrl = fileOrUrl;
if (fileOrUrl instanceof VirtualFile) {
assert myFile().getFileSystem() == myFS : "myFs=" + myFS + "; myFile().getFileSystem()=" + myFile().getFileSystem() + "; " + fileOrUrl;
if (myFile().getParent() == null && fs instanceof ArchiveFileSystem) {
assert nameId == JAR_SEPARATOR_NAME_ID : nameId;
}
}
}
private VirtualFile myFile() {
return myFile(myFileOrUrl);
}
void addLeaf(@NotNull VirtualFilePointerImpl pointer) {
Object leaves = this.leaves;
Object newLeaves;
if (leaves == null) {
newLeaves = pointer;
}
else if (leaves instanceof VirtualFilePointerImpl) {
newLeaves = new VirtualFilePointerImpl[]{(VirtualFilePointerImpl)leaves, pointer};
}
else {
newLeaves = ArrayUtil.append((VirtualFilePointerImpl[])leaves, pointer);
}
associate(newLeaves);
}
// return remaining leaves number
int removeLeaf(@NotNull VirtualFilePointerImpl pointer) {
Object leaves = this.leaves;
if (leaves == null) {
return 0;
}
if (leaves instanceof VirtualFilePointerImpl) {
if (leaves == pointer) {
this.leaves = null;
return 0;
}
return 1;
}
VirtualFilePointerImpl[] newLeaves = ArrayUtil.remove((VirtualFilePointerImpl[])leaves, pointer);
if (newLeaves.length == 0) newLeaves = null;
this.leaves = newLeaves;
return newLeaves == null ? 0 : newLeaves.length;
}
static VirtualFile myFile(@NotNull Object fileOrUrl) {
return fileOrUrl instanceof VirtualFile ? (VirtualFile)fileOrUrl : null;
}
@NotNull
private String myUrl() {
return myUrl(myFileOrUrl);
}
@NotNull
static String myUrl(Object fileOrUrl) {
return fileOrUrl instanceof VirtualFile ? ((VirtualFile)fileOrUrl).getUrl() : (String)fileOrUrl;
}
// for creating fake root
FilePartNode(@NotNull NewVirtualFileSystem fs) {
nameId = -1;
myFileOrUrl = "";
myFS = fs;
}
@NotNull
static CharSequence fromNameId(int nameId) {
return nameId == JAR_SEPARATOR_NAME_ID ? JarFileSystem.JAR_SEPARATOR : FileNameCache.getVFileName(nameId);
}
@NotNull
CharSequence getName() {
return fromNameId(nameId);
}
@Override
public String toString() {
return getName() + (children.length == 0 ? "" : " -> "+children.length);
}
static int getNameId(@NotNull VirtualFile file) {
VirtualFileSystem fs = file.getFileSystem();
if (fs instanceof ArchiveFileSystem && file.getParent() == null) {
return JAR_SEPARATOR_NAME_ID;
}
return ((VirtualFileSystemEntry)file).getNameId();
}
@Contract("_, _, true, _ -> !null")
FilePartNode findChildByNameId(@Nullable VirtualFile file,
int nameId,
boolean createIfNotFound,
@NotNull NewVirtualFileSystem childFs) {
if (nameId <= 0 && nameId != JAR_SEPARATOR_NAME_ID) throw new IllegalArgumentException("invalid argument nameId: "+nameId);
for (FilePartNode child : children) {
if (child.nameEqualTo(nameId)) return child;
}
if (createIfNotFound) {
CharSequence name = fromNameId(nameId);
int index = children.length == 0 ? -1 : binarySearchChildByName(name);
FilePartNode child;
assert index < 0 : index + " : child= '" + (child = children[index]) + "'"
+ "; child.nameEqualTo(nameId)=" + child.nameEqualTo(nameId)
+ "; child.getClass()=" + child.getClass()
+ "; child.nameId=" + child.nameId
+ "; child.getName()='" + child.getName() + "'"
+ "; nameId=" + nameId
+ "; name='" + name + "'"
+ "; compare(child) = " + StringUtil.compare(child.getName(), name, !SystemInfo.isFileSystemCaseSensitive) + ";"
+ " UrlPart.nameEquals: " + FileUtil.PATH_CHAR_SEQUENCE_HASHING_STRATEGY.equals(child.getName(), fromNameId(nameId))
+ "; name.equals(child.getName())=" + child.getName().equals(name)
;
Object fileOrUrl = file;
if (fileOrUrl == null) {
fileOrUrl = this.nameId == -1 ? name.toString() : childUrl(myUrl(), name, childFs);
}
child = new FilePartNode(nameId, fileOrUrl, childFs);
children = ArrayUtil.insert(children, -index-1, child);
return child;
}
return null;
}
boolean nameEqualTo(int nameId) {
return this.nameId == nameId;
}
int binarySearchChildByName(@NotNull CharSequence name) {
return ObjectUtils.binarySearch(0, children.length, i -> {
FilePartNode child = children[i];
CharSequence childName = child.getName();
return StringUtil.compare(childName, name, !SystemInfo.isFileSystemCaseSensitive);
});
}
void addRecursiveDirectoryPtrTo(@NotNull MultiMap<? super VirtualFilePointerListener, ? super VirtualFilePointerImpl> toFirePointers) {
processPointers(pointer -> { if (pointer.isRecursive()) toFirePointers.putValue(pointer.myListener, pointer); });
}
void doCheckConsistency(@NotNull String pathFromRoot) {
String name = getName().toString();
if (!(this instanceof FilePartNodeRoot)) {
String expectedUrl = VirtualFileManager.constructUrl(myFS.getProtocol(), pathFromRoot + (pathFromRoot.endsWith("/") ? "" : "/"));
String actualUrl = myUrl() + (myUrl().endsWith("/") ? "" : "/");
assert actualUrl.equals(expectedUrl) : "Expected url: '"+expectedUrl+"' but got: '"+actualUrl+"'";
}
assert !"..".equals(name) && !".".equals(name) : "url must not contain '.' or '..' but got: " + this;
for (int i = 0; i < children.length; i++) {
FilePartNode child = children[i];
child.doCheckConsistency(pathFromRoot + (pathFromRoot.isEmpty() || pathFromRoot.endsWith("/") || child.getName().equals(JarFileSystem.JAR_SEPARATOR) ? "" : "/") + child.getName());
if (i != 0) {
assert !FileUtil.namesEqual(child.getName().toString(), children[i-1].getName().toString()) : "child["+i+"] = "+child+"; [-1] = "+children[i-1];
}
}
int[] leafNumber = new int[1];
processPointers(p -> { assert p.myNode == this; leafNumber[0]++; });
int useCount = leafNumber[0];
assert (useCount == 0) == (leaves == null) : useCount + " - " + (leaves instanceof VirtualFilePointerImpl ? leaves : Arrays.toString((VirtualFilePointerImpl[])leaves));
// there's just a name in UrlPartNode
if (!(this instanceof UrlPartNode)) {
String url = myUrl();
String myPath = VfsUtilCore.urlToPath(url);
String nameFromPath = nameId == JAR_SEPARATOR_NAME_ID ? JarFileSystem.JAR_SEPARATOR : PathUtil.getFileName(myPath);
if (!myPath.isEmpty() && nameFromPath.isEmpty()/* && SystemInfo.isUnix*/) {
nameFromPath = "/";
}
assert StringUtilRt.equal(nameFromPath, name, SystemInfo.isFileSystemCaseSensitive) : "fileAndUrl: " + myFileOrUrl + "; but this: " + this+"; nameFromPath: "+nameFromPath+"; name: "+name+"; path: "+myPath+"; url: "+url+";";
VirtualFile file = myFile();
if (file != null) {
String fileName = file.getParent() == null && file.getFileSystem() instanceof ArchiveFileSystem ? JarFileSystem.JAR_SEPARATOR : file.getName();
assert fileName.equals(name) : "fileAndUrl: " + myFileOrUrl + "; but this: " + this;
assert file.getFileSystem() == myFS;
}
}
}
// update myFileOrUrl to a VirtualFile and replace UrlPartNode with FilePartNode if the file exists, including all subnodes
void update(@NotNull FilePartNode parent, @NotNull FilePartNodeRoot root) {
Object fileOrUrl = myFileOrUrl;
VirtualFile file = myFile(fileOrUrl);
boolean changed = false;
boolean nameChanged = false;
boolean fileIsValid = false;
if (file != null) {
fileIsValid = file.isValid();
if (fileIsValid && file.getParent() == null && file.getFileSystem() instanceof ArchiveFileSystem) {
VirtualFile local = ((ArchiveFileSystem)file.getFileSystem()).getLocalByEntry(file);
fileIsValid = local != null;
}
if (!fileIsValid) {
file = null;
changed = true;
}
}
Object parentFileOrUrl;
parentFileOrUrl = parent.myFileOrUrl;
String myName = getName().toString();
String url = null;
String parentUrl = null;
VirtualFile parentFile = myFile(parentFileOrUrl);
if (file == null) {
file = parentFile == null || !parentFile.isValid() ? null : findChildThroughJar(parentFile, myName, myFS);
if (file == null) {
parentUrl = myUrl(parentFileOrUrl);
url = childUrl(parentUrl, myName, myFS);
changed |= nameChanged = !Comparing.strEqual(url, myUrl(fileOrUrl));
}
else {
changed = true;
}
fileIsValid = file != null && file.isValid();
}
if (parent.nameId != -1 && !(parentFileOrUrl instanceof VirtualFile) && file != null) {
// if parent file can't be found then the child is not valid too
file = null;
fileIsValid = false;
url = myUrl(fileOrUrl);
}
if (file != null) {
if (fileIsValid) {
changed |= nameChanged = !StringUtil.equals(file.getNameSequence(), myName);
}
else {
file = null; // can't find, try next time
changed = true;
url = myUrl(fileOrUrl);
}
}
Object result = file == null ? url : file;
changed |= !Objects.equals(fileOrUrl, result);
FilePartNode thisNode = this;
if (changed) {
myFileOrUrl = result;
if (file != null && (this instanceof UrlPartNode || nameChanged)) {
// replace with FPPN if the actual file's appeared on disk to save memory with nameIds
thisNode = replaceWithFPPN(file, parent);
}
}
if (file != null && !Objects.equals(getParentThroughJar(file, myFS), parentFile)) {
// this node file must be moved to the other dir. remove and re-insert from the root to the correct path, preserving all children
FilePartNode newNode = root.findOrCreateByFile(file).node;
processPointers(p-> newNode.addLeaf(p));
newNode.children = children;
children = EMPTY_ARRAY;
changed = true;
String myOldPath = VfsUtilCore.urlToPath(childUrl(parentUrl=myUrl(parentFileOrUrl), myName, myFS));
root.removeEmptyNodesByPath(FilePartNodeRoot.splitNames(myOldPath));
thisNode = newNode;
nameChanged = true;
}
if (nameChanged) {
String myOldPath = VfsUtilCore.urlToPath(childUrl(parentUrl == null ? myUrl(parentFileOrUrl) : parentUrl, myName, myFS));
String myNewPath = VfsUtilCore.urlToPath(myUrl(result));
// fix UrlPartNodes with (now) wrong url start
thisNode.fixUrlPartNodes(myOldPath, myNewPath);
}
if (changed) {
for (FilePartNode child : thisNode.children) {
child.update(thisNode, root);
}
}
}
private void fixUrlPartNodes(@NotNull String oldPath, @NotNull String newPath) {
if (this instanceof UrlPartNode) {
String protocol = myFS.getProtocol();
String myUrl = myUrl();
if (StringUtil.startsWith(myUrl, protocol.length()+URLUtil.SCHEME_SEPARATOR.length(), oldPath)) {
myFileOrUrl = protocol + URLUtil.SCHEME_SEPARATOR + newPath + myUrl.substring(protocol.length() + URLUtil.SCHEME_SEPARATOR.length()+oldPath.length());
}
}
for (FilePartNode child : children) {
child.fixUrlPartNodes(oldPath, newPath);
}
}
@NotNull
private FilePartNode replaceWithFPPN(@NotNull VirtualFile file, @NotNull FilePartNode parent) {
int nameId = getNameId(file);
parent.children = ArrayUtil.remove(parent.children, this);
FilePartNode newNode = parent.findChildByNameId(file, nameId, true, (NewVirtualFileSystem)file.getFileSystem());
newNode.children = children; // old children are destroyed when renamed onto their parent
processPointers(pointer-> newNode.addLeaf(pointer));
leaves = null;
return newNode;
}
@NotNull
static String childUrl(@NotNull String parentUrl, @NotNull CharSequence childName, @NotNull NewVirtualFileSystem fs) {
if (childName.equals(JarFileSystem.JAR_SEPARATOR) && fs instanceof ArchiveFileSystem) {
return VirtualFileManager.constructUrl(fs.getProtocol(), StringUtil.trimEnd(VfsUtilCore.urlToPath(parentUrl), '/')) + childName;
}
return parentUrl.isEmpty() ? VirtualFileManager.constructUrl(fs.getProtocol(), childName.toString()) :
VirtualFileManager.constructUrl(fs.getProtocol(), StringUtil.trimEnd(VfsUtilCore.urlToPath(parentUrl), '/')) + '/' + childName;
}
private void associate(@Nullable Object leaves) {
this.leaves = leaves;
if (leaves != null) {
if (leaves instanceof VirtualFilePointerImpl) {
((VirtualFilePointerImpl)leaves).myNode = this;
}
else {
for (VirtualFilePointerImpl pointer : (VirtualFilePointerImpl[])leaves) {
pointer.myNode = this;
}
}
}
}
VirtualFilePointerImpl getPointer(VirtualFilePointerListener listener) {
Object leaves = this.leaves;
if (leaves == null) {
return null;
}
if (leaves instanceof VirtualFilePointerImpl) {
VirtualFilePointerImpl leaf = (VirtualFilePointerImpl)leaves;
return leaf.myListener == listener ? leaf : null;
}
VirtualFilePointerImpl[] array = (VirtualFilePointerImpl[])leaves;
for (VirtualFilePointerImpl pointer : array) {
if (pointer.myListener == listener) return pointer;
}
return null;
}
void addAllPointersTo(@NotNull Collection<? super VirtualFilePointerImpl> outList) {
processPointers(p->{ if (p.myNode != null) outList.add(p); });
}
void processPointers(@NotNull Consumer<? super VirtualFilePointerImpl> processor) {
Object leaves = this.leaves;
if (leaves == null) {
return;
}
if (leaves instanceof VirtualFilePointerImpl) {
processor.accept((VirtualFilePointerImpl)leaves);
return;
}
VirtualFilePointerImpl[] pointers = (VirtualFilePointerImpl[])leaves;
for (VirtualFilePointerImpl pointer : pointers) {
processor.accept(pointer);
}
}
// for "file://a/b/c.txt" return "a/b", for "jar://a/b/j.jar!" return "file://a/b/j.jar"
static VirtualFile getParentThroughJar(@NotNull VirtualFile file, @NotNull NewVirtualFileSystem fs) {
VirtualFile parent = file.getParent();
if (parent == null && fs instanceof ArchiveFileSystem) {
parent = ((ArchiveFileSystem)fs).getLocalByEntry(file);
}
return parent;
}
static VirtualFile findChildThroughJar(@NotNull VirtualFile file, @NotNull String name, @NotNull NewVirtualFileSystem childFs) {
VirtualFile child;
if (name.equals(JarFileSystem.JAR_SEPARATOR) && childFs instanceof ArchiveFileSystem) {
child = ((ArchiveFileSystem)childFs).getRootByLocal(file);
}
else {
child = file.findChild(name);
}
return child;
}
boolean removeEmptyNodesByFile(@NotNull List<VirtualFile> parts) {
if (parts.isEmpty()) {
return children.length == 0;
}
VirtualFile file = parts.remove(parts.size()-1);
FilePartNode child = findChildByNameId(null, getNameId(file), false, (NewVirtualFileSystem)file.getFileSystem());
if (child == null) {
return false;
}
boolean toRemove = child.removeEmptyNodesByFile(parts);
if (toRemove) {
children = children.length == 1 ? EMPTY_ARRAY : ArrayUtil.remove(children, child);
return children.length == 0 && leaves == null;
}
return false;
}
boolean removeEmptyNodesByPath(@NotNull List<String> parts) {
if (parts.isEmpty()) {
return children.length == 0;
}
String name = parts.remove(parts.size()-1);
int index = binarySearchChildByName(name);
if (index < 0) {
return false;
}
FilePartNode child = children[index];
boolean toRemove = child.removeEmptyNodesByPath(parts);
if (toRemove) {
children = children.length == 1 ? EMPTY_ARRAY : ArrayUtil.remove(children, child);
return children.length == 0 && leaves == null;
}
return false;
}
}
@@ -0,0 +1,260 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl.NodeToUpdate;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.impl.NullVirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathUtilRt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class FilePartNodeRoot extends FilePartNode {
private FilePartNodeRoot(@NotNull NewVirtualFileSystem fs) {
super(fs);
}
@Override
public String toString() {
return "root -> "+children.length;
}
@NotNull
@Override
CharSequence getName() {
return "";
}
@NotNull
NodeToUpdate findOrCreateByFile(@NotNull VirtualFile file) {
int nameId = getNameId(file);
NewVirtualFileSystem fs = (NewVirtualFileSystem)file.getFileSystem();
VirtualFile parent = getParentThroughJar(file, fs);
return matchById(parent, file, nameId, new MultiMap<>(), true, fs);
}
/**
* Appends to {@code out} all nodes under this node whose path (beginning from this node) starts with the given path
* ({@code parent.getPath() + "/" + getVFileName(childNameId)}) and all nodes under this node with recursive directory pointers whose
* path is ancestor of the given path.
*/
void addRelevantPointersFrom(@NotNull VirtualFileSystemEntry parent,
@Nullable VirtualFile file,
int childNameId,
@NotNull MultiMap<? super VirtualFilePointerListener, ? super VirtualFilePointerImpl> toFirePointers,
@NotNull List<? super NodeToUpdate> toUpdateNodes,
boolean addSubdirectoryPointers,
@NotNull NewVirtualFileSystem fs) {
if (childNameId <= 0) throw new IllegalArgumentException("invalid argument childNameId: "+childNameId);
NodeToUpdate toUpdate = matchById(parent, file, childNameId, toFirePointers, false, fs);
if (toUpdate != null) {
toUpdateNodes.add(toUpdate);
toUpdate.node.processPointers(pointer -> toFirePointers.putValue(pointer.myListener, pointer));
if (addSubdirectoryPointers) {
// when "a/b" changed, treat all "a/b/*" virtual file pointers as changed because that's what happens on directory rename "a"->"newA": "a" deleted and "newA" created
addAllPointersStrictlyUnder(toUpdate.node, toFirePointers);
}
}
}
private static void addAllPointersStrictlyUnder(@NotNull FilePartNode node,
@NotNull MultiMap<? super VirtualFilePointerListener, ? super VirtualFilePointerImpl> toFirePointers) {
for (FilePartNode child : node.children) {
child.processPointers(pointer -> toFirePointers.putValue(pointer.myListener, pointer));
addAllPointersStrictlyUnder(child, toFirePointers);
}
}
/**
* Tries to match the given path (parent, childNameId) with the trie structure of FilePartNodes
* <p>Recursive nodes (i.e. the nodes containing VFP with recursive==true) will be added to outDirs.
*/
@Contract("_, _, _, _, true, _ -> !null")
private NodeToUpdate matchById(@Nullable VirtualFile parent,
@Nullable VirtualFile file,
int childNameId,
@NotNull MultiMap<? super VirtualFilePointerListener, ? super VirtualFilePointerImpl> toFirePointers,
boolean createIfNotFound,
@NotNull NewVirtualFileSystem fs) {
if (childNameId <= 0 && childNameId != JAR_SEPARATOR_NAME_ID) throw new IllegalArgumentException("invalid argument childNameId: " + childNameId);
List<VirtualFile> parts = parent == null ? Collections.emptyList() : getHierarchy(parent, fs);
FilePartNode node = this;
for (int i = parts.size() - 1; i >= 0; i--) {
VirtualFile part = parts.get(i);
int nameId = getNameId(part);
node = node.findChildByNameId(part, nameId, createIfNotFound, (NewVirtualFileSystem)part.getFileSystem());
if (node == null) return null;
// recursive pointers must be fired even for events deep under them
node.addRecursiveDirectoryPtrTo(toFirePointers);
}
FilePartNode child = node.findChildByNameId(file, childNameId, createIfNotFound, fs);
return child == null ? null : new NodeToUpdate(node, child);
}
private static @NotNull List<VirtualFile> getHierarchy(@NotNull VirtualFile file, @NotNull NewVirtualFileSystem fs) {
List<VirtualFile> result = new ArrayList<>();
while (true) {
result.add(file);
file = getParentThroughJar(file, fs);
if (file == null) break;
fs = (NewVirtualFileSystem)file.getFileSystem();
}
return result;
}
@NotNull
NodeToUpdate findOrCreateByPath(@NotNull String path, @NotNull NewVirtualFileSystem fs) {
List<String> names = splitNames(path);
NewVirtualFile fsRoot = null;
VirtualFile NEVER_TRIED_TO_FIND = NullVirtualFile.INSTANCE;
// we try to never call file.findChild() because it's expensive
VirtualFile currentFile = NEVER_TRIED_TO_FIND;
FilePartNode currentNode = this;
FilePartNode parentNode = this;
NewVirtualFileSystem currentFS = fs instanceof ArchiveFileSystem ? LocalFileSystem.getInstance() : fs;
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
if (name.equals(JarFileSystem.JAR_SEPARATOR) && currentFS instanceof LocalFileSystem) {
// switch inside jar
currentFS = fs;
}
int index = currentNode.binarySearchChildByName(name);
if (index >= 0) {
parentNode = currentNode;
currentNode = currentNode.children[index];
//noinspection UseVirtualFileEquals
currentFile = currentFile == NEVER_TRIED_TO_FIND || currentFile == null ? currentFile : currentFile.findChild(name);
continue;
}
// create and insert new node
// first, have to check if the file root/names(end)/.../names[i] exists
// if yes, create nameId-based FilePartNode (for faster search and memory efficiency),
// if not, create temp UrlPartNode which will be replaced with FPPN when the real file is created
//noinspection UseVirtualFileEquals
if (currentFile == NEVER_TRIED_TO_FIND) {
if (fsRoot == null) {
String rootPath = ContainerUtil.getLastItem(names);
fsRoot = ManagingFS.getInstance().findRoot(rootPath, fs instanceof ArchiveFileSystem ? LocalFileSystem.getInstance() : fs);
if (fsRoot != null && !fsRoot.getName().equals(rootPath)) {
// ignore really weird root names, like "/" under windows
fsRoot = null;
}
}
currentFile = fsRoot == null ? null : findFileFromRoot(fsRoot, currentFS, names, i);
}
else {
currentFile = currentFile == null ? null : findChildThroughJar(currentFile, name, currentFS);
}
FilePartNode child = currentFile == null ? new UrlPartNode(name, myUrl(currentNode.myFileOrUrl), currentFS)
: new FilePartNode(name.equals(JarFileSystem.JAR_SEPARATOR) ? JAR_SEPARATOR_NAME_ID : getNameId(currentFile), currentFile, currentFS);
currentNode.children = ArrayUtil.insert(currentNode.children, -index - 1, child);
parentNode = currentNode;
currentNode = child;
}
return new NodeToUpdate(parentNode, currentNode);
}
@NotNull
static List<String> splitNames(@NotNull String path) {
List<String> names = new ArrayList<>(20);
int end = path.length();
if (end == 0) return names;
while (true) {
boolean isJarSeparator =
StringUtil.endsWith(path, 0, end, JarFileSystem.JAR_SEPARATOR) && end > 2 && path.charAt(end - 3) != '/';
if (isJarSeparator) {
names.add(JarFileSystem.JAR_SEPARATOR);
end = end - 2;
continue;
}
if (path.charAt(end-1) == '/') {
end--;
}
if (end == 0 && path.charAt(0) == '/') {
end = 1; // here's this weird ROOT file in temp system
}
int startIndex = extractName(path, end);
assert startIndex != end : "startIndex: "+startIndex+"; end: "+end+"; path:'"+path+"'; toExtract: '"+path.substring(0, end)+"'";
names.add(path.substring(startIndex, end));
if (startIndex == 0) {
break;
}
end = startIndex;
}
return names;
}
private static VirtualFile findFileFromRoot(@NotNull NewVirtualFile root,
@NotNull NewVirtualFileSystem fs,
@NotNull List<String> names,
int startIndex) {
VirtualFile file = root;
// start from before-the-last because it's the root, which we already found
for (int i = names.size() - 2; i >= startIndex; i--) {
String name = names.get(i);
file = findChildThroughJar(file, name, fs);
if (file == null) break;
}
return file;
}
// returns start index of the name (i.e. path[return..length) is considered a name)
private static int extractName(@NotNull CharSequence path, int length) {
if (length == 1 && path.charAt(0) == '/') {
return 0; // in case of TEMP file system there is this weird ROOT file
}
int i = StringUtil.lastIndexOf(path, '/', 0, length);
if (i != -1 && PathUtilRt.isWindowsUNCRoot(path, i)) {
// UNC
return 0;
}
return i + 1;
}
void removePointer(@NotNull VirtualFilePointerImpl pointer) {
FilePartNode node = pointer.myNode;
int remainingLeaves = node.removeLeaf(pointer);
if (remainingLeaves == 0) {
VirtualFile file = myFile(node.myFileOrUrl);
if (file == null) {
List<String> parts = splitNames(VfsUtilCore.urlToPath(myUrl(node.myFileOrUrl)));
removeEmptyNodesByPath(parts);
}
else {
List<VirtualFile> parts = getHierarchy(file, (NewVirtualFileSystem)file.getFileSystem());
removeEmptyNodesByFile(parts);
}
}
}
void checkConsistency() {
if (VirtualFilePointerManagerImpl.IS_UNDER_UNIT_TEST && !ApplicationInfoImpl.isInStressTest()) {
doCheckConsistency("");
}
}
@NotNull
static FilePartNodeRoot createFakeRoot(@NotNull NewVirtualFileSystem fs) {
return new FilePartNodeRoot(fs);
}
}
@@ -1,511 +0,0 @@
/*
* 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.openapi.vfs.impl;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
import com.intellij.openapi.vfs.newvfs.impl.NullVirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Trie data structure for succinct storage and fast retrieval of file pointers.
* File pointer "a/b/x.txt" is stored in the tree with nodes a->b->x.txt
*/
class FilePointerPartNode {
private static final FilePointerPartNode[] EMPTY_ARRAY = new FilePointerPartNode[0];
private final int nameId; // name id of the VirtualFile corresponding to this node
FilePointerPartNode @NotNull [] children = EMPTY_ARRAY; // sorted by this.getName(). elements never updated inplace
final FilePointerPartNode parent;
// file pointers for this exact path (e.g. concatenation of all "part" fields down from the root).
// Either VirtualFilePointerImpl or VirtualFilePointerImpl[] (when it so happened that several pointers merged into one node - e.g. after file rename onto existing pointer)
private Object leaves;
// in case there is file pointer exists for this part, its info is saved here
volatile Pair<VirtualFile, String> myFileAndUrl; // must not be both null
volatile long myLastUpdated = -1; // contains latest result of ManagingFS.getInstance().getStructureModificationCount()
volatile int useCount;
int pointersUnder; // number of alive pointers in this node plus all nodes beneath
private static final VirtualFileManager ourFileManager = VirtualFileManager.getInstance();
private FilePointerPartNode(int nameId, @NotNull FilePointerPartNode parent) {
assert nameId > 0 : nameId + "; " + getClass();
this.nameId = nameId;
this.parent = parent;
}
boolean urlEndsWithName(@NotNull String urlAfter, VirtualFile fileAfter) {
if (fileAfter != null) {
return nameId == getNameId(fileAfter);
}
return StringUtil.endsWith(urlAfter, getName());
}
@NotNull
static FilePointerPartNode createFakeRoot() {
return new FilePointerPartNode(null) {
@Override
public String toString() {
return "root -> "+children.length;
}
@NotNull
@Override
CharSequence getName() {
return "";
}
};
}
// for creating fake root
FilePointerPartNode(FilePointerPartNode parent) {
nameId = -1;
this.parent = parent;
}
@NotNull
static CharSequence fromNameId(int nameId) {
return FileNameCache.getVFileName(nameId);
}
@NotNull
CharSequence getName() {
return FileNameCache.getVFileName(nameId);
}
@Override
public String toString() {
return getName() + (children.length == 0 ? "" : " -> "+children.length);
}
/**
* Tries to match the given path (parent, childNameId) with the trie structure of FilePointerPartNodes
* <p>Recursive nodes (i.e. the nodes containing VFP with recursive==true) will be added to outDirs.
*/
@Contract("_,_,_,true,_->!null")
private FilePointerPartNode matchById(@Nullable VirtualFile parent,
int childNameId,
@Nullable List<? super FilePointerPartNode> outDirs,
boolean createIfNotFound,
@NotNull VirtualFileSystem fs) {
if (childNameId <= 0) throw new IllegalArgumentException("invalid argument childNameId: " + childNameId);
FilePointerPartNode leaf;
if (parent == null) {
leaf = this;
}
else {
VirtualFile gParent = getParentThroughJars(parent, fs);
int parentNameId = getNameId(parent);
leaf = matchById(gParent, parentNameId, outDirs, createIfNotFound, fs);
if (leaf == null) return null;
}
leaf.addRecursiveDirectoryPtrTo(outDirs);
return leaf.findChildByNameId(childNameId, createIfNotFound);
}
private static int getNameId(@NotNull VirtualFile file) {
return ((VirtualFileSystemEntry)file).getNameId();
}
// returns start index of the name (i.e. path[return..length) is considered a name)
private static int extractName(@NotNull CharSequence path, int length) {
if (length == 1 && path.charAt(0) == '/') {
return 0; // in case of TEMP file system there is this weird ROOT file
}
int i = StringUtil.lastIndexOf(path, '/', 0, length);
return i + 1;
}
@Contract("_,true->!null")
private FilePointerPartNode findChildByNameId(int nameId, boolean createIfNotFound) {
//assert Thread.holdsLock(VirtualFilePointerManager.getInstance());
if (nameId <= 0) throw new IllegalArgumentException("invalid argument nameId: "+nameId);
for (FilePointerPartNode child : children) {
if (child.nameEqualTo(nameId)) return child;
}
if (createIfNotFound) {
CharSequence name = null;
int index = children.length == 0 ? -1 : binarySearchChildByName(fromNameId(nameId));
FilePointerPartNode child;
assert index < 0 : index + " : child= '" + (child = children[index]) + "'"
+ "; child.nameEqualTo(nameId)=" + child.nameEqualTo(nameId)
+ "; child.getClass()=" + child.getClass()
+ "; child.nameId=" + child.nameId
+ "; child.getName()='" + child.getName() + "'"
+ "; nameId=" + nameId
+ "; name='" + name + "'"
+ "; compare(child) = " + StringUtil.compare(child.getName(), name, !SystemInfo.isFileSystemCaseSensitive) + ";"
+ " UrlPart.nameEquals: " + FileUtil.PATH_CHAR_SEQUENCE_HASHING_STRATEGY.equals(child.getName(), fromNameId(nameId))
+ "; name.equals(child.getName())=" + child.getName().equals(name)
;
child = new FilePointerPartNode(nameId, this);
children = ArrayUtil.insert(children, -index-1, child);
return child;
}
return null;
}
boolean nameEqualTo(int nameId) {
return this.nameId == nameId;
}
private int binarySearchChildByName(@NotNull CharSequence name) {
return ObjectUtils.binarySearch(0, children.length, i -> {
FilePointerPartNode child = children[i];
CharSequence childName = child.getName();
return StringUtil.compare(childName, name, !SystemInfo.isFileSystemCaseSensitive);
});
}
private void addRecursiveDirectoryPtrTo(@Nullable List<? super FilePointerPartNode> dirs) {
if(dirs != null && hasRecursiveDirectoryPointer() && ContainerUtil.getLastItem(dirs) != this) {
dirs.add(this);
}
}
/**
* Appends to {@code out} all nodes under this node whose path (beginning from this node) starts with the given path
* ({@code (parent != null ? parent.getPath() : "") + (separator ? "/" : "") + childName}) and all nodes under this node with recursive directory pointers whose
* path is ancestor of the given path.
*/
void addRelevantPointersFrom(@NotNull VirtualFileSystemEntry parent,
int childNameId,
@NotNull List<? super FilePointerPartNode> out,
boolean addSubdirectoryPointers,
@NotNull VirtualFileSystem fs) {
if (childNameId <= 0) throw new IllegalArgumentException("invalid argument childNameId: "+childNameId);
FilePointerPartNode node = matchById(parent, childNameId, out, false, fs);
if (node != null) {
if (node.leaves != null) {
out.add(node);
}
if (addSubdirectoryPointers) {
// when "a/b" changed, treat all "a/b/*" virtual file pointers as changed because that's what happens on directory rename "a"->"newA": "a" deleted and "newA" created
addAllPointersStrictlyUnder(node, out);
}
}
}
private boolean hasRecursiveDirectoryPointer() {
if (leaves == null) return false;
if (leaves instanceof VirtualFilePointer) {
return ((VirtualFilePointerImpl)leaves).isRecursive();
}
VirtualFilePointerImpl[] leaves = (VirtualFilePointerImpl[])this.leaves;
for (VirtualFilePointerImpl leaf : leaves) {
if (leaf.isRecursive()) return true;
}
return false;
}
private static void addAllPointersStrictlyUnder(@NotNull FilePointerPartNode node, @NotNull List<? super FilePointerPartNode> out) {
for (FilePointerPartNode child : node.children) {
if (child.leaves != null) {
out.add(child);
}
addAllPointersStrictlyUnder(child, out);
}
}
void checkConsistency() {
if (VirtualFilePointerManagerImpl.IS_UNDER_UNIT_TEST && !ApplicationInfoImpl.isInStressTest()) {
doCheckConsistency();
}
}
private void doCheckConsistency() {
String name = getName().toString();
assert !"..".equals(name) && !".".equals(name) : "url must not contain '.' or '..' but got: " + this;
int childSum = 0;
for (int i = 0; i < children.length; i++) {
FilePointerPartNode child = children[i];
childSum += child.pointersUnder;
child.doCheckConsistency();
assert child.parent == this;
if (i != 0) {
assert !FileUtil.namesEqual(child.getName().toString(), children[i-1].getName().toString()) : "child["+i+"] = "+child+"; [-1] = "+children[i-1];
}
}
childSum += leavesNumber();
assert (useCount == 0) == (leaves == null) : useCount + " - " + (leaves instanceof VirtualFilePointerImpl ? leaves : Arrays.toString((VirtualFilePointerImpl[])leaves));
assert pointersUnder == childSum : "expected: "+pointersUnder+"; actual: "+childSum;
Pair<VirtualFile, String> fileAndUrl = myFileAndUrl;
if (fileAndUrl != null && fileAndUrl.second != null) {
String url = fileAndUrl.second;
String path = VfsUtilCore.urlToPath(url);
path = StringUtil.trimEnd(path, JarFileSystem.JAR_SEPARATOR);
String nameFromPath = PathUtil.getFileName(path);
if (!path.isEmpty() && nameFromPath.isEmpty() && SystemInfo.isUnix) {
nameFromPath = "/";
}
assert StringUtilRt.equal(nameFromPath, name, SystemInfo.isFileSystemCaseSensitive) : "fileAndUrl: " + fileAndUrl + "; but this: " + this+"; nameFromPath: "+nameFromPath+"; name: "+name+"; parent: "+parent+"; path: "+path+"; url: "+url;
}
boolean hasFile = fileAndUrl != null && fileAndUrl.first != null;
if (hasFile) {
assert fileAndUrl.first.getName().equals(name) : "fileAndUrl: " + fileAndUrl + "; but this: " + this;
}
}
// returns root node
@NotNull
FilePointerPartNode remove() {
int pointersNumber = leavesNumber();
assert leaves != null : toString();
associate(null, null);
useCount = 0;
FilePointerPartNode node;
for (node = this; node.parent != null; node = node.parent) {
int pointersAfter = node.pointersUnder-=pointersNumber;
if (pointersAfter == 0) {
node.parent.children = ArrayUtil.remove(node.parent.children, node);
node.myFileAndUrl = null;
}
}
if ((node.pointersUnder-=pointersNumber) == 0) {
node.children = EMPTY_ARRAY; // clear root node, especially in tests
}
return node;
}
@Nullable("null means this node's myFileAndUrl became invalid")
// returns pair.second != null always
Pair<VirtualFile, String> update() {
final long lastUpdated = myLastUpdated;
final Pair<VirtualFile, String> fileAndUrl = myFileAndUrl;
if (fileAndUrl == null) return null;
final long fsModCount = ManagingFS.getInstance().getStructureModificationCount();
if (lastUpdated == fsModCount) return fileAndUrl;
VirtualFile file = fileAndUrl.first;
String url = fileAndUrl.second;
boolean changed = false;
if (url == null) {
url = file.getUrl();
if (!file.isValid()) file = null;
changed = true;
}
boolean fileIsValid = file != null && file.isValid();
if (file != null && !fileIsValid) {
file = null;
changed = true;
}
if (file == null) {
file = ourFileManager.findFileByUrl(url);
fileIsValid = file != null && file.isValid();
if (file != null) {
changed = true;
}
}
if (file != null) {
if (fileIsValid) {
url = file.getUrl(); // refresh url, it can differ
changed |= !url.equals(fileAndUrl.second);
}
else {
file = null; // can't find, try next time
changed = true;
}
}
Pair<VirtualFile, String> result;
if (changed) {
myFileAndUrl = result = Pair.create(file, url);
}
else {
result = fileAndUrl;
}
myLastUpdated = fsModCount; // must be the last
return result;
}
void associate(Object leaves, Pair<VirtualFile, String> fileAndUrl) {
this.leaves = leaves;
myFileAndUrl = fileAndUrl;
// assign myNode last because .update() reads that field outside lock
if (leaves != null) {
if (leaves instanceof VirtualFilePointerImpl) {
((VirtualFilePointerImpl)leaves).myNode = this;
}
else {
for (VirtualFilePointerImpl pointer : (VirtualFilePointerImpl[])leaves) {
pointer.myNode = this;
}
}
}
myLastUpdated = -1;
}
int incrementUsageCount(int delta) {
return useCount += delta;
}
int numberOfPointersUnder() {
return pointersUnder;
}
VirtualFilePointerImpl getAnyPointer() {
Object leaves = this.leaves;
return leaves == null ? null : leaves instanceof VirtualFilePointerImpl ? (VirtualFilePointerImpl)leaves : ((VirtualFilePointerImpl[])leaves)[0];
}
private int leavesNumber() {
Object leaves = this.leaves;
return leaves == null ? 0 : leaves instanceof VirtualFilePointerImpl ? 1 : ((VirtualFilePointerImpl[])leaves).length;
}
void addAllPointersTo(@NotNull Collection<? super VirtualFilePointerImpl> outList) {
Object leaves = this.leaves;
if (leaves == null) {
return;
}
if (leaves instanceof VirtualFilePointerImpl) {
outList.add((VirtualFilePointerImpl)leaves);
}
else {
ContainerUtil.addAll(outList, (VirtualFilePointerImpl[])leaves);
}
}
@NotNull
FilePointerPartNode findOrCreateNodeByFile(@NotNull VirtualFile file, @NotNull NewVirtualFileSystem fs) {
int nameId = getNameId(file);
VirtualFile parent = getParentThroughJars(file, fs);
return matchById(parent, nameId, null, true, fs);
}
// for "file://a/b/c.txt" return "a/b", for "jar://a/b/j.jar!/c.txt" return "/a/b/j.jar"
private static VirtualFile getParentThroughJars(@NotNull VirtualFile file, @NotNull VirtualFileSystem fs) {
VirtualFile parent = file.getParent();
if (parent == null && fs instanceof ArchiveFileSystem) {
VirtualFile local = ((ArchiveFileSystem)fs).getLocalByEntry(file);
if (local != null) {
parent = local.getParent();
}
}
return parent;
}
@NotNull
static FilePointerPartNode findOrCreateNodeByPath(@NotNull FilePointerPartNode rootNode, @NotNull String path, @NotNull NewVirtualFileSystem fs) {
List<String> names = splitNames(path);
NewVirtualFile fsRoot = null;
VirtualFile NEVER_TRIED_TO_FIND = NullVirtualFile.INSTANCE;
// we try to never call file.findChild() because it's expensive
VirtualFile currentFile = NEVER_TRIED_TO_FIND;
FilePointerPartNode currentNode = rootNode;
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
int index = currentNode.binarySearchChildByName(name);
if (index >= 0) {
currentNode = currentNode.children[index];
//noinspection UseVirtualFileEquals
currentFile = currentFile == NEVER_TRIED_TO_FIND || currentFile == null ? currentFile : currentFile.findChild(name);
continue;
}
// create and insert new node
// first, have to check if the file root/names(end)/.../names[i] exists
// if yes, create nameId-based FilePointerPartNode (for faster search and memory efficiency),
// if not, create temp UrlPartNode which will be replaced with FPPN when the real file is created
//noinspection UseVirtualFileEquals
if (currentFile == NEVER_TRIED_TO_FIND) {
if (fsRoot == null) {
String rootPath = ContainerUtil.getLastItem(names);
fsRoot = ManagingFS.getInstance().findRoot(rootPath, fs instanceof ArchiveFileSystem ? LocalFileSystem.getInstance() : fs);
if (fsRoot != null && !fsRoot.getName().equals(rootPath)) {
// ignore really weird root names, like "/" under windows
fsRoot = null;
}
}
currentFile = fsRoot == null ? null : findFileFromRoot(fsRoot, fs, names, i);
}
else {
currentFile = currentFile == null ? null : currentFile.findChild(name);
}
FilePointerPartNode child = currentFile == null ? new UrlPartNode(name, currentNode)
: new FilePointerPartNode(getNameId(currentFile), currentNode);
currentNode.children = ArrayUtil.insert(currentNode.children, -index - 1, child);
currentNode = child;
if (i != 0 && fs instanceof ArchiveFileSystem && currentFile != null && !currentFile.isDirectory()) {
currentFile = ((ArchiveFileSystem)fs).getRootByLocal(currentFile);
}
}
return currentNode;
}
@NotNull
private static List<String> splitNames(@NotNull String path) {
List<String> names = new ArrayList<>(20);
int end = path.length();
if (end == 0) return names;
while (true) {
int startIndex = extractName(path, end);
assert startIndex != end : "startIndex: "+startIndex+"; end: "+end+"; path:'"+path+"'; toExtract: '"+path.substring(0, end)+"'";
names.add(path.substring(startIndex, end));
if (startIndex == 0) {
break;
}
int skipSeparator = StringUtil.endsWith(path, 0, startIndex, JarFileSystem.JAR_SEPARATOR) && startIndex > 2 && path.charAt(startIndex-3) != '/' ? 2 : 1;
end = startIndex - skipSeparator;
if (end == 0 && path.charAt(0) == '/') {
end = 1; // here's this weird ROOT file in temp system
}
}
return names;
}
private static VirtualFile findFileFromRoot(@NotNull NewVirtualFile root,
@NotNull NewVirtualFileSystem fs,
@NotNull List<String> names,
int startIndex) {
VirtualFile file = root;
// start from before-the-last because it's the root, which we already found
for (int i = names.size() - 2; i >= startIndex; i--) {
String name = names.get(i);
file = file.findChild(name);
if (fs instanceof ArchiveFileSystem && file != null && !file.isDirectory() && file.getFileSystem() != fs) {
file = ((ArchiveFileSystem)fs).getRootByLocal(file);
}
if (file == null) break;
}
return file;
}
}
@@ -18,14 +18,15 @@ package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import org.jetbrains.annotations.NotNull;
class IdentityVirtualFilePointer extends VirtualFilePointerImpl implements VirtualFilePointer, Disposable {
private final VirtualFile myFile;
private final String myUrl;
private volatile int useCount;
IdentityVirtualFilePointer(VirtualFile file, @NotNull String url) {
IdentityVirtualFilePointer(VirtualFile file, @NotNull String url, VirtualFilePointerListener listener) {
super(listener);
myFile = file;
myUrl = url;
}
@@ -58,11 +59,6 @@ class IdentityVirtualFilePointer extends VirtualFilePointerImpl implements Virtu
return myFile == null || myFile.isValid();
}
@Override
int incrementUsageCount(int delta) {
return useCount += delta;
}
@Override
public void dispose() {
incrementUsageCount(-1);
@@ -4,21 +4,23 @@ package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import org.jetbrains.annotations.NotNull;
/** Node which stores explicit 'name' instead of nameId.
/**
* Node which stores explicit 'name' instead of nameId.
* The latter can be unavailable (e.g. when creating the pointer from the url of non-yet-existing file)
* or incorrect (e.g. when creating the pointer from the url "/x/y/Z.TXT" for the file "z.txt" on case-insensitive file system)
* As soon as the corresponding file got created, this UrlPartNode is replaced with FilePointerPartNode, which contains nameId and is faster and more succinct
* As soon as the corresponding file got created, this UrlPartNode is replaced with FilePartNode, which contains nameId and is faster and more succinct
*/
class UrlPartNode extends FilePointerPartNode {
class UrlPartNode extends FilePartNode {
@NotNull
private final String name;
UrlPartNode(@NotNull String name, @NotNull FilePointerPartNode parent) {
super(parent);
UrlPartNode(@NotNull String name, @NotNull String parentUrl, @NotNull NewVirtualFileSystem fs) {
super(fs);
this.name = name;
myFileOrUrl = childUrl(parentUrl, name, fs);
if (SystemInfo.isUnix) {
if (name.isEmpty()) {
throw new IllegalArgumentException('\'' + name + '\'');
@@ -31,11 +33,6 @@ class UrlPartNode extends FilePointerPartNode {
}
}
@Override
boolean urlEndsWithName(@NotNull String urlAfter, VirtualFile fileAfter) {
return StringUtil.endsWith(urlAfter, getName());
}
@NotNull
@Override
CharSequence getName() {
@@ -18,10 +18,10 @@ package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TraceableDisposable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.PathUtil;
import org.jetbrains.annotations.NotNull;
@@ -31,61 +31,52 @@ class VirtualFilePointerImpl extends TraceableDisposable implements VirtualFileP
private static final boolean TRACE_CREATION = LOG.isDebugEnabled() || ApplicationManager.getApplication().isUnitTestMode();
volatile FilePointerPartNode myNode; // null means disposed
volatile FilePartNode myNode; // null means disposed
private int useCount;
boolean recursive; // true if the validityChanged() event should be fired for any change under this directory. Used for library jar directories.
final VirtualFilePointerListener myListener;
VirtualFilePointerImpl() {
VirtualFilePointerImpl(VirtualFilePointerListener listener) {
super(TRACE_CREATION);
myListener = listener;
}
@Override
@NotNull
public String getFileName() {
FilePointerPartNode node = checkDisposed(myNode);
FilePartNode node = checkDisposed(myNode);
if (node == null) return "";
Pair<VirtualFile, String> result = node.update();
if (result == null) return "";
VirtualFile file = result.first;
if (file != null) {
return file.getName();
Object result = node.myFileOrUrl;
if (result instanceof VirtualFile) {
return ((VirtualFile)result).getName();
}
String url = result.second;
String url = (String)result;
int index = url.lastIndexOf('/');
return index >= 0 ? url.substring(index + 1) : url;
}
@Override
public VirtualFile getFile() {
FilePointerPartNode node = checkDisposed(myNode);
FilePartNode node = checkDisposed(myNode);
if (node == null) return null;
Pair<VirtualFile, String> result = node.update();
return result == null ? null : result.first;
return FilePartNode.myFile(node.myFileOrUrl);
}
@Override
@NotNull
public String getUrl() {
FilePointerPartNode node = myNode;
FilePartNode node = myNode;
if (node == null) return "";
// optimization: when file is null we shouldn't try to do expensive findFileByUrl() just to return the url
Pair<VirtualFile, String> fileAndUrl = node.myFileAndUrl;
if (fileAndUrl != null && fileAndUrl.getFirst() == null) {
return fileAndUrl.getSecond();
}
Pair<VirtualFile, String> result = node.update();
return result == null ? "" : result.second;
return FilePartNode.myUrl(node.myFileOrUrl);
}
@Override
@NotNull
public String getPresentableUrl() {
FilePointerPartNode node = checkDisposed(myNode);
if (node == null) return "";
Pair<VirtualFile, String> result = node.update();
return result == null ? "" : PathUtil.toPresentableUrl(result.second);
return PathUtil.toPresentableUrl(getUrl());
}
private FilePointerPartNode checkDisposed(FilePointerPartNode node) {
private FilePartNode checkDisposed(FilePartNode node) {
if (node == null) {
ProgressManager.checkCanceled();
LOG.error("Already disposed: URL='" + this + "'");
@@ -96,16 +87,14 @@ class VirtualFilePointerImpl extends TraceableDisposable implements VirtualFileP
@Override
public boolean isValid() {
FilePointerPartNode node = myNode;
Pair<VirtualFile, String> result = node == null ? null : node.update();
return result != null && result.first != null;
FilePartNode node = myNode;
return node != null && FilePartNode.myFile(node.myFileOrUrl) != null;
}
@Override
public String toString() {
FilePointerPartNode node = myNode;
Pair<VirtualFile, String> fileAndUrl;
return node == null ? "(disposed)" : (fileAndUrl = node.myFileAndUrl) == null ? "?" : fileAndUrl.second;
FilePartNode node = myNode;
return node == null ? "(disposed)" : FilePartNode.myUrl(node.myFileOrUrl);
}
public void dispose() {
@@ -124,9 +113,7 @@ class VirtualFilePointerImpl extends TraceableDisposable implements VirtualFileP
}
int incrementUsageCount(int delta) {
FilePointerPartNode node = checkDisposed(myNode);
if (node == null) return 1;
return node.incrementUsageCount(delta);
return useCount += delta;
}
boolean isRecursive() {
@@ -12,7 +12,6 @@ import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
@@ -22,6 +21,7 @@ import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.AsyncFileListener.ChangeApplier;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
@@ -33,13 +33,10 @@ import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.io.URLUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -47,35 +44,32 @@ import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManager implements Disposable {
public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManager implements Disposable, BulkFileListener {
private static final Logger LOG = Logger.getInstance(VirtualFilePointerManagerImpl.class);
static final boolean IS_UNDER_UNIT_TEST = ApplicationManager.getApplication().isUnitTestMode();
private static final VirtualFilePointerListener NULL_LISTENER = new VirtualFilePointerListener() {
@Override
public int hashCode() {
return 4; // chosen by fair dice roll. guaranteed to be random. see https://xkcd.com/221/ for details.
}
@Override
public boolean equals(Object obj) {
return obj == this;
}
};
private final Map<VirtualFileSystem, Map<VirtualFilePointerListener, FilePointerPartNode>> myRoots = ContainerUtil.newIdentityTroveMap(); // guarded by this
/*
virtual file pointers are stored in a trie structure rooted either here in myLocalRoot or in myTempRoot.
vfp for a local file "file://c:/temp/x.txt" is stored in myLocalRoot->FilePartNode(c:)->FilePartNode(temp)->FilePartNode(x.txt)
vfp for jar://c:/temp/x.jar!/META-INF" is stored in myLocalRoot->FilePartNode(c:)->FilePartNode(temp)->FilePartNode(x.jar)->FilePartNode(!/)->FilePartNode(META-INF)
When the corresponding virtual file doesn't exist on disk, UrlPartNode is used instead of FilePartNode and replaced with the latter as soon as the file is created
*/
private final FilePartNodeRoot myLocalRoot = FilePartNodeRoot.createFakeRoot(LocalFileSystem.getInstance()); // guarded by this
private final FilePartNodeRoot myTempRoot = FilePartNodeRoot.createFakeRoot(TempFileSystem.getInstance()); // guarded by this
// compare by identity because VirtualFilePointerContainer has too smart equals
private final Set<VirtualFilePointerContainerImpl> myContainers = ContainerUtil.newIdentityTroveSet(); // guarded by myContainers
private final @NotNull VirtualFilePointerListener myPublisher;
private int myPointerSetModCount;
private volatile CollectedEvents myCollectedEvents;
static final class MyAsyncFileListener implements AsyncFileListener {
@Override
public ChangeApplier prepareChange(@NotNull List<? extends VFileEvent> events) {
return ((VirtualFilePointerManagerImpl)getInstance()).prepareChange(events);
}
public VirtualFilePointerManagerImpl() {
myPublisher = ApplicationManager.getApplication().getMessageBus().syncPublisher(VirtualFilePointerListener.TOPIC);
}
public void addAsyncFileListenerTo(@NotNull List<? super AsyncFileListener> listeners) {
listeners.add((AsyncFileListener)events -> prepareChange(events));
}
@Override
@@ -102,52 +96,30 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
}
}
private static VirtualFilePointer @NotNull [] toPointers(@NotNull Collection<? extends FilePointerPartNode> nodes) {
if (nodes.isEmpty()) return VirtualFilePointer.EMPTY_ARRAY;
List<VirtualFilePointer> list = new ArrayList<>(nodes.size());
for (FilePointerPartNode node : nodes) {
node.addAllPointersTo(list);
}
return list.toArray(VirtualFilePointer.EMPTY_ARRAY);
}
@TestOnly
@NotNull
synchronized List<VirtualFilePointer> getPointersUnder(@NotNull VirtualFileSystemEntry parent, @NotNull String childName) {
synchronized Collection<? extends VirtualFilePointer> getPointersUnder(@NotNull VirtualFileSystemEntry parent, @NotNull String childName) {
assert !StringUtil.isEmptyOrSpaces(childName);
MultiMap<VirtualFilePointerListener, FilePointerPartNode> nodes = MultiMap.create();
addRelevantPointers(parent, toNameId(childName), nodes, true, parent.getFileSystem());
List<VirtualFilePointer> pointers = new ArrayList<>();
for (FilePointerPartNode node : nodes.values()) {
node.addAllPointersTo(pointers);
}
return pointers;
@NotNull MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> nodes = MultiMap.create();
addRelevantPointers(null, parent, toNameId(childName), nodes, new ArrayList<>(), true, parent.getFileSystem());
return nodes.values();
}
private void addRelevantPointers(@NotNull VirtualFileSystemEntry parent,
private void addRelevantPointers(@Nullable VirtualFile file,
@NotNull VirtualFileSystemEntry parent,
int childNameId,
@NotNull MultiMap<VirtualFilePointerListener, FilePointerPartNode> out,
@NotNull MultiMap<? super VirtualFilePointerListener, ? super VirtualFilePointerImpl> toFirePointers,
@NotNull List<? super NodeToUpdate> toUpdateNodes,
boolean addSubdirectoryPointers,
@NotNull VirtualFileSystem fs) {
@NotNull NewVirtualFileSystem fs) {
if (childNameId <= 0) throw new IllegalArgumentException("invalid argument childNameId: "+childNameId);
Map<VirtualFilePointerListener, FilePointerPartNode> myPointers = myRoots.get(fs);
if (myPointers != null) {
for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
FilePointerPartNode root = entry.getValue();
VirtualFilePointerListener listener = entry.getKey();
List<FilePointerPartNode> outNodes = (List<FilePointerPartNode>)out.getModifiable(listener);
root.addRelevantPointersFrom(parent, childNameId, outNodes, addSubdirectoryPointers, fs);
}
}
if (fs instanceof LocalFileSystem) {
// search in archive file systems because they might be changed too when the LFS is changed
for (Map.Entry<VirtualFileSystem, Map<VirtualFilePointerListener, FilePointerPartNode>> entry : myRoots.entrySet()) {
VirtualFileSystem rootFS = entry.getKey();
if (rootFS instanceof ArchiveFileSystem) {
addRelevantPointers(parent, childNameId, out, addSubdirectoryPointers, rootFS);
}
}
}
getRoot(fs).addRelevantPointersFrom(parent, file, childNameId, toFirePointers, toUpdateNodes, addSubdirectoryPointers, fs);
}
@NotNull
private FilePartNodeRoot getRoot(@NotNull NewVirtualFileSystem fs) {
// have to have at least two roots unfortunately: for the local and temp file systems because their paths can overlap
return fs instanceof TempFileSystem ? myTempRoot : myLocalRoot;
}
@Override
@@ -211,7 +183,7 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
if (url == null) {
url = file.getUrl();
}
return getOrCreateIdentity(url, found, recursive, parentDisposable);
return getOrCreateIdentity(url, found, recursive, parentDisposable, listener);
}
if (file == null) {
@@ -219,7 +191,8 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
// if newly created path is the same as the one extracted from url then the url did not change, we can reuse it
//noinspection StringEquality
if (cleanPath != path) {
url = VirtualFileManager.constructUrl(protocol, cleanPath + (fileSystem instanceof ArchiveFileSystem ? JarFileSystem.JAR_SEPARATOR : ""));
String fsSpecificPath = cleanPath + (fileSystem instanceof ArchiveFileSystem && !cleanPath.contains(JarFileSystem.JAR_SEPARATOR) ? JarFileSystem.JAR_SEPARATOR : "");
url = VirtualFileManager.constructUrl(protocol, fsSpecificPath);
path = cleanPath;
}
if (url.contains("..")) {
@@ -237,7 +210,7 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
}
if (file == null && StringUtil.isEmptyOrSpaces(path)) {
// somebody tries to create pointer to root which is pointless but damages our fake root node.
return getOrCreateIdentity(url, VirtualFileManager.getInstance().findFileByUrl(url), recursive, parentDisposable);
return getOrCreateIdentity(url, VirtualFileManager.getInstance().findFileByUrl(url), recursive, parentDisposable, listener);
}
}
// else url has come from VirtualFile.getPath() and is good enough
@@ -250,10 +223,11 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
private synchronized IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url,
@Nullable VirtualFile found,
boolean recursive,
@NotNull Disposable parentDisposable) {
@NotNull Disposable parentDisposable,
@Nullable VirtualFilePointerListener listener) {
IdentityVirtualFilePointer pointer = myUrlToIdentity.get(url);
if (pointer == null) {
pointer = new IdentityVirtualFilePointer(found, url) {
pointer = new IdentityVirtualFilePointer(found, url, listener) {
@Override
public void dispose() {
//noinspection SynchronizeOnThis
@@ -350,23 +324,24 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
@NotNull Disposable parentDisposable,
@Nullable VirtualFilePointerListener listener,
@NotNull NewVirtualFileSystem fs) {
VirtualFilePointerListener nl = ObjectUtils.notNull(listener, NULL_LISTENER);
Map<VirtualFilePointerListener, FilePointerPartNode> myPointers = myRoots.computeIfAbsent(fs, __ -> new THashMap<>());
FilePointerPartNode root = myPointers.computeIfAbsent(nl, __ -> FilePointerPartNode.createFakeRoot());
VirtualFileSystem fsFromFile = file == null ? VirtualFileManager.getInstance().getFileSystem(VirtualFileManager.extractProtocol(url)) : file.getFileSystem();
assert fs == fsFromFile : "fs=" + fs + "; file.fs=" + fsFromFile;
FilePointerPartNode node = file == null ? FilePointerPartNode.findOrCreateNodeByPath(root, path, fs) : root.findOrCreateNodeByFile(file, fs);
FilePartNodeRoot root = getRoot(fs);
FilePartNode node = (file == null ? root
.findOrCreateByPath(fs instanceof ArchiveFileSystem && !path.contains(JarFileSystem.JAR_SEPARATOR) ? path + JarFileSystem.JAR_SEPARATOR : path, fs)
: root.findOrCreateByFile(file)).node;
assert fs == node.myFS : "fs=" + fs + "; myFS=" + node.myFS;
VirtualFilePointerImpl pointer = node.getAnyPointer();
VirtualFilePointerImpl pointer = node.getPointer(listener);
if (pointer == null) {
pointer = new VirtualFilePointerImpl();
Pair<VirtualFile, String> fileAndUrl = Pair.create(file, file == null ? url : file.getUrl());
node.associate(pointer, fileAndUrl);
for (FilePointerPartNode n=node; n!= null; n=n.parent) {
n.pointersUnder++;
}
pointer = new VirtualFilePointerImpl(listener);
node.addLeaf(pointer);
}
pointer.incrementUsageCount(1);
pointer.recursive = recursive;
if (!pointer.recursive) {
pointer.recursive = recursive;
}
root.checkConsistency();
DelegatingDisposable.registerDisposable(parentDisposable, pointer);
@@ -441,29 +416,60 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
return container;
}
private static class CollectedEvents {
private final @NotNull MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> toFirePointers;
private final @NotNull List<NodeToUpdate> toUpdateNodes;
private @NotNull final List<EventDescriptor> eventList;
private final long startModCount;
private final long prepareElapsedMs;
CollectedEvents(@NotNull MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> toFirePointers,
@NotNull List<NodeToUpdate> toUpdateNodes,
@NotNull List<EventDescriptor> eventList,
long startModCount,
long prepareElapsedMs) {
this.toFirePointers = toFirePointers;
this.toUpdateNodes = toUpdateNodes;
this.eventList = eventList;
this.startModCount = startModCount;
this.prepareElapsedMs = prepareElapsedMs;
}
}
static class NodeToUpdate {
private final FilePartNode parent;
final FilePartNode node;
NodeToUpdate(@NotNull FilePartNode parent, @NotNull FilePartNode node) {
this.parent = parent;
this.node = node;
}
}
@NotNull
public ChangeApplier prepareChange(@NotNull List<? extends VFileEvent> events) {
private CollectedEvents collectEvents(@NotNull List<? extends VFileEvent> events) {
long start = System.currentTimeMillis();
MultiMap<VirtualFilePointerListener, FilePointerPartNode> toFireEvents = MultiMap.create();
MultiMap<VirtualFilePointerListener, FilePointerPartNode> toUpdateUrl = MultiMap.create();
List<EventDescriptor> eventList;
List<VirtualFilePointer> allPointersToFire = new ArrayList<>();
MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> toFirePointers = MultiMap.create();
List<NodeToUpdate> toUpdateNodes = new ArrayList<>();
long startModCount;
List<EventDescriptor> eventList;
List<VirtualFilePointer> allPointersToFire;
//noinspection SynchronizeOnThis
synchronized (this) {
startModCount = myPointerSetModCount;
for (VFileEvent event : events) {
ProgressManager.checkCanceled();
VirtualFileSystem fs = event.getFileSystem();
if (!(fs instanceof VirtualFilePointerCapableFileSystem)) continue;
VirtualFileSystem vfs = event.getFileSystem();
if (!(vfs instanceof VirtualFilePointerCapableFileSystem) || !(vfs instanceof NewVirtualFileSystem)) continue;
NewVirtualFileSystem fs = (NewVirtualFileSystem)vfs;
if (event instanceof VFileDeleteEvent) {
VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
VirtualFileSystemEntry file = (VirtualFileSystemEntry)deleteEvent.getFile();
VirtualFileSystemEntry parent = file.getParent();
if (parent != null) {
addRelevantPointers(parent, file.getNameId(), toFireEvents, true, fs);
addRelevantPointers(file, parent, file.getNameId(), toFirePointers, toUpdateNodes, true, fs);
}
}
else if (event instanceof VFileCreateEvent) {
@@ -480,101 +486,97 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
FileType fileType = FileTypeManager.getInstance().getFileTypeByExtension(FileUtilRt.getExtension(createdFileName));
fireSubdirectoryPointers = fileType instanceof ArchiveFileType;
}
addRelevantPointers((VirtualFileSystemEntry)createEvent.getParent(), createEvent.getChildNameId(), toFireEvents, fireSubdirectoryPointers, fs);
// when new file created its UrlPartNode should be converted to id-based FilePointerPartNode to save memory
toUpdateUrl.putAllValues(toFireEvents);
addRelevantPointers(null, (VirtualFileSystemEntry)createEvent.getParent(), createEvent.getChildNameId(), toFirePointers,
toUpdateNodes, fireSubdirectoryPointers, fs);
}
else if (event instanceof VFileCopyEvent) {
VFileCopyEvent copyEvent = (VFileCopyEvent)event;
addRelevantPointers((VirtualFileSystemEntry)copyEvent.getNewParent(), toNameId(copyEvent.getNewChildName()), toFireEvents, true, fs);
addRelevantPointers(null, (VirtualFileSystemEntry)copyEvent.getNewParent(), toNameId(copyEvent.getNewChildName()), toFirePointers,
toUpdateNodes, true, fs);
}
else if (event instanceof VFileMoveEvent) {
VFileMoveEvent moveEvent = (VFileMoveEvent)event;
VirtualFileSystemEntry eventFile = (VirtualFileSystemEntry)moveEvent.getFile();
int newNameId = eventFile.getNameId();
addRelevantPointers((VirtualFileSystemEntry)moveEvent.getNewParent(), newNameId, toFireEvents, true, fs);
// files deleted from eventFile and created in moveEvent.getNewParent()
addRelevantPointers(null, (VirtualFileSystemEntry)moveEvent.getNewParent(), newNameId, toFirePointers, toUpdateNodes, true, fs);
MultiMap<VirtualFilePointerListener, FilePointerPartNode> nodes = MultiMap.create();
VirtualDirectoryImpl parent = eventFile.getParent();
if (parent != null) {
addRelevantPointers(parent, newNameId, nodes, true, fs);
addRelevantPointers(eventFile, parent, newNameId, toFirePointers, toUpdateNodes, true, fs);
}
toFireEvents.putAllValues(nodes); // files deleted from eventFile and created in moveEvent.getNewParent()
collectNodes(nodes, toUpdateUrl);
}
else if (event instanceof VFilePropertyChangeEvent) {
VFilePropertyChangeEvent change = (VFilePropertyChangeEvent)event;
if (VirtualFile.PROP_NAME.equals(change.getPropertyName())
&& !Comparing.equal(change.getOldValue(), change.getNewValue())) {
if (VirtualFile.PROP_NAME.equals(change.getPropertyName()) && !Comparing.equal(change.getOldValue(), change.getNewValue())) {
VirtualFileSystemEntry eventFile = (VirtualFileSystemEntry)change.getFile();
VirtualDirectoryImpl parent = eventFile.getParent(); // e.g. for LightVirtualFiles
if (parent != null) {
int newNameId = toNameId(change.getNewValue().toString());
addRelevantPointers(parent, newNameId, toFireEvents, true, fs);
addRelevantPointers(eventFile, parent, newNameId, toFirePointers, toUpdateNodes, true, fs);
MultiMap<VirtualFilePointerListener, FilePointerPartNode> nodes = MultiMap.create();
addRelevantPointers(parent, eventFile.getNameId(), nodes, true, fs);
collectNodes(nodes, toUpdateUrl);
// old pointers remain valid after rename, no need to fire
addRelevantPointers(eventFile, parent, eventFile.getNameId(), new MultiMap<>(), toUpdateNodes, true, fs);
}
}
}
}
eventList = new ArrayList<>();
for (Map.Entry<VirtualFilePointerListener, Collection<FilePointerPartNode>> entry : toFireEvents.entrySet()) {
VirtualFilePointerListener listener = entry.getKey();
if (listener == NULL_LISTENER) continue;
Collection<FilePointerPartNode> values = entry.getValue();
VirtualFilePointer[] toFirePointers = toPointers(values);
if (toFirePointers.length != 0) {
eventList.add(new EventDescriptor(listener, toFirePointers));
ContainerUtil.addAll(allPointersToFire, toFirePointers);
}
}
allPointersToFire = new ArrayList<>();
groupPointersToFire(toFirePointers, eventList, allPointersToFire);
}
if (!allPointersToFire.isEmpty()) {
VirtualFilePointer[] allPointers = allPointersToFire.toArray(VirtualFilePointer.EMPTY_ARRAY);
eventList.add(new EventDescriptor(myPublisher, allPointers));
}
long prepareElapsedMs = System.currentTimeMillis() - start;
VirtualFilePointer[] allPointers = allPointersToFire.isEmpty() ? VirtualFilePointer.EMPTY_ARRAY : allPointersToFire.toArray(VirtualFilePointer.EMPTY_ARRAY);
return new CollectedEvents(toFirePointers, toUpdateNodes, eventList, startModCount, prepareElapsedMs);
}
// converts multimap with pointers to fire into a convenient
// - (listener->pointers created with this listener) map for firing individual listeners and
// - allPointersToFire list to fire in bulk via VirtualFilePointerListener.TOPIC
private static void groupPointersToFire(@NotNull MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> toFirePointers,
@NotNull List<? super EventDescriptor> eventList,
@NotNull List<? super VirtualFilePointer> allPointersToFire) {
for (Map.Entry<VirtualFilePointerListener, Collection<VirtualFilePointerImpl>> entry : toFirePointers.entrySet()) {
VirtualFilePointerListener listener = entry.getKey();
if (listener == null) continue;
Collection<VirtualFilePointerImpl> values = entry.getValue();
VirtualFilePointerImpl[] array = values.toArray(new VirtualFilePointerImpl[0]);
if (array.length != 0) {
eventList.add(new EventDescriptor(listener, array));
ContainerUtil.addAll(allPointersToFire, array);
}
}
}
@NotNull
public ChangeApplier prepareChange(@NotNull List<? extends VFileEvent> events) {
myCollectedEvents = collectEvents(events);
return new ChangeApplier() {
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized") private ChangeApplier delegate;
@Override
public void beforeVfsChange() {
//noinspection SynchronizeOnThis
CollectedEvents collected;
synchronized (VirtualFilePointerManagerImpl.this) {
if (startModCount != myPointerSetModCount) {
delegate = prepareChange(events);
} else {
collected = myCollectedEvents;
if (collected.startModCount == myPointerSetModCount) {
incModificationCount();
}
else {
myCollectedEvents = collected = collectEvents(events);
}
}
if (delegate != null) {
delegate.beforeVfsChange();
return;
}
for (EventDescriptor descriptor : eventList) {
for (EventDescriptor descriptor : collected.eventList) {
descriptor.fireBefore();
}
if (allPointers.length != 0) {
ApplicationManager.getApplication().getMessageBus()
.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(allPointers);
}
assertConsistency();
}
@Override
public void afterVfsChange() {
if (delegate != null) {
delegate.afterVfsChange();
return;
}
after(toFireEvents, toUpdateUrl, eventList, allPointers, prepareElapsedMs, events.size());
}
};
}
@@ -582,137 +584,84 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
return FileNameCache.storeName(name);
}
private static void collectNodes(@NotNull MultiMap<VirtualFilePointerListener, FilePointerPartNode> nodes, @NotNull MultiMap<VirtualFilePointerListener, FilePointerPartNode> toUpdateUrl) {
for (Map.Entry<VirtualFilePointerListener, Collection<FilePointerPartNode>> entry : nodes.entrySet()) {
VirtualFilePointerListener listener = entry.getKey();
Collection<FilePointerPartNode> values = entry.getValue();
for (FilePointerPartNode node : values) {
VirtualFilePointerImpl pointer = node.getAnyPointer();
if (pointer != null) {
VirtualFile file = pointer.getFile();
if (file != null) {
toUpdateUrl.putValue(listener, node);
}
}
}
}
}
synchronized void assertConsistency() {
if (IS_UNDER_UNIT_TEST && !ApplicationInfoImpl.isInStressTest()) {
for (Map<VirtualFilePointerListener, FilePointerPartNode> myPointers : myRoots.values()) {
for (FilePointerPartNode root : myPointers.values()) {
root.checkConsistency();
}
}
myLocalRoot.checkConsistency();
myTempRoot.checkConsistency();
}
}
private void after(@NotNull MultiMap<VirtualFilePointerListener, FilePointerPartNode> toFireEvents,
@NotNull MultiMap<VirtualFilePointerListener, FilePointerPartNode> toUpdateUrls,
@NotNull List<? extends EventDescriptor> eventList,
VirtualFilePointer @NotNull [] allPointers,
long prepareElapsedMs,
int eventsSize) {
@Override
public void before(@NotNull List<? extends VFileEvent> events) {
if (myCollectedEvents == null) {
myCollectedEvents = collectEvents(events);
}
}
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
after(events.size());
}
private void after(int eventsSize) {
CollectedEvents collectedEvents = myCollectedEvents;
if (collectedEvents == null) {
// some crazy invalid event nesting happened, like in DbSrcFileSystem.getOutputStream
// where refresh is called right inside stream.close(), leading to before(), before(), after(), after() events
return;
}
myCollectedEvents = null;
long start = System.currentTimeMillis();
ApplicationManager.getApplication().assertIsWriteThread(); // guarantees no attempts to get read action lock under "this" lock
incModificationCount();
VirtualFileManager virtualFileManager = VirtualFileManager.getInstance();
//noinspection SynchronizeOnThis
synchronized (this) {
for (FilePointerPartNode node : toUpdateUrls.values()) {
Pair<VirtualFile, String> pairBefore = node.myFileAndUrl;
if (pairBefore == null) continue; // disposed in the meantime
String urlBefore = pairBefore.second;
Pair<VirtualFile, String> after = node.update();
assert after != null : "can't invalidate inside modification";
String urlAfter = after.second;
VirtualFile fileAfter = after.first;
// exact comparision because we want to fire events on rename even under case-insensitive FS
if (!urlBefore.equals(urlAfter) || !node.urlEndsWithName(urlAfter, fileAfter)) {
VirtualFileSystem fs = virtualFileManager.getFileSystem(VirtualFileManager.extractProtocol(urlAfter));
if (fs instanceof NewVirtualFileSystem) {
List<VirtualFilePointerImpl> myPointers = new SmartList<>();
node.addAllPointersTo(myPointers);
for (NodeToUpdate toUpdate : collectedEvents.toUpdateNodes) {
FilePartNode parent = toUpdate.parent;
FilePartNode node = toUpdate.node;
// url has changed, reinsert
int useCount = node.useCount;
FilePointerPartNode root = node.remove();
String path = trimTrailingSeparators(VfsUtilCore.urlToPath(urlAfter));
FilePointerPartNode newNode = fileAfter == null ? FilePointerPartNode.findOrCreateNodeByPath(root, path, (NewVirtualFileSystem)fs)
: root.findOrCreateNodeByFile(fileAfter, (NewVirtualFileSystem)fs);
newNode.addAllPointersTo(myPointers);
int pointersDelta = myPointers.size() - newNode.pointersUnder;
Object newMyPointers = myPointers.size() == 1 ? myPointers.get(0) : myPointers.toArray(new VirtualFilePointerImpl[0]);
newNode.associate(newMyPointers, after);
newNode.incrementUsageCount(useCount);
for (FilePointerPartNode n = newNode; n != null; n = n.parent) {
n.pointersUnder += pointersDelta;
}
}
}
node.update(parent, getRoot(node.myFS));
}
}
for (FilePointerPartNode node : toFireEvents.values()) {
node.update();
}
for (EventDescriptor event : eventList) {
for (EventDescriptor event : collectedEvents.eventList) {
event.fireAfter();
}
if (allPointers.length != 0) {
ApplicationManager.getApplication().getMessageBus().syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(allPointers);
}
assertConsistency();
long afterElapsedMs = System.currentTimeMillis() - start;
if (afterElapsedMs > 1000 || prepareElapsedMs > 1000) {
int totalPointers;
synchronized (this) {
totalPointers = myRoots.values().stream().flatMapToInt(myPointers->myPointers.values().stream().mapToInt(root -> root.pointersUnder)).sum();
}
LOG.warn("VirtualFilePointerManagerImpl.prepareChange("+eventsSize+" events): "+prepareElapsedMs+"ms. total pointers: "+totalPointers
+"; afterElapsedMs: "+afterElapsedMs+"ms.; eventList.size(): "+eventList.size()+
"; toFireEvents.size(): "+toFireEvents.size()+"; toUpdateUrls.size(): "+toUpdateUrls.size()+"; eventList: "+
ContainerUtil.getFirstItems(eventList, 100));
if (afterElapsedMs > 1000 || collectedEvents.prepareElapsedMs > 1000) {
LOG.warn("VirtualFilePointerManagerImpl.prepareChange(" + eventsSize + " events): " + collectedEvents.prepareElapsedMs + "ms."
+ "; total pointers: " + numberOfPointers()
+ "; afterElapsedMs: " + afterElapsedMs + "ms.; eventList.size(): " + collectedEvents.eventList.size() +
"; toFirePointers.size(): " + collectedEvents.toFirePointers.size() + "; toUpdateNodes.size(): " + collectedEvents.toUpdateNodes
.size() + "; eventList: " +
ContainerUtil.getFirstItems(collectedEvents.eventList, 100));
}
}
synchronized boolean decrementUsageCount(@NotNull VirtualFilePointerImpl pointer) {
boolean shouldKill = pointer.incrementUsageCount(-1) == 0;
if (shouldKill) {
removeNodeFrom(pointer);
}
return shouldKill;
}
private void removeNodeFrom(@NotNull VirtualFilePointerImpl pointer) {
FilePointerPartNode root = pointer.myNode.remove();
boolean rootNodeEmpty = root.children.length == 0 ;
if (rootNodeEmpty) {
for (Map<VirtualFilePointerListener, FilePointerPartNode> myPointers : myRoots.values()) {
myPointers.values().remove(root);
}
if (!shouldKill) {
return false;
}
getRoot(pointer.myNode.myFS).removePointer(pointer);
pointer.myNode = null;
assertConsistency();
myPointerSetModCount++;
return true;
}
@Override
public long getModificationCount() {
// depends on PersistentFS.getStructureModificationCount() - because com.intellij.openapi.vfs.impl.FilePointerPartNode.update does
// depends on PersistentFS.getStructureModificationCount() - because com.intellij.openapi.vfs.impl.FilePartNode.update does
// depends on its own modification counter - because we need to change both before and after VFS changes
return super.getModificationCount() + PersistentFS.getInstance().getStructureModificationCount();
}
private static class DelegatingDisposable implements Disposable {
private static final ConcurrentMap<Disposable, DelegatingDisposable> ourInstances = ConcurrentCollectionFactory.createMap(ContainerUtil.identityStrategy());
private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts = new TObjectIntHashMap<>(ContainerUtil.identityStrategy()); // guarded by this
private final Disposable myParent;
@@ -774,18 +723,12 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
@TestOnly
synchronized int numberOfPointers() {
int number = 0;
for (Map<VirtualFilePointerListener, FilePointerPartNode> myPointers : myRoots.values()) {
for (FilePointerPartNode root : myPointers.values()) {
number += root.numberOfPointersUnder();
}
}
return number;
return dumpAllPointers().size();
}
@TestOnly
synchronized int numberOfListeners() {
return myRoots.values().stream().flatMap(myPointers -> myPointers.keySet().stream()).collect(Collectors.toSet()).size();
return ContainerUtil.count(dumpAllPointers(), pointer -> ((VirtualFilePointerImpl)pointer).myListener != null);
}
@TestOnly
@@ -793,14 +736,14 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
return myUrlToIdentity.size();
}
// tests need to operate on the deterministic number of pointers, so we clear all of them out of the way during the test execution
// some tests need to operate on the deterministic number of pointers, so we clear all of them out of the way during the test execution
@TestOnly
void shelveAllPointersIn(@NotNull Runnable runnable) {
Map<VirtualFileSystem, Map<VirtualFilePointerListener, FilePointerPartNode>> shelvedPointers;
FilePartNode[] oldChildren;
//noinspection SynchronizeOnThis
synchronized (this) {
shelvedPointers = new LinkedHashMap<>(myRoots);
myRoots.clear();
oldChildren = myLocalRoot.children;
myLocalRoot.children = new FilePartNode[0];
}
try {
runnable.run();
@@ -808,26 +751,22 @@ public final class VirtualFilePointerManagerImpl extends VirtualFilePointerManag
finally {
//noinspection SynchronizeOnThis
synchronized (this) {
myRoots.clear();
myRoots.putAll(shelvedPointers);
myLocalRoot.children = oldChildren;
}
}
}
@NotNull
synchronized Collection<VirtualFilePointer> dumpAllPointers() {
Collection<VirtualFilePointer> result = new THashSet<>();
for (Map<VirtualFilePointerListener, FilePointerPartNode> myPointers : myRoots.values()) {
for (FilePointerPartNode node : myPointers.values()) {
dumpPointersRecursivelyTo(node, result);
}
}
Collection<VirtualFilePointer> result = new ArrayList<>();
dumpPointersRecursivelyTo(myLocalRoot, result);
dumpPointersRecursivelyTo(myTempRoot, result);
return result;
}
private static void dumpPointersRecursivelyTo(@NotNull FilePointerPartNode node, @NotNull Collection<? super VirtualFilePointer> result) {
private static void dumpPointersRecursivelyTo(@NotNull FilePartNode node, @NotNull Collection<? super VirtualFilePointer> result) {
node.addAllPointersTo(result);
for (FilePointerPartNode child : node.children) {
for (FilePartNode child : node.children) {
dumpPointersRecursivelyTo(child, result);
}
}
@@ -12,8 +12,10 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.AsyncFileListener;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.impl.VirtualFileManagerImpl;
import com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -66,9 +68,11 @@ public final class AsyncEventSupport {
}
List<AsyncFileListener.ChangeApplier> appliers = new ArrayList<>();
List<AsyncFileListener> allListeners = ContainerUtil.concat(
EP_NAME.getExtensionList(),
((VirtualFileManagerImpl)VirtualFileManager.getInstance()).getAsyncFileListeners());
List<AsyncFileListener> allListeners = new ArrayList<>();
// must be the first
((VirtualFilePointerManagerImpl)VirtualFilePointerManager.getInstance()).addAsyncFileListenerTo(allListeners);
allListeners.addAll(EP_NAME.getExtensionList());
((VirtualFileManagerImpl)VirtualFileManager.getInstance()).addAsyncFileListenersTo(allListeners);
for (AsyncFileListener listener : allListeners) {
ProgressManager.checkCanceled();
long startNs = System.nanoTime();
@@ -26,6 +26,7 @@ import com.intellij.openapi.vfs.impl.win32.Win32LocalFileSystem;
import com.intellij.openapi.vfs.newvfs.*;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.newvfs.impl.*;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.*;
import com.intellij.util.containers.*;
import com.intellij.util.io.ReplicatorInputStream;
@@ -673,7 +674,7 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable {
VFileContentChangeEvent event = new VFileContentChangeEvent(requestor, file, file.getModificationStamp(), modStamp, false);
List<VFileContentChangeEvent> events = Collections.singletonList(event);
getPublisher().before(events);
fireBeforeEvents(getPublisher(), events);
NewVirtualFileSystem delegate = getDelegate(file);
// FSRecords.ContentOutputStream already buffered, no need to wrap in BufferedStream
@@ -692,7 +693,7 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable {
attributes != null ? attributes.length : DEFAULT_LENGTH,
// due to fs rounding timestamp of written file can be significantly different from current time
attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP);
getPublisher().after(events);
fireAfterEvents(getPublisher(), events);
}
}
}
@@ -732,11 +733,11 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable {
BulkFileListener publisher = getPublisher();
if (jarDeleteEvents.isEmpty() && outApplyActions.isEmpty()) {
// optimisation: skip all groupings
publisher.before(outValidatedEvents);
fireBeforeEvents(publisher, outValidatedEvents);
applyEvent(event);
publisher.after(outValidatedEvents);
fireAfterEvents(publisher, outValidatedEvents);
}
else {
outApplyActions.add(() -> applyEvent(event));
@@ -1026,12 +1027,22 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable {
PingProgress.interactWithEdtProgress();
// do defensive copy to cope with ill-written listeners that save passed list for later processing
List<VFileEvent> toSend = ContainerUtil.immutableList(applyEvents.toArray(new VFileEvent[0]));
publisher.before(toSend);
fireBeforeEvents(publisher, toSend);
PingProgress.interactWithEdtProgress();
applyActions.forEach(Runnable::run);
PingProgress.interactWithEdtProgress();
fireAfterEvents(publisher, toSend);
}
private static void fireBeforeEvents(@NotNull BulkFileListener publisher, @NotNull List<? extends VFileEvent> toSend) {
publisher.before(toSend);
((BulkFileListener)VirtualFilePointerManager.getInstance()).before(toSend);
}
private static void fireAfterEvents(@NotNull BulkFileListener publisher, @NotNull List<? extends VFileEvent> toSend) {
((BulkFileListener)VirtualFilePointerManager.getInstance()).after(toSend);
publisher.after(toSend);
}
@@ -65,7 +65,6 @@
<applicationService serviceInterface="com.intellij.openapi.vfs.pointers.VirtualFilePointerManager"
serviceImplementation="com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl"/>
<vfs.asyncListener implementation="com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl$MyAsyncFileListener"/>
<applicationService serviceImplementation="com.intellij.util.net.HttpConfigurable" preload="await"/>
<applicationService serviceImplementation="com.intellij.ide.ClipboardSynchronizer" preload="true"/>
@@ -12,11 +12,15 @@ import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.roots.libraries.LibraryUtil;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
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.persistent.PersistentFSImpl;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
@@ -28,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@RunFirst
@@ -111,17 +116,52 @@ public class VirtualFilePointerRootsTest extends HeavyPlatformTestCase {
String name = "xxx" + (i%20);
events.add(new VFileCreateEvent(this, temp, name, true, null, null, true, null));
}
PlatformTestUtil.startPerformanceTest("vfp update", 7_500, () -> {
PlatformTestUtil.startPerformanceTest("vfp update", 3_000, () -> {
for (int i = 0; i < 100; i++) {
// simulate VFS refresh events since launching the actual refresh is too slow
AsyncFileListener.ChangeApplier applier = myVirtualFilePointerManager.prepareChange(events);
applier.beforeVfsChange();
applier.afterVfsChange();
myVirtualFilePointerManager.after(events);
}
}).assertTiming();
});
}
public void testUpdatePerformanceOfFewLongPointers() throws IOException {
VirtualFile root = TempFileSystem.getInstance().findFileByPath("/");
VirtualFile d = root;
for (int i=0;i <20; i++) {
d = VfsTestUtil.createDir(d, "directory" + i);
}
VirtualFile dir = d;
VirtualFile f = WriteAction.compute(() -> dir.createChildData(this, "file.txt"));
VirtualFilePointer pointer = myVirtualFilePointerManager.create(dir.getUrl()+"/file.txt", disposable, new VirtualFilePointerListener() {});
FileAttributes attributes = new FileAttributes(false, false, false, false, 0, 1, true);
List<VFileEvent> createEvents = Collections.singletonList(new VFileCreateEvent(this, dir, "file.txt", false, attributes, null, true, null));
List<VFileEvent> deleteEvents = Collections.singletonList(new VFileDeleteEvent(this, f, true));
PersistentFSImpl persistentFS = (PersistentFSImpl)ManagingFS.getInstance();
PlatformTestUtil.startPerformanceTest("update()", 5000, () -> {
for (int i=0; i<500_000; i++) {
persistentFS.incStructuralModificationCount();
AsyncFileListener.ChangeApplier applier = myVirtualFilePointerManager.prepareChange(createEvents);
applier.beforeVfsChange();
applier.afterVfsChange();
myVirtualFilePointerManager.after(createEvents);
persistentFS.incStructuralModificationCount();
AsyncFileListener.ChangeApplier applier2 = myVirtualFilePointerManager.prepareChange(deleteEvents);
applier2.beforeVfsChange();
applier2.afterVfsChange();
myVirtualFilePointerManager.after(deleteEvents);
}
}).assertTiming();
}
public void testCidrCrazyAddCreateRenames() throws IOException {
VirtualFile root = getVirtualFile(createTempDirectory());
VirtualFile dir1 = WriteAction.compute(() -> root.createChildDirectory(this, "dir1"));
@@ -15,7 +15,10 @@ import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.IoTestUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.impl.jar.JarFileSystemImpl;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
@@ -57,14 +60,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
/**
* @author dsl
*/
public class VirtualFilePointerTest extends BareTestFixtureTestCase {
private static final Logger LOG = Logger.getInstance(VirtualFilePointerTest.class);
@Rule public TempDirectory tempDir = new TempDirectory();
@Rule
public TempDirectory tempDir = new TempDirectory();
private final Disposable disposable = Disposer.newDisposable();
private VirtualFilePointerManagerImpl myVirtualFilePointerManager;
private Collection<VirtualFilePointer> pointersBefore;
@@ -95,7 +94,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
}
private VirtualFilePointer createPointerByFile(File file, VirtualFilePointerListener fileListener) {
private @NotNull VirtualFilePointer createPointerByFile(@NotNull File file, VirtualFilePointerListener fileListener) {
String url = VirtualFileManager.constructUrl(LocalFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(file.getPath()));
VirtualFile vFile = getVirtualFile(file);
return vFile == null
@@ -103,7 +102,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
: myVirtualFilePointerManager.create(vFile, disposable, fileListener);
}
private static void verifyPointersInCorrectState(VirtualFilePointer[] pointers) {
private static void verifyPointersInCorrectState(VirtualFilePointer @NotNull [] pointers) {
for (VirtualFilePointer pointer : pointers) {
VirtualFile file = pointer.getFile();
assertTrue(file == null || file.isValid());
@@ -160,6 +159,43 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
assertEquals("[before:true, after:false]", fileToDeleteListener.log.toString());
}
@Test
public void testPointerAfterFileDeleteAndRecreateMustBeValid() {
File fileToDelete = tempDir.newFile("toDelete.txt");
LoggingListener fileToDeleteListener = new LoggingListener();
VirtualFilePointer fileToDeletePointer = createPointerByFile(fileToDelete, fileToDeleteListener);
assertTrue(fileToDeletePointer.isValid());
VirtualFile file = fileToDeletePointer.getFile();
assertNotNull(file);
VirtualFile parent = file.getParent();
VfsTestUtil.deleteFile(getVirtualFile(fileToDelete));
assertFalse(fileToDeletePointer.isValid());
assertEquals("[before:true, after:false]", fileToDeleteListener.log.toString());
fileToDeleteListener.log.clear();
VfsTestUtil.createFile(parent, fileToDelete.getName());
assertTrue(fileToDeletePointer.isValid());
assertEquals("[before:false, after:true]", fileToDeleteListener.log.toString());
}
@Test
public void testPointerMustBeAlreadyUpToDateInAfterListener() {
File fileToDelete = tempDir.newFile("file.txt");
VirtualFilePointer pointer = createPointerByFile(fileToDelete, null);
assertTrue(pointer.isValid());
VirtualFile file = pointer.getFile();
assertNotNull(file);
MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(disposable);
connection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
Object url = ((VirtualFilePointerImpl)pointer).myNode.myFileOrUrl;
assertTrue(url.toString(), url instanceof String);
assertFalse(pointer.isValid());
}
});
VfsTestUtil.deleteFile(getVirtualFile(fileToDelete));
}
@Test
public void testCreate() throws IOException {
File fileToCreate = new File(tempDir.getRoot(), "toCreate.txt");
@@ -209,7 +245,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
checkFileName("/./", "/./");
}
private void checkFileName(String prefix, String suffix) throws IOException {
private void checkFileName(@NotNull String prefix, @NotNull String suffix) throws IOException {
VirtualFile temp = getVirtualTempRoot();
String name = "toCreate.txt";
VirtualFilePointer fileToCreatePointer = createPointerByFile(new File(tempDir.getRoot(), prefix + name + suffix), null);
@@ -239,7 +275,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
}
@Test
public void testMoveFileUnderExistingPointer() throws IOException {
public void testMoveFileToExistingPointerMustValidatePointer() throws IOException {
File moveTarget = tempDir.newDirectory("moveTarget");
File fileToMove = tempDir.newFile("toMove.txt");
@@ -290,6 +326,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
assertTrue(virtualFile.isValid());
VirtualFile target = getVirtualFile(moveTarget);
assertTrue(target.isValid());
assertTrue(target.isDirectory());
WriteAction.runAndWait(() -> virtualFile.move(this, target));
}
@@ -305,7 +342,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
}
@Test
public void testRenamingFileUnderTheExistingPointer() throws IOException {
public void testRenameFileToExistingPointerMustValidatePointer() throws IOException {
File file = tempDir.newFile("f1");
LoggingListener listener = new LoggingListener();
VirtualFilePointer pointer = createPointerByFile(new File(file.getParent(), "f2"), listener);
@@ -338,7 +375,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
}
@Test
public void testCreate1() throws IOException {
public void testCreateFileToExistingPointerUrlMustValidatePointer() throws IOException {
File fileToCreate = new File(tempDir.getRoot(), "toCreate1.txt");
LoggingListener fileToCreateListener = new LoggingListener();
VirtualFilePointer fileToCreatePointer = createPointerByFile(fileToCreate, fileToCreateListener);
@@ -348,7 +385,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
assertTrue(fileToCreatePointer.isValid());
assertEquals("[before:false, after:true]", fileToCreateListener.log.toString());
String expectedUrl = VirtualFileManager.constructUrl(LocalFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(fileToCreate.getPath()));
assertThat(fileToCreatePointer.getUrl()).isEqualToIgnoringCase(expectedUrl);
assertEquals(0, FileUtil.comparePaths(fileToCreatePointer.getUrl(), expectedUrl));
}
@Test
@@ -373,7 +410,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
File jar = new File(jarParent, "x.jar");
File originalJar = new File(PathManagerEx.getTestDataPath() + "/psi/generics22/collect-2.2.jar");
FileUtil.copy(originalJar, jar);
getVirtualFile(jar); // make sure we receive events when .jar changes
assertNotNull(getVirtualFile(jar)); // make sure we receive events when .jar changes
VirtualFilePointerListener listener = new LoggingListener();
VirtualFilePointer jarParentPointer = createPointerByFile(jarParent, listener);
@@ -399,12 +436,19 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
assertTrue(jarParentPointer.isValid());
assertTrue(jarPointer.isValid());
String insideJar = jarUrl+"/META-INF/MANIFEST.MF";
VirtualFilePointer insidePointer = myVirtualFilePointerManager.create(insideJar, disposable, null);
assertTrue(insidePointer.isValid());
JarFileSystemImpl.cleanupForNextTest(); // WTF, won't let delete jar otherwise
assertTrue(jar.delete());
assertTrue(jarParent.delete());
vTemp.refresh(false, true);
verifyPointersInCorrectState(pointersToWatch);
assertFalse(jarParentPointer.isValid());
assertFalse(jarPointer.isValid());
assertFalse(insidePointer.isValid());
}
@Test
@@ -574,7 +618,8 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
WriteAction.runAndWait(() -> dir1.delete(this));
assertNull(p1.getFile());
assertEquals(dir2, p2.getFile());
myVirtualFilePointerManager.assertConsistency();
WriteAction.runAndWait(() -> dir2.rename(this, "dir1"));
assertEquals(dir2, p1.getFile());
assertEquals(dir2, p2.getFile());
@@ -586,6 +631,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
WriteAction.runAndWait(() -> root.createChildDirectory(this, "dir1"));
assertEquals(dir2, p1.getFile());
assertEquals(dir2, p2.getFile());
myVirtualFilePointerManager.assertConsistency();
}
@Test
@@ -651,6 +697,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
AtomicReference<Throwable> exception = new AtomicReference<>(null);
int i;
int nThreads = Runtime.getRuntime().availableProcessors();
FilePartNodeRoot fakeRoot = FilePartNodeRoot.createFakeRoot(LocalFileSystem.getInstance());
for (i = 0; !t.timedOut(i) && i<50_000; i++) {
Disposable disposable = Disposer.newDisposable();
// supply listener to separate pointers under one root so that it will be removed on dispose
@@ -664,8 +711,7 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
try {
ready.countDown();
while (run.get()) {
bb.myNode.myLastUpdated = -15;
bb.getUrl();
bb.myNode.update(((VirtualFilePointerImpl)fileToCreatePointer).myNode, fakeRoot);
}
}
catch (Throwable e) {
@@ -982,8 +1028,8 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
@Test
public void testCreateActualFileEventMustChangePointersCreatedEarlierWithWrongCase_InChildrenLoadedDirectory() throws IOException {
IoTestUtil.assumeWindows();
VirtualFile vroot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tempDir.getRoot());
UsefulTestCase.assertEmpty(vroot.getChildren());
VirtualFile vRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tempDir.getRoot());
UsefulTestCase.assertEmpty(vRoot.getChildren());
LoggingListener fileToCreateListener = new LoggingListener(true);
String name = "toCreate.txt";
File fileToCreate = new File(tempDir.getRoot(), name);
@@ -999,4 +1045,22 @@ public class VirtualFilePointerTest extends BareTestFixtureTestCase {
VirtualFilePointer p3 = createPointerByFile(new File(fileToCreate.getParentFile(), name.toUpperCase(Locale.US)), fileToCreateListener);
assertTrue(p3.isValid());
}
@Test
public void testRawGetPerformance() {
VirtualFile vTemp = getVirtualTempRoot();
VirtualFile file = VfsTestUtil.createFile(vTemp, "f.txt");
vTemp.refresh(false, true);
VirtualFilePointer pointer = createPointerByFile(new File(file.getPath()), null);
assertTrue(pointer.isValid());
assertNotNull(pointer.getFile());
assertTrue(pointer.getFile().isValid());
PlatformTestUtil.startPerformanceTest("get()", 500, () -> {
for (int i=0; i<200_000_000; i++) {
pointer.getFile();
}
}).assertTiming();
}
}
@@ -66,13 +66,14 @@ public final class PathUtilRt {
return path.substring(0, end);
}
private static int getLastIndexOfPathSeparator(@NotNull String path, int end) {
return Math.max(path.lastIndexOf('/', end - 1), path.lastIndexOf('\\', end - 1));
private static int getLastIndexOfPathSeparator(@NotNull CharSequence path, int end) {
return Math.max(StringUtilRt.lastIndexOf(path,'/', 0,end - 1), StringUtilRt.lastIndexOf(path, '\\', 0,end - 1));
}
private static boolean isWindowsUNCRoot(@NotNull String path, int lastPathSeparatorPosition) {
public static boolean isWindowsUNCRoot(@NotNull CharSequence path, int lastPathSeparatorPosition) {
return Platform.CURRENT == Platform.WINDOWS &&
(path.startsWith("//") || path.startsWith("\\\\")) && getLastIndexOfPathSeparator(path, lastPathSeparatorPosition) == 1;
(StringUtilRt.startsWith(path, "//") || StringUtilRt.startsWith(path, "\\\\"))
&& getLastIndexOfPathSeparator(path, lastPathSeparatorPosition) == 1;
}
@NotNull