[platform] an ability to perform periodic VFS refresh when the IDE is inactive (IJPL-181)

GitOrigin-RevId: 039f752cf36f0fff0c6f6a2645976b9e8b8f3f1e
This commit is contained in:
Roman Shevchenko
2023-10-22 10:06:17 +00:00
committed by intellij-monorepo-bot
parent 6ea7e71a61
commit 1bf8a93f55
5 changed files with 61 additions and 13 deletions
@@ -24,9 +24,11 @@ import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.getOpenedProjects
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.newvfs.ManagingFS
import com.intellij.openapi.vfs.newvfs.NewVirtualFile
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.openapi.vfs.newvfs.RefreshQueueImpl
import com.intellij.openapi.vfs.newvfs.RefreshSession
import com.intellij.openapi.wm.IdeFrame
import com.intellij.platform.ide.progress.ModalTaskOwner
@@ -55,7 +57,8 @@ internal class SaveAndSyncHandlerImpl(private val coroutineScope: CoroutineScope
private val blockSaveOnFrameDeactivationCount = AtomicInteger()
private val blockSyncOnFrameActivationCount = AtomicInteger()
private val refreshSession = AtomicReference<RefreshSession>()
private val refreshSession = AtomicReference<RefreshSession?>()
private var bgRefreshJob: Job? = null
private val saveAppAndProjectsSettingsTask = SaveTask()
private val saveQueue = ArrayDeque<SaveTask>()
@@ -117,6 +120,7 @@ internal class SaveAndSyncHandlerImpl(private val coroutineScope: CoroutineScope
}
coroutineScope.awaitCancellationAndInvoke {
bgRefreshJob?.cancel()
refreshSession.get()?.cancel()
}
}
@@ -179,19 +183,24 @@ internal class SaveAndSyncHandlerImpl(private val coroutineScope: CoroutineScope
.subscribe(ApplicationActivationListener.TOPIC, object : ApplicationActivationListener {
override fun applicationDeactivated(ideFrame: IdeFrame) {
externalChangesModificationTracker.incModificationCount()
if (!settings.isSaveOnFrameDeactivation || !canSyncOrSave()) {
return
if (settings.isSaveOnFrameDeactivation && canSyncOrSave()) {
// for many tasks (compilation, web development, etc.), it is important to save documents on frame deactivation ASAP
(FileDocumentManager.getInstance() as FileDocumentManagerImpl).saveAllDocuments(false)
if (addToSaveQueue(saveAppAndProjectsSettingsTask)) {
requestSave()
}
}
// for web development, it is crucially important to save documents on frame deactivation as early as possible
(FileDocumentManager.getInstance() as FileDocumentManagerImpl).saveAllDocuments(false)
if (addToSaveQueue(saveAppAndProjectsSettingsTask)) {
requestSave()
if (settings.isBackgroundSync) {
bgRefreshJob = startBackgroundSync()
}
}
override fun applicationActivated(ideFrame: IdeFrame) {
bgRefreshJob?.cancel()
bgRefreshJob = null
if (settings.isSyncOnFrameActivation) {
scheduleRefresh()
}
@@ -303,6 +312,27 @@ internal class SaveAndSyncHandlerImpl(private val coroutineScope: CoroutineScope
private fun canSyncOrSave(): Boolean =
!LaterInvocator.isInModalContext() && !ProgressManager.getInstance().hasModalProgressIndicator()
private fun startBackgroundSync(): Job {
LOG.debug("starting background VFS sync")
val bgRefreshSession = AtomicReference<RefreshSession?>()
val job = coroutineScope.launch(CoroutineName("background sync")) {
val roots = listOf(*ManagingFS.getInstance().localRoots)
val queue = RefreshQueue.getInstance() as RefreshQueueImpl
val interval = Registry.intValue("vfs.background.refresh.interval", 15).coerceIn(0, Int.MAX_VALUE).seconds
while (true) {
if (roots.any { it is NewVirtualFile && it.isDirty }) {
val session = queue.createBackgroundRefreshSession(roots)
bgRefreshSession.set(session)
session.launch()
bgRefreshSession.set(null)
}
delay(interval)
}
}
job.invokeOnCompletion { if (it is CancellationException) bgRefreshSession.getAndSet(null)?.cancel() }
return job
}
override fun scheduleRefresh() {
externalChangesModificationTracker.incModificationCount()
check(refreshRequests.tryEmit(Unit))
@@ -83,6 +83,7 @@ checkbox.save.files.automatically=Save files if the IDE is idle for
checkbox.save.files.on.frame.deactivation=Save files when switching to a different application or a built-in terminal
checkbox.safe.write=Back up files before saving
checkbox.synchronize.files.on.frame.activation=Synchronize external changes when switching to the IDE window or opening an editor tab
checkbox.synchronize.files.in.background=Synchronize external changes periodically when the IDE is inactive (experimental)
label.autosave.comment=Autosave cannot be disabled completely. <a>How it works</a>
title.accessibility=Accessibility
checkbox.support.screen.readers=Support screen readers
@@ -3023,4 +3024,4 @@ plugin.advertiser.upgrade.to=Upgrade to
plugin.advertiser.free.trial.action=free 1 month trial
inline.completion.tooltip.shortcuts.accept.description=to accept
inline.completion.tooltip.shortcuts.accept.select.custom=Custom...
inline.completion.tooltip.shortcuts.accept.select.custom=Custom...
@@ -23,6 +23,7 @@ import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.storage.HeavyProcessLatch;
import com.intellij.util.ui.EDT;
import kotlinx.coroutines.CoroutineScope;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
@@ -178,7 +179,7 @@ public final class RefreshQueueImpl extends RefreshQueue implements Disposable {
@Override
public @NotNull RefreshSession createSession(boolean async, boolean recursively, @Nullable Runnable finishRunnable, @NotNull ModalityState state) {
return new RefreshSessionImpl(async, recursively, finishRunnable, state);
return new RefreshSessionImpl(async, recursively, false, finishRunnable, state);
}
@Override
@@ -186,6 +187,11 @@ public final class RefreshQueueImpl extends RefreshQueue implements Disposable {
new RefreshSessionImpl(async, events).launch();
}
@ApiStatus.Internal
public @NotNull RefreshSession createBackgroundRefreshSession(@NotNull List<@NotNull VirtualFile> files) {
return new RefreshSessionImpl(files);
}
@Override
public void dispose() {
synchronized (mySessions) {
@@ -40,6 +40,7 @@ final class RefreshSessionImpl extends RefreshSession {
private final boolean myIsAsync;
private final boolean myIsRecursive;
private final boolean myIsBackground;
private final Runnable myFinishRunnable;
private final ModalityState myModality;
private final @Nullable Throwable myStartTrace;
@@ -51,9 +52,10 @@ final class RefreshSessionImpl extends RefreshSession {
private volatile boolean myCancelled;
private volatile boolean myLaunched;
RefreshSessionImpl(boolean async, boolean recursive, @Nullable Runnable finishRunnable, @NotNull ModalityState modality) {
RefreshSessionImpl(boolean async, boolean recursive, boolean background, @Nullable Runnable finishRunnable, @NotNull ModalityState modality) {
myIsAsync = async;
myIsRecursive = recursive;
myIsBackground = background;
myFinishRunnable = finishRunnable;
myModality = getSaneModalityState(modality);
TransactionGuard.getInstance().assertWriteSafeContext(myModality);
@@ -61,8 +63,13 @@ final class RefreshSessionImpl extends RefreshSession {
myStartTrace = app.isUnitTestMode() && (async || !app.isDispatchThread()) ? new Throwable() : null;
}
RefreshSessionImpl(List<VirtualFile> files) {
this(false, true, true, null, getSaneModalityState(ModalityState.defaultModalityState()));
addAllFiles(files);
}
RefreshSessionImpl(boolean async, List<? extends VFileEvent> events) {
this(async, false, null, getSaneModalityState(ModalityState.defaultModalityState()));
this(async, false, false, null, getSaneModalityState(ModalityState.defaultModalityState()));
var filtered = events.stream().filter(Objects::nonNull).toList();
if (filtered.size() < events.size()) LOG.error("The list of events must not contain null elements");
myEvents.addAll(filtered);
@@ -168,7 +175,7 @@ final class RefreshSessionImpl extends RefreshSession {
count++;
if (LOG.isTraceEnabled()) LOG.trace("events=" + events.size());
}
while (myIsRecursive && count < RETRY_LIMIT && ContainerUtil.exists(workQueue, f -> ((NewVirtualFile)f).isDirty()));
while (myIsRecursive && !myIsBackground && count < RETRY_LIMIT && ContainerUtil.exists(workQueue, f -> ((NewVirtualFile)f).isDirty()));
t = NANOSECONDS.toMillis(System.nanoTime() - t);
int localRoots = 0, archiveRoots = 0, otherRoots = 0;
@@ -1752,6 +1752,10 @@ non.edt.swing.report.categories=unknown
non.edt.swing.report.categories.description=Log non-EDT swing access. Requires `idea.check.swing.threading=true` system property to work \
Possible values (might use several separated by comma): none,all,preloader,component,configurable,unknown
vfs.background.refresh.interval=15
vfs.background.refresh.interval.description=A delay between subsequent filesystem scans for \
"Settings | Appearance & Behavior | System Settings | Synchronize ... when the IDE is inactive" option, in seconds
vfs.refresh.worker.parallelism=6
vfs.refresh.worker.parallelism.description=How many threads will be used to access the file system for detecting changes. \
Modern SSD and NVMe drives allow running many operations in parallel.