Do not start multiple background threads loading dependencies for different scripts

^KT-27743 Fixed
This commit is contained in:
Natalia Selezneva
2018-11-15 12:44:31 +03:00
parent 8ce63f6567
commit b8086d9093
6 changed files with 120 additions and 167 deletions
@@ -77,7 +77,7 @@ class ScriptDependenciesManager internal constructor(
@TestOnly @TestOnly
fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) { fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) {
val scriptDefinition = findScriptDefinition(virtualFile, project)!! val scriptDefinition = findScriptDefinition(virtualFile, project)!!
SyncScriptDependenciesLoader(virtualFile, scriptDefinition, project).updateDependencies() SyncScriptDependenciesLoader(project).updateDependencies(virtualFile, scriptDefinition)
} }
} }
} }
@@ -32,12 +32,16 @@ import com.intellij.psi.PsiManager
import com.intellij.util.Alarm import com.intellij.util.Alarm
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.script.dependencies.AsyncScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.FromFileAttributeScriptDependenciesLoader import org.jetbrains.kotlin.idea.core.script.dependencies.FromFileAttributeScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.ScriptDependenciesLoader import org.jetbrains.kotlin.idea.core.script.dependencies.SyncScriptDependenciesLoader
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.LegacyResolverWrapper
import org.jetbrains.kotlin.script.findScriptDefinition import org.jetbrains.kotlin.script.findScriptDefinition
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.ScriptDependencies
class ScriptDependenciesUpdater( class ScriptDependenciesUpdater(
@@ -47,6 +51,10 @@ class ScriptDependenciesUpdater(
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project) private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400 private val scriptChangesListenerDelay = 1400
private val asyncLoader = AsyncScriptDependenciesLoader(project)
private val syncLoader = SyncScriptDependenciesLoader(project)
private val fileAttributeLoader = FromFileAttributeScriptDependenciesLoader(project)
init { init {
listenForChangesInScripts() listenForChangesInScripts()
} }
@@ -56,12 +64,21 @@ class ScriptDependenciesUpdater(
val scriptDef = findScriptDefinition(file, project) ?: return ScriptDependencies.Empty val scriptDef = findScriptDefinition(file, project) ?: return ScriptDependencies.Empty
FromFileAttributeScriptDependenciesLoader(file, scriptDef, project).updateDependencies() fileAttributeLoader.updateDependencies(file, scriptDef)
ScriptDependenciesLoader.updateDependencies(file, scriptDef, project)
updateDependencies(file, scriptDef)
return cache[file] ?: ScriptDependencies.Empty return cache[file] ?: ScriptDependencies.Empty
} }
private fun updateDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition) {
val loader = when (scriptDef.dependencyResolver) {
is AsyncDependenciesResolver, is LegacyResolverWrapper -> asyncLoader
else -> syncLoader
}
loader.updateDependencies(file, scriptDef)
}
private fun listenForChangesInScripts() { private fun listenForChangesInScripts() {
project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) { override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
@@ -81,7 +98,8 @@ class ScriptDependenciesUpdater(
val scriptDef = ktFile.script?.kotlinScriptDefinition ?: return val scriptDef = ktFile.script?.kotlinScriptDefinition ?: return
if (!ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) return if (!ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) return
ScriptDependenciesLoader.updateDependencies(file, scriptDef, project)
updateDependencies(file, scriptDef)
} }
}) })
@@ -113,7 +131,7 @@ class ScriptDependenciesUpdater(
scriptsQueue.addRequest( scriptsQueue.addRequest(
{ {
FileDocumentManager.getInstance().saveDocument(document) FileDocumentManager.getInstance().saveDocument(document)
ScriptDependenciesLoader.updateDependencies(file, scriptDef, project) updateDependencies(file, scriptDef)
}, },
scriptChangesListenerDelay, scriptChangesListenerDelay,
true true
@@ -5,128 +5,104 @@
package org.jetbrains.kotlin.idea.core.script.dependencies package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import kotlinx.coroutines.* import kotlinx.coroutines.Runnable
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.cancelOnDisposal
import org.jetbrains.kotlin.script.KotlinScriptDefinition import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.LegacyResolverWrapper import org.jetbrains.kotlin.script.LegacyResolverWrapper
import org.jetbrains.kotlin.script.asResolveFailure import org.jetbrains.kotlin.script.asResolveFailure
import java.util.concurrent.Executors import org.jetbrains.kotlin.script.findScriptDefinition
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.dependencies.DependenciesResolver
@Suppress("EXPERIMENTAL_FEATURE_WARNING") class AsyncScriptDependenciesLoader internal constructor(project: Project) : ScriptDependenciesLoader(project) {
class AsyncScriptDependenciesLoader( private val backgroundTaskLock = ReentrantReadWriteLock()
file: VirtualFile, private var backgroundTasksQueue: LoaderBackgroundTask? = null
scriptDef: KotlinScriptDefinition,
project: Project
) : ScriptDependenciesLoader(file, scriptDef, project) {
override fun loadDependencies() { override fun loadDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition) {
if (!shouldSendNewRequest(lastRequest)) { backgroundTaskLock.write {
return if (backgroundTasksQueue == null) {
} backgroundTasksQueue = LoaderBackgroundTask()
backgroundTasksQueue!!.addTask(file)
lastRequest?.cancel() backgroundTasksQueue!!.start()
lastRequest = sendRequest().stampBy(file) } else {
backgroundTasksQueue!!.addTask(file)
if (shouldUseBackgroundThread()) {
runBlocking {
lastRequest?.job?.actualJob?.join()
} }
} }
} }
override fun shouldUseBackgroundThread() = KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
override fun shouldShowNotification(): Boolean = !KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled override fun shouldShowNotification(): Boolean = !KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
private var lastRequest: ModStampedRequest? = null private fun runDependenciesUpdate(file: VirtualFile) {
val scriptDef = runReadAction { findScriptDefinition(file, project) } ?: return
private val asyncUpdatesDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() // runBlocking is using there to avoid loading dependencies asynchronously
private val legacyUpdatesDispatcher = // because it leads to starting more than one gradle daemon in case of resolving dependencies in build.gradle.kts
Executors.newFixedThreadPool( // It is more efficient to use one hot daemon consistently than multiple daemon in parallel
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) val result = runBlocking {
).asCoroutineDispatcher() try {
resolveDependencies(file, scriptDef)
private fun shouldSendNewRequest(previousRequest: ModStampedRequest?): Boolean { } catch (t: Throwable) {
if (previousRequest == null) return true t.asResolveFailure(scriptDef)
return file.modificationStamp != previousRequest.modificationStamp }
}
processResult(result, file, scriptDef)
} }
private fun sendRequest(): TimeStampedJob { private suspend fun resolveDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition): DependenciesResolver.ResolveResult {
val currentTimeStamp = TimeStamps.next()
val dependenciesResolver = scriptDef.dependencyResolver val dependenciesResolver = scriptDef.dependencyResolver
val scriptContents = contentLoader.getScriptContents(scriptDef, file) val scriptContents = contentLoader.getScriptContents(scriptDef, file)
val environment = contentLoader.getEnvironment(scriptDef) val environment = contentLoader.getEnvironment(scriptDef)
val newJob = if (dependenciesResolver is AsyncDependenciesResolver) { return if (dependenciesResolver is AsyncDependenciesResolver) {
launchAsyncUpdate(asyncUpdatesDispatcher, currentTimeStamp) { dependenciesResolver.resolveAsync(scriptContents, environment)
dependenciesResolver.resolveAsync(scriptContents, environment)
}
} else { } else {
assert(dependenciesResolver is LegacyResolverWrapper) assert(dependenciesResolver is LegacyResolverWrapper)
launchAsyncUpdate(legacyUpdatesDispatcher, currentTimeStamp) { dependenciesResolver.resolve(scriptContents, environment)
dependenciesResolver.resolve(scriptContents, environment) }
}
private inner class LoaderBackgroundTask {
private val sequenceOfFiles: ConcurrentLinkedQueue<VirtualFile> = ConcurrentLinkedQueue()
fun start() {
if (shouldShowNotification()) {
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
loadDependencies(null)
})
} else {
object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
loadDependencies(indicator)
}
}.queue()
} }
} }
return TimeStampedJob(newJob, currentTimeStamp) fun addTask(file: VirtualFile) {
} sequenceOfFiles.add(file)
private fun launchAsyncUpdate(
dispatcher: CoroutineDispatcher,
currentTimeStamp: TimeStamp,
doResolve: suspend () -> DependenciesResolver.ResolveResult
): Job = GlobalScope.launch(dispatcher + project.cancelOnDisposal) {
val result = try {
doResolve()
} catch (t: Throwable) {
t.asResolveFailure(scriptDef)
} }
processResult(currentTimeStamp, result) private fun loadDependencies(indicator: ProgressIndicator?) {
} while (true) {
if (indicator?.isCanceled == true || sequenceOfFiles.isEmpty()) {
private fun processResult( backgroundTaskLock.write {
currentTimeStamp: TimeStamp, backgroundTasksQueue = null
result: DependenciesResolver.ResolveResult }
) { return
val lastTimeStamp = lastRequest?.job?.timeStamp }
val isLastSentRequest = lastTimeStamp == null || lastTimeStamp == currentTimeStamp runDependenciesUpdate(sequenceOfFiles.poll())
if (isLastSentRequest) {
if (lastRequest != null) {
// no job running atm unless there is a job started while we process this result
lastRequest = ModStampedRequest(lastRequest!!.modificationStamp, job = null)
} }
processResult(result)
} }
} }
private class ModStampedRequest(val modificationStamp: Long, val job: TimeStampedJob?) {
fun cancel() = job?.actualJob?.cancel()
}
private class TimeStampedJob(val actualJob: Job, val timeStamp: TimeStamp) {
fun stampBy(virtualFile: VirtualFile) =
ModStampedRequest(
virtualFile.modificationStamp,
this
)
}
private data class TimeStamp(private val stamp: Long) {
operator fun compareTo(other: TimeStamp) = this.stamp.compareTo(other.stamp)
}
private object TimeStamps {
private var current: Long = 0
fun next() =
TimeStamp(current++)
}
} }
@@ -11,18 +11,14 @@ import org.jetbrains.kotlin.idea.core.script.scriptDependencies
import org.jetbrains.kotlin.script.KotlinScriptDefinition import org.jetbrains.kotlin.script.KotlinScriptDefinition
import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.ScriptDependencies
class FromFileAttributeScriptDependenciesLoader( class FromFileAttributeScriptDependenciesLoader(project: Project) : ScriptDependenciesLoader(project) {
file: VirtualFile,
scriptDef: KotlinScriptDefinition,
project: Project
) : ScriptDependenciesLoader(file, scriptDef, project) {
override fun loadDependencies() { override fun loadDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition) {
val deserializedDependencies = file.scriptDependencies ?: return val deserializedDependencies = file.scriptDependencies ?: return
saveToCache(deserializedDependencies) saveToCache(deserializedDependencies, file)
} }
private fun saveToCache(deserialized: ScriptDependencies) { private fun saveToCache(deserialized: ScriptDependencies, file: VirtualFile) {
val rootsChanged = cache.hasNotCachedRoots(deserialized) val rootsChanged = cache.hasNotCachedRoots(deserialized)
cache.save(file, deserialized) cache.save(file, deserialized)
if (rootsChanged) { if (rootsChanged) {
@@ -30,6 +26,5 @@ class FromFileAttributeScriptDependenciesLoader(
} }
} }
override fun shouldUseBackgroundThread() = false
override fun shouldShowNotification(): Boolean = false override fun shouldShowNotification(): Boolean = false
} }
@@ -8,60 +8,33 @@ package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.SLRUMap
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import org.jetbrains.kotlin.idea.core.script.* import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.script.* import org.jetbrains.kotlin.script.KotlinScriptDefinition
import java.util.concurrent.ConcurrentHashMap import org.jetbrains.kotlin.script.ScriptContentLoader
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver import org.jetbrains.kotlin.script.ScriptReportSink
import org.jetbrains.kotlin.script.adjustByDefinition
import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.dependencies.DependenciesResolver
abstract class ScriptDependenciesLoader( abstract class ScriptDependenciesLoader(protected val project: Project) {
protected val file: VirtualFile,
protected val scriptDef: KotlinScriptDefinition,
protected val project: Project
) {
companion object {
private val loaders = ConcurrentHashMap<VirtualFile, ScriptDependenciesLoader>()
fun updateDependencies( fun updateDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition) {
file: VirtualFile, if (cache[file] == null || fileModificationStamps[file.path] != file.modificationStamp) {
scriptDef: KotlinScriptDefinition, fileModificationStamps.put(file.path, file.modificationStamp)
project: Project
) {
val existingLoader = loaders[file]
if (existingLoader != null) return existingLoader.updateDependencies()
val newLoader = when (scriptDef.dependencyResolver) { loadDependencies(file, scriptDef)
is AsyncDependenciesResolver,
is LegacyResolverWrapper -> AsyncScriptDependenciesLoader(file, scriptDef, project)
else -> SyncScriptDependenciesLoader(file, scriptDef, project)
}
loaders.put(file, newLoader)
newLoader.updateDependencies()
} }
} }
fun updateDependencies() { private val fileModificationStamps: SLRUMap<String, Long> = SLRUMap(10, 10)
if (shouldUseBackgroundThread()) {
object : Task.Backgroundable(project, "Kotlin: Loading dependencies for ${file.name} ...", true) {
override fun run(indicator: ProgressIndicator) {
loadDependencies()
}
}.queue()
} else {
loadDependencies()
}
}
protected abstract fun loadDependencies() protected abstract fun loadDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition)
protected abstract fun shouldUseBackgroundThread(): Boolean
protected abstract fun shouldShowNotification(): Boolean protected abstract fun shouldShowNotification(): Boolean
protected val contentLoader = ScriptContentLoader(project) protected val contentLoader = ScriptContentLoader(project)
@@ -69,12 +42,10 @@ abstract class ScriptDependenciesLoader(
private val reporter: ScriptReportSink = ServiceManager.getService(project, ScriptReportSink::class.java) private val reporter: ScriptReportSink = ServiceManager.getService(project, ScriptReportSink::class.java)
protected fun processResult(result: DependenciesResolver.ResolveResult) { protected fun processResult(result: DependenciesResolver.ResolveResult, file: VirtualFile, scriptDef: KotlinScriptDefinition) {
loaders.remove(file)
if (cache[file] == null) { if (cache[file] == null) {
saveDependencies(result) saveDependencies(result, file, scriptDef)
attachReportsIfChanged(result) attachReportsIfChanged(result, file, scriptDef)
return return
} }
@@ -82,15 +53,15 @@ abstract class ScriptDependenciesLoader(
if (cache[file] != newDependencies) { if (cache[file] != newDependencies) {
if (shouldShowNotification() && !ApplicationManager.getApplication().isUnitTestMode) { if (shouldShowNotification() && !ApplicationManager.getApplication().isUnitTestMode) {
file.addScriptDependenciesNotificationPanel(result, project) { file.addScriptDependenciesNotificationPanel(result, project) {
saveDependencies(it) saveDependencies(it, file, scriptDef)
attachReportsIfChanged(it) attachReportsIfChanged(it, file, scriptDef)
} }
} else { } else {
saveDependencies(result) saveDependencies(result, file, scriptDef)
attachReportsIfChanged(result) attachReportsIfChanged(result, file, scriptDef)
} }
} else { } else {
attachReportsIfChanged(result) attachReportsIfChanged(result, file, scriptDef)
if (shouldShowNotification()) { if (shouldShowNotification()) {
file.removeScriptDependenciesNotificationPanel(project) file.removeScriptDependenciesNotificationPanel(project)
@@ -98,13 +69,13 @@ abstract class ScriptDependenciesLoader(
} }
} }
private fun attachReportsIfChanged(result: DependenciesResolver.ResolveResult) { private fun attachReportsIfChanged(result: DependenciesResolver.ResolveResult, file: VirtualFile, scriptDef: KotlinScriptDefinition) {
if (file.getUserData(IdeScriptReportSink.Reports) != result.reports.takeIf { it.isNotEmpty() }) { if (file.getUserData(IdeScriptReportSink.Reports) != result.reports.takeIf { it.isNotEmpty() }) {
reporter.attachReports(file, result.reports) reporter.attachReports(file, result.reports)
} }
} }
private fun saveDependencies(result: DependenciesResolver.ResolveResult) { private fun saveDependencies(result: DependenciesResolver.ResolveResult, file: VirtualFile, scriptDef: KotlinScriptDefinition) {
if (shouldShowNotification()) { if (shouldShowNotification()) {
file.removeScriptDependenciesNotificationPanel(project) file.removeScriptDependenciesNotificationPanel(project)
} }
@@ -120,7 +91,6 @@ abstract class ScriptDependenciesLoader(
} }
} }
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
protected fun notifyRootsChanged() { protected fun notifyRootsChanged() {
val doNotifyRootsChanged = Runnable { val doNotifyRootsChanged = Runnable {
runWriteAction { runWriteAction {
@@ -9,17 +9,11 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.script.KotlinScriptDefinition import org.jetbrains.kotlin.script.KotlinScriptDefinition
class SyncScriptDependenciesLoader( class SyncScriptDependenciesLoader internal constructor(project: Project) : ScriptDependenciesLoader(project) {
file: VirtualFile, override fun loadDependencies(file: VirtualFile, scriptDef: KotlinScriptDefinition) {
scriptDef: KotlinScriptDefinition,
project: Project
) : ScriptDependenciesLoader(file, scriptDef, project) {
override fun loadDependencies() {
val result = contentLoader.loadContentsAndResolveDependencies(scriptDef, file) val result = contentLoader.loadContentsAndResolveDependencies(scriptDef, file)
processResult(result) processResult(result, file, scriptDef)
} }
override fun shouldUseBackgroundThread(): Boolean = false
override fun shouldShowNotification(): Boolean = false override fun shouldShowNotification(): Boolean = false
} }