From 1bf8a93f55850ab73149b8e7ec8c1ea7ed966800 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 22 Oct 2023 09:44:19 +0200 Subject: [PATCH] [platform] an ability to perform periodic VFS refresh when the IDE is inactive (IJPL-181) GitOrigin-RevId: 039f752cf36f0fff0c6f6a2645976b9e8b8f3f1e --- .../src/SaveAndSyncHandlerImpl.kt | 46 +++++++++++++++---- .../resources/messages/IdeBundle.properties | 3 +- .../openapi/vfs/newvfs/RefreshQueueImpl.java | 8 +++- .../vfs/newvfs/RefreshSessionImpl.java | 13 ++++-- .../util/resources/misc/registry.properties | 4 ++ 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/platform/configuration-store-impl/src/SaveAndSyncHandlerImpl.kt b/platform/configuration-store-impl/src/SaveAndSyncHandlerImpl.kt index 9bc3372ad925..4be1fb5c37c0 100644 --- a/platform/configuration-store-impl/src/SaveAndSyncHandlerImpl.kt +++ b/platform/configuration-store-impl/src/SaveAndSyncHandlerImpl.kt @@ -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() + private val refreshSession = AtomicReference() + private var bgRefreshJob: Job? = null private val saveAppAndProjectsSettingsTask = SaveTask() private val saveQueue = ArrayDeque() @@ -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() + 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)) diff --git a/platform/platform-api/resources/messages/IdeBundle.properties b/platform/platform-api/resources/messages/IdeBundle.properties index d789ef07379b..02e2174f9cff 100644 --- a/platform/platform-api/resources/messages/IdeBundle.properties +++ b/platform/platform-api/resources/messages/IdeBundle.properties @@ -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. How it works 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... \ No newline at end of file +inline.completion.tooltip.shortcuts.accept.select.custom=Custom... diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshQueueImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshQueueImpl.java index f593d8d84107..0412364b2359 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshQueueImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshQueueImpl.java @@ -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) { diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java index e2d12c925c91..f61f96d729b2 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java @@ -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 files) { + this(false, true, true, null, getSaneModalityState(ModalityState.defaultModalityState())); + addAllFiles(files); + } + RefreshSessionImpl(boolean async, List 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; diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index fc31e648c45e..ea5969d6ee8f 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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.