[platform] avoiding EDT when saving schemes (IJPL-149701)

The component interested in scheme VFS events is `SchemeFileTracker`, but it only processes external events - so the scheme manager just has to keep VFS in sync for these events to happen.

GitOrigin-RevId: 24e0078ae2ff1e295b550d450fcdf78df5945f08
This commit is contained in:
Roman Shevchenko
2024-07-30 18:22:46 +00:00
committed by intellij-monorepo-bot
parent dac8047d97
commit 3160e0cf4e
3 changed files with 81 additions and 103 deletions
@@ -2,6 +2,7 @@
package com.intellij.configurationStore.schemeManager
import com.intellij.configurationStore.LOG
import com.intellij.configurationStore.RELOADING_STORAGE_WRITE_REQUESTOR
import com.intellij.configurationStore.StoreReloadManager
import com.intellij.configurationStore.StoreReloadManagerImpl
import com.intellij.openapi.diagnostic.debug
@@ -24,8 +25,9 @@ internal class SchemeFileTracker<T : Scheme, M : T>(
override fun after(events: List<VFileEvent>) {
val list = ArrayList<SchemeChangeEvent<T, M>>()
for (event in events) {
if (event.requestor is SchemeManagerImpl<*, *>) {
if (event.requestor is SchemeManagerImpl<*, *> || event.requestor == RELOADING_STORAGE_WRITE_REQUESTOR) {
continue
}
@@ -3,7 +3,6 @@ package com.intellij.configurationStore.schemeManager
import com.intellij.configurationStore.*
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.SettingsCategory
@@ -14,12 +13,14 @@ import com.intellij.openapi.options.SchemeManager
import com.intellij.openapi.options.SchemeManagerFactory
import com.intellij.openapi.options.SchemeProcessor
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.addSuppressed
import com.intellij.util.containers.ContainerUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.TestOnly
@@ -104,16 +105,11 @@ sealed class SchemeManagerFactoryBase : SchemeManagerFactory(), SettingsSavingCo
final override suspend fun save() {
var error: Throwable? = null
val events = mutableListOf<VFileEvent>()
for (registeredManager in managers) {
try {
if (registeredManager.isUseVfs) {
withContext(Dispatchers.EDT) {
registeredManager.save()
}
}
else {
registeredManager.save()
}
registeredManager.saveImpl(events)
}
catch (e: CancellationException) {
throw e
@@ -122,14 +118,16 @@ sealed class SchemeManagerFactoryBase : SchemeManagerFactory(), SettingsSavingCo
throw e
}
catch (e: Throwable) {
if (error == null) {
error = e
}
else {
error.addSuppressed(e)
}
error = addSuppressed(error, e)
}
}
if (events.isNotEmpty()) {
blockingContext {
RefreshQueue.getInstance().processEvents(false, events)
}
}
error?.let {
throw it
}
@@ -6,7 +6,6 @@ import com.intellij.configurationStore.*
import com.intellij.diagnostic.PluginException
import com.intellij.ide.ui.UITheme
import com.intellij.ide.ui.laf.TempUIThemeLookAndFeelInfo
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.impl.stores.ComponentStorageUtil
@@ -22,6 +21,9 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.SafeWriteRequestor
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.NewVirtualFile
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.*
import com.intellij.util.io.directoryStreamIfExists
import com.intellij.util.io.write
@@ -38,7 +40,6 @@ import java.util.*
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.Predicate
import kotlin.io.path.invariantSeparatorsPathString
import kotlin.io.path.isDirectory
import kotlin.io.path.isHidden
import kotlin.io.path.readBytes
@@ -55,8 +56,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
private val fileChangeSubscriber: FileChangeSubscriber? = null,
private val settingsCategory: SettingsCategory = SettingsCategory.OTHER,
) : SchemeManagerBase<T, MUTABLE_SCHEME>(processor), SafeWriteRequestor, StorageManagerFileWriteRequestor {
internal val isUseVfs: Boolean
get() = fileChangeSubscriber != null
private val isUpdateVfs: Boolean = fileChangeSubscriber != null
internal val isOldSchemeNaming: Boolean = schemeNameToFileName == OLD_NAME_CONVERTER
@@ -67,6 +67,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
internal val schemes: MutableList<T>
get() = schemeListManager.schemes
@Volatile
internal var cachedVirtualDirectory: VirtualFile? = null
internal val schemeExtension: String
@@ -84,7 +85,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
updateExtension = false
}
if (isUseVfs) {
if (isUpdateVfs) {
runCatching { refreshVirtualDirectory() }.getOrLogException(LOG)
}
}
@@ -335,6 +336,14 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
(processor !is LazySchemeProcessor || processor.isSchemeFile(name))
override fun save() {
val events = if (isUpdateVfs) mutableListOf<VFileEvent>() else Collections.emptyList()
saveImpl(events)
if (events.isNotEmpty()) {
RefreshQueue.getInstance().processEvents(false, events)
}
}
internal fun saveImpl(events: MutableList<VFileEvent>) {
if (isLoadingSchemes.get()) {
LOG.warn("Skip save - schemes are loading")
}
@@ -365,7 +374,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
val filesToDelete = HashSet(filesToDelete)
for (scheme in changedSchemes) {
try {
saveScheme(scheme, nameGenerator, filesToDelete)
saveScheme(scheme, nameGenerator, filesToDelete, events)
}
catch (e: CancellationException) { throw e }
catch (e: ProcessCanceledException) { throw e }
@@ -378,11 +387,11 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
schemeListManager.data.schemeToInfo.values.removeIf { filesToDelete.contains(it.fileName) }
this.filesToDelete.removeAll(filesToDelete)
deleteFiles(errorCollector, filesToDelete)
deleteFiles(errorCollector, filesToDelete, events)
// remove empty directory only if some file was deleted - avoid check on each save
if (!hasSchemes && (provider == null || !provider.isApplicable(fileSpec, roamingType))) {
removeDirectoryIfEmpty(errorCollector)
removeDirectoryIfEmpty(errorCollector, events)
}
}
@@ -393,7 +402,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
override fun getSettingsCategory(): SettingsCategory = settingsCategory
private fun removeDirectoryIfEmpty(errorCollector: ErrorCollector) {
private fun removeDirectoryIfEmpty(errorCollector: ErrorCollector, events: MutableList<VFileEvent>) {
ioDirectory.directoryStreamIfExists {
for (file in it) {
if (!file.isHidden()) {
@@ -405,31 +414,23 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
LOG.info("Remove scheme directory ${ioDirectory.fileName}")
if (isUseVfs) {
try {
NioFiles.deleteRecursively(ioDirectory)
}
catch (e: Throwable) {
errorCollector.addError(e)
}
if (isUpdateVfs) {
val dir = getVirtualDirectory()
cachedVirtualDirectory = null
if (dir != null) {
runWriteAction {
try {
dir.delete(this)
}
catch (e: Throwable) {
errorCollector.addError(e)
}
}
}
}
else {
try {
NioFiles.deleteRecursively(ioDirectory)
}
catch (e: Throwable) {
errorCollector.addError(e)
events += VFileDeleteEvent(/*requestor =*/ this, dir)
}
}
}
private fun saveScheme(scheme: MUTABLE_SCHEME, nameGenerator: UniqueNameGenerator, filesToDelete: MutableSet<String>) {
private fun saveScheme(scheme: MUTABLE_SCHEME, nameGenerator: UniqueNameGenerator, filesToDelete: MutableSet<String>, events: MutableList<VFileEvent>) {
var externalInfo: ExternalInfo? = schemeListManager.getExternalInfo(scheme)
val currentFileNameWithoutExtension = externalInfo?.fileNameWithoutExtension
val element = processor.writeScheme(scheme)?.let { it as? Element ?: (it as Document).detachRootElement() }
@@ -484,45 +485,30 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
currentFileNameWithoutExtension != null &&
nameGenerator.isUnique(currentFileNameWithoutExtension)
if (providerPath == null) {
if (isUseVfs) {
var file: VirtualFile? = null
var dir = getVirtualDirectory()
if (dir == null || !dir.isValid) {
dir = createDir(ioDirectory, this)
if (renamed) {
externalInfo!!.scheduleDelete(filesToDelete, "renamed")
}
var dir = if (isUpdateVfs) getVirtualDirectory() else null
val ioFile = ioDirectory.resolve(fileName)
ioFile.write(byteOut.internalBuffer, 0, byteOut.size())
if (isUpdateVfs) {
if (dir == null) {
dir = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(ioDirectory)
cachedVirtualDirectory = dir
}
if (renamed) {
val oldFile = dir.findChild(externalInfo!!.fileName)
if (oldFile != null) {
// VFS doesn't allow renaming to an existing file, so, check it
if (dir.findChild(fileName) == null) {
runWriteAction {
oldFile.rename(this, fileName)
}
file = oldFile
}
else {
externalInfo.scheduleDelete(filesToDelete, "renamed")
}
if (dir != null) {
val file = dir.findChild(fileName)
if (file != null) {
events += updatingEvent(ioFile, file)
}
else {
// an old file deletion event is generated by `deleteFiles`
events += creationEvent(ioFile, dir)
}
}
if (file == null) {
file = SlowOperations.knownIssue("IDEA-338219, EA-867032").use {
dir.getOrCreateChild(requestor = this, fileName, directory = false)
}
}
runWriteAction {
file.getOutputStream(this).use { byteOut.writeTo(it) }
}
}
else {
if (renamed) {
externalInfo!!.scheduleDelete(filesToDelete, "renamed")
}
ioDirectory.resolve(fileName).write(byteOut.internalBuffer, 0, byteOut.size())
}
}
else {
@@ -582,7 +568,7 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
return info != null && processor.getSchemeKey(scheme) != info.schemeKey
}
private fun deleteFiles(errorCollector: ErrorCollector, filesToDelete: MutableSet<String>) {
private fun deleteFiles(errorCollector: ErrorCollector, filesToDelete: MutableSet<String>, events: MutableList<VFileEvent>) {
if (provider != null) {
val iterator = filesToDelete.iterator()
for (name in iterator) {
@@ -605,30 +591,22 @@ class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(
LOG.debug { "Delete scheme files: ${filesToDelete.joinToString()}" }
if (isUseVfs) {
getVirtualDirectory()?.let { virtualDir ->
val childrenToDelete = virtualDir.children.filter { filesToDelete.contains(it.name) }
if (childrenToDelete.isNotEmpty()) {
runWriteAction {
for (file in childrenToDelete) {
try {
file.delete(this)
}
catch (e: Throwable) {
errorCollector.addError(e)
}
}
}
}
for (name in filesToDelete) {
try {
NioFiles.deleteRecursively(ioDirectory.resolve(name))
}
catch (e: Throwable) {
errorCollector.addError(e)
}
}
else {
for (name in filesToDelete) {
try {
NioFiles.deleteRecursively(ioDirectory.resolve(name))
}
catch (e: Throwable) {
errorCollector.addError(e)
if (isUpdateVfs) {
val dir = getVirtualDirectory()
if (dir != null) {
for (file in dir.children) {
if (file.isValid && file.name in filesToDelete) {
events += VFileDeleteEvent(/*requestor =*/ this, file)
}
}
}
}