nio based refresh worker (disabled by default)

This commit is contained in:
Maxim.Mossienko
2017-04-18 20:29:47 +02:00
parent 69e89c9d6a
commit b58c8e21f4
3 changed files with 452 additions and 1 deletions
@@ -0,0 +1,437 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vfs.newvfs.persistent;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VFileProperty;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
import com.intellij.util.Function;
import com.intellij.util.containers.Queue;
import com.intellij.util.text.FilePathHashingStrategy;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributes;
import java.nio.file.attribute.PosixFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class LocalFileSystemRefreshWorker {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.RefreshWorker");
private static final Logger LOG_ATTRIBUTES = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.RefreshWorker_Attributes");
private final boolean myIsRecursive;
private final Queue<NewVirtualFile> myRefreshQueue = new Queue<NewVirtualFile>(100);
private final List<VFileEvent> myFileEventSet = new ArrayList<>();
private volatile boolean myCancelled;
public LocalFileSystemRefreshWorker(@NotNull NewVirtualFile refreshRoot, boolean isRecursive) {
myIsRecursive = isRecursive;
myRefreshQueue.addLast(refreshRoot);
}
@NotNull
public List<VFileEvent> getEvents() {
return myFileEventSet;
}
public void cancel() {
myCancelled = true;
}
public void scan() {
NewVirtualFile root = myRefreshQueue.pullFirst();
boolean rootDirty = root.isDirty();
if (LOG.isDebugEnabled()) LOG.debug("root=" + root + " dirty=" + rootDirty);
if (!rootDirty) return;
NewVirtualFileSystem fs = root.getFileSystem();
FileAttributes rootAttributes = fs.getAttributes(root);
if (rootAttributes == null) {
myFileEventSet.add(createDeletionEvent(root));
root.markClean();
return;
}
else if (rootAttributes.isDirectory()) {
fs = PersistentFS.replaceWithNativeFS(fs);
}
myRefreshQueue.addLast(root);
try {
processQueue(fs, PersistentFS.getInstance());
}
catch (RefreshCancelledException e) {
LOG.debug("refresh cancelled");
}
}
private void processQueue(NewVirtualFileSystem fs, PersistentFS persistence) throws RefreshCancelledException {
TObjectHashingStrategy<String> strategy = FilePathHashingStrategy.create(fs.isCaseSensitive());
while (!myRefreshQueue.isEmpty()) {
NewVirtualFile file = myRefreshQueue.pullFirst();
boolean fileDirty = file.isDirty();
if (LOG.isTraceEnabled()) LOG.trace("file=" + file + " dirty=" + fileDirty);
if (!fileDirty) continue;
checkCancelled(file);
if (file.isDirectory()) {
boolean fullSync = ((VirtualDirectoryImpl)file).allChildrenLoaded();
if (fullSync) {
fullDirRefresh(fs, persistence, strategy, (VirtualDirectoryImpl)file);
}
else {
partialDirRefresh(fs, persistence, strategy, (VirtualDirectoryImpl)file);
}
}
else {
refreshFile(fs, persistence, strategy, file);
}
if (myIsRecursive || !file.isDirectory()) {
file.markClean();
}
}
}
private void refreshFile(NewVirtualFileSystem fs,
PersistentFS persistence,
TObjectHashingStrategy<String> strategy,
NewVirtualFile file) {
RefreshingFileVisitor refreshingFileVisitor = new RefreshingFileVisitor(file, persistence, fs,
null,
Collections.singletonList(file), strategy);
refreshingFileVisitor.visit(file);
myFileEventSet.addAll(refreshingFileVisitor.getEventSet());
}
private static final AtomicInteger myRequests = new AtomicInteger();
private static final AtomicLong myTime = new AtomicLong();
private void fullDirRefresh(NewVirtualFileSystem fs, PersistentFS persistence, TObjectHashingStrategy<String> strategy, VirtualDirectoryImpl dir) {
while (true) {
// obtaining directory snapshot
String[] currentNames;
VirtualFile[] children;
Application application = ApplicationManager.getApplication();
AccessToken token = application.acquireReadActionLock();
try {
if (application.isDisposed()) {
return;
}
currentNames = persistence.list(dir);
children = dir.getChildren();
}
finally {
token.finish();
}
RefreshingFileVisitor refreshingFileVisitor = new RefreshingFileVisitor(dir, persistence, fs, null, Arrays.asList(children), strategy);
refreshingFileVisitor.visit(dir);
// generating events unless a directory was changed in between
token = application.acquireReadActionLock();
try {
if (application.isDisposed()) {
return;
}
if (!Arrays.equals(currentNames, persistence.list(dir)) || !Arrays.equals(children, dir.getChildren())) {
if (LOG.isDebugEnabled()) LOG.debug("retry: " + dir);
continue;
}
myFileEventSet.addAll(refreshingFileVisitor.getEventSet());
break;
}
finally {
token.finish();
}
}
}
private void partialDirRefresh(NewVirtualFileSystem fs,
PersistentFS persistence,
TObjectHashingStrategy<String> strategy,
VirtualDirectoryImpl dir) {
while (true) {
// obtaining directory snapshot
List<VirtualFile> cached;
List<String> wanted;
AccessToken token = ApplicationManager.getApplication().acquireReadActionLock();
try {
cached = dir.getCachedChildren();
wanted = dir.getSuspiciousNames();
}
finally {
token.finish();
}
if (cached.size() == 0 && wanted.size() == 0) return;
RefreshingFileVisitor refreshingFileVisitor = new RefreshingFileVisitor(dir, persistence, fs, wanted, cached, strategy);
refreshingFileVisitor.visit(dir);
// generating events unless a directory was changed in between
token = ApplicationManager.getApplication().acquireReadActionLock();
try {
if (!cached.equals(dir.getCachedChildren()) || !wanted.equals(dir.getSuspiciousNames())) {
if (LOG.isDebugEnabled()) LOG.debug("retry: " + dir);
continue;
}
myFileEventSet.addAll(refreshingFileVisitor.getEventSet());
break;
}
finally {
token.finish();
}
}
}
private static class RefreshCancelledException extends RuntimeException { }
private void checkCancelled(@NotNull NewVirtualFile stopAt) {
if (myCancelled || ourCancellingCondition != null && ourCancellingCondition.fun(stopAt)) {
forceMarkDirty(stopAt);
while (!myRefreshQueue.isEmpty()) {
NewVirtualFile next = myRefreshQueue.pullFirst();
forceMarkDirty(next);
}
throw new RefreshCancelledException();
}
}
private static void forceMarkDirty(NewVirtualFile file) {
file.markClean(); // otherwise consequent markDirty() won't have any effect
file.markDirty();
}
private static Function<VirtualFile, Boolean> ourCancellingCondition;
@TestOnly
public static void setCancellingCondition(@Nullable Function<VirtualFile, Boolean> condition) {
assert ApplicationManager.getApplication().isUnitTestMode();
ourCancellingCondition = condition;
}
static VFileEvent createAttributeChangeEvent(@NotNull VirtualFile file, @NotNull String property, Object current, Object upToDate) {
if (LOG.isTraceEnabled()) LOG.trace("update '" + property + "' file=" + file);
return new VFilePropertyChangeEvent(null, file, property, current, upToDate, true);
}
static VFileEvent createUpdateContentEvent(@NotNull VirtualFile file) {
if (LOG.isTraceEnabled()) LOG.trace("update file=" + file);
return new VFileContentChangeEvent(null, file, file.getModificationStamp(), -1, true);
}
static VFileEvent createCreationEvent(@NotNull VirtualFile parent, @NotNull String childName, boolean isDirectory, boolean isReCreation) {
if (LOG.isTraceEnabled()) LOG.trace("create parent=" + parent + " name=" + childName + " dir=" + isDirectory);
return new VFileCreateEvent(null, parent, childName, isDirectory, true, isReCreation);
}
static VFileEvent createDeletionEvent(@Nullable VirtualFile file) {
if (file != null) {
if (LOG.isTraceEnabled()) LOG.trace("delete file=" + file);
return new VFileDeleteEvent(null, file, true);
}
return null;
}
private class RefreshingFileVisitor extends SimpleFileVisitor<Path> {
private final List<VFileEvent> myFileEvents = new ArrayList<>();
private final Map<String, VirtualFile> myPersistentChildren;
private final Set<String> myChildrenWeAreInterested; // null - no limit
private final VirtualFile myFileOrDir;
private final PersistentFS myPersistence;
private final NewVirtualFileSystem myFs;
public RefreshingFileVisitor(VirtualFile fileOrDir,
PersistentFS persistence,
NewVirtualFileSystem fs,
Collection<String> persistentChildrenToRefresh,
Collection<VirtualFile> existingPersistentChildren,
TObjectHashingStrategy<String> strategy) {
myFileOrDir = fileOrDir;
myPersistence = persistence;
myFs = fs;
myPersistentChildren = new THashMap<>(existingPersistentChildren.size(), strategy);
myChildrenWeAreInterested = persistentChildrenToRefresh != null ? new THashSet<String>(persistentChildrenToRefresh, strategy) : null;
for(VirtualFile child:existingPersistentChildren) {
String name = child.getName();
myPersistentChildren.put(name, child);
if (myChildrenWeAreInterested != null) myChildrenWeAreInterested.add(name);
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String name = file.getName(file.getNameCount() - 1).toString();
if (acceptsFileName(name)) {
NewVirtualFile child = (NewVirtualFile)myPersistentChildren.remove(name);
if (child == null) { // new file is created
VFileEvent event =
createCreationEvent(myFileOrDir.isDirectory() ? myFileOrDir : myFileOrDir.getParent(), name, attrs.isDirectory(), false);
myFileEvents.add(event);
return FileVisitResult.CONTINUE;
}
checkCancelled(child);
boolean currentIsDirectory = child.isDirectory();
boolean currentIsSymlink = child.is(VFileProperty.SYMLINK);
boolean currentIsSpecial = child.is(VFileProperty.SPECIAL);
if (currentIsDirectory != attrs.isDirectory() ||
currentIsSymlink != attrs.isSymbolicLink() ||
currentIsSpecial != attrs.isOther()) { // symlink or directory or special changed
myFileEvents.add(createDeletionEvent(child));
VFileEvent event = createCreationEvent(myFileOrDir.isDirectory() ? myFileOrDir : myFileOrDir.getParent(), child.getName(), attrs.isDirectory(), true);
myFileEvents.add(event);
// ignore everything else
child.markClean();
return FileVisitResult.CONTINUE;
}
String currentName = child.getName();
if (!currentName.equals(name)) {
myFileEvents.add(createAttributeChangeEvent(child, VirtualFile.PROP_NAME, currentName, name));
}
if (!attrs.isDirectory()) {
if (myPersistence.getTimeStamp(child) != attrs.lastModifiedTime().toMillis() ||
myPersistence.getLastRecordedLength(child) != attrs.size()) {
myFileEvents.add(createUpdateContentEvent(child));
child.markClean();
return FileVisitResult.CONTINUE;
}
} else {
if (myIsRecursive) {
myRefreshQueue.addLast(child);
}
}
boolean currentWritable = myPersistence.isWritable(child);
boolean isWritable;
if (attrs instanceof DosFileAttributes) {
DosFileAttributes dosFileAttributes = (DosFileAttributes)attrs;
isWritable = attrs.isDirectory() || !dosFileAttributes.isReadOnly();
} else if (attrs instanceof PosixFileAttributes) {
isWritable = ((PosixFileAttributes)attrs).permissions().contains(OWNER_WRITE);
} else {
isWritable = file.toFile().canWrite();
}
if (LOG_ATTRIBUTES.isDebugEnabled()) {
LOG_ATTRIBUTES.debug("file=" + file + " writable vfs=" + child.isWritable() + " persistence=" + currentWritable + " real=" + isWritable);
}
if (currentWritable != isWritable) {
myFileEvents.add(createAttributeChangeEvent(child, VirtualFile.PROP_WRITABLE, currentWritable, isWritable));
}
if (attrs instanceof DosFileAttributes) {
boolean currentHidden = child.is(VFileProperty.HIDDEN);
boolean upToDateHidden = ((DosFileAttributes)attrs).isHidden();
if (currentHidden != upToDateHidden) {
myFileEvents.add(createAttributeChangeEvent(child, VirtualFile.PROP_HIDDEN, currentHidden, upToDateHidden));
}
}
if (attrs.isSymbolicLink()) {
String currentTarget = child.getCanonicalPath();
String upToDateTarget = myFs.resolveSymLink(child);
String upToDateVfsTarget = upToDateTarget != null ? FileUtil.toSystemIndependentName(upToDateTarget) : null;
if (!Comparing.equal(currentTarget, upToDateVfsTarget)) {
myFileEvents.add(createAttributeChangeEvent(child, VirtualFile.PROP_SYMLINK_TARGET, currentTarget, upToDateVfsTarget));
}
}
if(!child.isDirectory()) child.markClean();
}
return FileVisitResult.CONTINUE;
}
protected boolean acceptsFileName(String name) {
return !VfsUtil.isBadName(name) && (myChildrenWeAreInterested == null || myChildrenWeAreInterested.contains(name));
}
public void visit(VirtualFile fileOrDir) {
long started = System.nanoTime();
try {
Path path = Paths.get(fileOrDir.getPath());
if (fileOrDir.isDirectory()) {
Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), 1, this);
} else {
visitFile(path, Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS));
}
}
catch (AccessDeniedException ignore) {
int a = 1;
}
catch (IOException ex) {
LOG.error(ex);
}
int requests = myRequests.incrementAndGet();
long l = myTime.addAndGet(System.nanoTime() - started);
if (requests % 1000 == 0) {
System.out.println("refresh:" + myRequests + " for " + (l / 1000000));
}
}
public List<VFileEvent> getEventSet() {
if (myPersistentChildren.size() > 0) {
for(VirtualFile child:myPersistentChildren.values()) {
myFileEvents.add(createDeletionEvent(child));
}
myPersistentChildren.clear();
}
return myFileEvents;
}
}
}
@@ -23,9 +23,11 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.VFileProperty;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.events.*;
@@ -60,22 +62,32 @@ public class RefreshWorker {
private final Queue<Pair<NewVirtualFile, FileAttributes>> myRefreshQueue = new Queue<>(100);
private final List<VFileEvent> myEvents = new ArrayList<>();
private volatile boolean myCancelled;
private final LocalFileSystemRefreshWorker myLocalFileSystemRefreshWorker;
public RefreshWorker(@NotNull NewVirtualFile refreshRoot, boolean isRecursive) {
boolean canUseNioRefresher = refreshRoot.isInLocalFileSystem() && !(refreshRoot.getFileSystem() instanceof TempFileSystem);
myLocalFileSystemRefreshWorker = canUseNioRefresher && Registry.is("vfs.use.nio-based.local.refresh.worker") ?
new LocalFileSystemRefreshWorker(refreshRoot, isRecursive) : null;
myIsRecursive = isRecursive;
myRefreshQueue.addLast(pair(refreshRoot, null));
}
@NotNull
public List<VFileEvent> getEvents() {
if (myLocalFileSystemRefreshWorker != null) return myLocalFileSystemRefreshWorker.getEvents();
return myEvents;
}
public void cancel() {
if (myLocalFileSystemRefreshWorker != null) myLocalFileSystemRefreshWorker.cancel();
myCancelled = true;
}
public void scan() {
if (myLocalFileSystemRefreshWorker != null) {
myLocalFileSystemRefreshWorker.scan();
return;
}
NewVirtualFile root = myRefreshQueue.peekFirst().first;
NewVirtualFileSystem fs = root.getFileSystem();
if (root.isDirectory()) {
@@ -413,6 +425,7 @@ public class RefreshWorker {
@TestOnly
public static void setCancellingCondition(@Nullable Function<VirtualFile, Boolean> condition) {
assert ApplicationManager.getApplication().isUnitTestMode();
LocalFileSystemRefreshWorker.setCancellingCondition(condition);
ourCancellingCondition = condition;
}
}
@@ -1114,4 +1114,5 @@ performance.watcher.sampling.interval.ms.description=If the product is unrespons
JavaScript.Language.Service.truncate.traced.messages=true
JavaScript.Language.Service.truncate.traced.messages.description=Truncate traced JavaScript language Service messages in log
runManager.use.schemeManager=false
runManager.use.schemeManager=false
vfs.use.nio-based.local.refresh.worker=false