Refactoring of ScriptDependenciesUpdater

Extract classes for sync and async dependencies loading
This commit is contained in:
Natalia Selezneva
2018-04-28 13:14:35 +03:00
parent 97848f2812
commit 082916be70
8 changed files with 322 additions and 212 deletions
@@ -17,7 +17,6 @@
package org.jetbrains.kotlin.script
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
@@ -78,7 +77,6 @@ class ScriptContentLoader(private val project: Project) {
catch (e: Throwable) {
e.asResolveFailure(scriptDef)
}
ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports)
return result
}
@@ -99,7 +99,7 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
definitions = definitionsByContributor.values.flattenTo(mutableListOf()).asSequence()
clearCache()
// TODO: clear by script type/definition
ServiceManager.getService(project, ScriptDependenciesUpdater::class.java).clear()
ServiceManager.getService(project, ScriptDependenciesCache::class.java).clear()
}
private fun ScriptDefinitionContributor.safeGetDefinitions(): List<KotlinScriptDefinition> {
@@ -23,6 +23,7 @@ import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.URLUtil
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.dependencies.SyncScriptDependenciesLoader
import org.jetbrains.kotlin.script.ScriptDefinitionProvider
import org.jetbrains.kotlin.script.ScriptDependenciesProvider
import org.jetbrains.kotlin.script.findScriptDefinition
@@ -78,8 +79,7 @@ class ScriptDependenciesManager internal constructor(
fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) {
with(getInstance(project)) {
val scriptDefinition = ScriptDefinitionProvider.getInstance(project).findScriptDefinition(virtualFile)!!
cacheUpdater.updateSync(virtualFile, scriptDefinition)
cacheUpdater.notifyRootsChanged()
SyncScriptDependenciesLoader(virtualFile, scriptDefinition, project, shouldNotifyRootsChanged = true).updateDependencies()
}
}
}
@@ -18,103 +18,52 @@ package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
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.isProjectOrWorkspaceFile
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.ex.StatusBarEx
import com.intellij.openapi.wm.ex.WindowManagerEx
import kotlinx.coroutines.experimental.CoroutineDispatcher
import kotlinx.coroutines.experimental.asCoroutineDispatcher
import kotlinx.coroutines.experimental.launch
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.core.util.cancelOnDisposal
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.core.script.dependencies.FromFileAttributeScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.ScriptDependenciesLoader
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.kotlin.script.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.DependenciesResolver
import org.jetbrains.kotlin.script.ScriptDefinitionProvider
import org.jetbrains.kotlin.script.findScriptDefinition
import kotlin.script.experimental.dependencies.ScriptDependencies
import kotlin.script.experimental.dependencies.ScriptReport
class ScriptDependenciesUpdater(
private val project: Project,
private val cache: ScriptDependenciesCache,
private val scriptDefinitionProvider: ScriptDefinitionProvider
) {
private val requests = ConcurrentHashMap<String, ModStampedRequest>()
private val contentLoader = ScriptContentLoader(project)
private val asyncUpdatesDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
private val legacyUpdatesDispatcher =
Executors.newFixedThreadPool(
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
).asCoroutineDispatcher()
private val modifiedScripts = mutableSetOf<VirtualFile>()
init {
listenToVfsChanges()
}
private class TimeStampedJob(val actualJob: Task.Backgroundable, val timeStamp: TimeStamp) {
fun stampBy(virtualFile: VirtualFile) = ModStampedRequest(virtualFile.modificationStamp, this)
}
private class ModStampedRequest(
val modificationStamp: Long,
val job: TimeStampedJob?
) {
fun cancel() {
val actualJob = job?.actualJob ?: return
val frame = (WindowManager.getInstance() as? WindowManagerEx)?.findFrameFor(actualJob.project)
val statusBar = frame?.statusBar as? StatusBarEx ?: return
statusBar.backgroundProcesses.find { it.first == actualJob }?.second?.cancel()
}
}
fun getCurrentDependencies(file: VirtualFile): ScriptDependencies {
cache[file]?.let { return it }
tryLoadingFromDisk(file)
performUpdate(file)
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) ?: return ScriptDependencies.Empty
FromFileAttributeScriptDependenciesLoader(file, scriptDef, project).updateDependencies()
ScriptDependenciesLoader.updateDependencies(file, scriptDef, project, shouldNotifyRootsChanged = false)
return cache[file] ?: ScriptDependencies.Empty
}
fun reloadModifiedScripts() {
for (it in modifiedScripts.filter { cache[it] != null }) {
performUpdate(it)
val scriptDef = scriptDefinitionProvider.findScriptDefinition(it) ?: return
ScriptDependenciesLoader.updateDependencies(it, scriptDef, project, shouldNotifyRootsChanged = true)
}
modifiedScripts.clear()
}
private fun tryLoadingFromDisk(file: VirtualFile): Boolean {
val deserializedDependencies = file.scriptDependencies ?: return false
saveToCache(deserializedDependencies, file)
return true
}
private fun saveToCache(deserialized: ScriptDependencies, file: VirtualFile) {
val rootsChanged = cache.hasNotCachedRoots(deserialized)
cache.save(file, deserialized)
if (rootsChanged) {
notifyRootsChanged()
}
}
fun requestUpdate(files: Iterable<VirtualFile>) {
files.forEach { file ->
if (!file.isValid) {
@@ -125,137 +74,6 @@ class ScriptDependenciesUpdater(
}
}
private fun performUpdate(file: VirtualFile) {
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) ?: return
when (scriptDef.dependencyResolver) {
is AsyncDependenciesResolver, is LegacyResolverWrapper -> {
updateAsync(file, scriptDef)
}
else -> updateSync(file, scriptDef)
}
}
private fun updateAsync(
file: VirtualFile,
scriptDefinition: KotlinScriptDefinition
) {
val path = file.path
val lastRequest = requests[path]
if (!shouldSendNewRequest(file, lastRequest)) {
return
}
lastRequest?.cancel()
requests[path] = sendRequest(file, scriptDefinition).stampBy(file)
return
}
private fun shouldSendNewRequest(file: VirtualFile, previousRequest: ModStampedRequest?): Boolean {
if (previousRequest == null) return true
return file.modificationStamp != previousRequest.modificationStamp
}
private fun sendRequest(
file: VirtualFile,
scriptDef: KotlinScriptDefinition
): TimeStampedJob {
val currentTimeStamp = TimeStamps.next()
val newJob = object : Task.Backgroundable(project, "Kotlin: Loading dependencies for ${file.name} ...", true) {
override fun run(indicator: ProgressIndicator) {
if (updateSync(file, scriptDef)) {
notifyRootsChanged()
}
}
}
newJob.queue()
return TimeStampedJob(newJob, currentTimeStamp)
}
private fun launchAsyncUpdate(
dispatcher: CoroutineDispatcher,
file: VirtualFile,
currentTimeStamp: TimeStamp,
scriptDef: KotlinScriptDefinition,
doResolve: suspend () -> DependenciesResolver.ResolveResult
) = launch(dispatcher + project.cancelOnDisposal) {
val result = try {
doResolve()
} catch (t: Throwable) {
t.asResolveFailure(scriptDef)
}
processResult(file, currentTimeStamp, result, scriptDef)
}
private fun processResult(
file: VirtualFile,
currentTimeStamp: TimeStamp,
result: DependenciesResolver.ResolveResult,
scriptDef: KotlinScriptDefinition
) {
val lastRequest = requests[file.path]
val lastTimeStamp = lastRequest?.job?.timeStamp
val isLastSentRequest = lastTimeStamp == null || lastTimeStamp == currentTimeStamp
if (isLastSentRequest) {
if (lastRequest != null) {
// no job running atm unless there is a job started while we process this result
requests.replace(file.path, lastRequest, ModStampedRequest(lastRequest.modificationStamp, job = null))
}
ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports)
val resultingDependencies = result.dependencies?.adjustByDefinition(scriptDef) ?: return
if (saveNewDependencies(resultingDependencies, file, result.reports.any { it.severity == ScriptReport.Severity.FATAL })) {
notifyRootsChanged()
}
}
}
fun updateSync(file: VirtualFile, scriptDef: KotlinScriptDefinition): Boolean {
val result = contentLoader.loadContentsAndResolveDependencies(scriptDef, file)
val newDeps = result.dependencies?.adjustByDefinition(scriptDef) ?: ScriptDependencies.Empty
return saveNewDependencies(newDeps, file, result.reports.any { it.severity == ScriptReport.Severity.FATAL })
}
private fun saveNewDependencies(
new: ScriptDependencies,
file: VirtualFile,
hasFatalErrors: Boolean
): Boolean {
val rootsChanged = cache.hasNotCachedRoots(new)
if (cache.save(file, new)) {
if (hasFatalErrors) {
file.scriptDependencies = null
} else {
file.scriptDependencies = new
}
}
return rootsChanged
}
fun notifyRootsChanged() {
val rootsChangesRunnable = {
runWriteAction {
if (project.isDisposed) return@runWriteAction
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) {
rootsChangesRunnable()
} else {
launch(EDT(project)) {
rootsChangesRunnable()
}
}
}
private fun listenToVfsChanges() {
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() {
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
@@ -279,21 +97,6 @@ class ScriptDependenciesUpdater(
}
})
}
fun clear() {
cache.clear()
requests.clear()
}
}
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++)
}
@set: TestOnly
@@ -0,0 +1,128 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import kotlinx.coroutines.experimental.*
import org.jetbrains.kotlin.idea.core.util.cancelOnDisposal
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.LegacyResolverWrapper
import org.jetbrains.kotlin.script.asResolveFailure
import java.util.concurrent.Executors
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.DependenciesResolver
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
class AsyncScriptDependenciesLoader(
file: VirtualFile,
scriptDef: KotlinScriptDefinition,
project: Project
) : ScriptDependenciesLoader(file, scriptDef, project, true) {
override fun loadDependencies() {
if (!shouldSendNewRequest(lastRequest)) {
return
}
lastRequest?.cancel()
lastRequest = sendRequest().stampBy(file)
runBlocking {
lastRequest?.job?.actualJob?.join()
}
}
override fun shouldUseBackgroundThread() = true
private var lastRequest: ModStampedRequest? = null
private val asyncUpdatesDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
private val legacyUpdatesDispatcher =
Executors.newFixedThreadPool(
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
).asCoroutineDispatcher()
private fun shouldSendNewRequest(previousRequest: ModStampedRequest?): Boolean {
if (previousRequest == null) return true
return file.modificationStamp != previousRequest.modificationStamp
}
private fun sendRequest(): TimeStampedJob {
val currentTimeStamp = TimeStamps.next()
val dependenciesResolver = scriptDef.dependencyResolver
val scriptContents = contentLoader.getScriptContents(scriptDef, file)
val environment = contentLoader.getEnvironment(scriptDef)
val newJob = if (dependenciesResolver is AsyncDependenciesResolver) {
launchAsyncUpdate(asyncUpdatesDispatcher, currentTimeStamp) {
dependenciesResolver.resolveAsync(scriptContents, environment)
}
} else {
assert(dependenciesResolver is LegacyResolverWrapper)
launchAsyncUpdate(legacyUpdatesDispatcher, currentTimeStamp) {
dependenciesResolver.resolve(scriptContents, environment)
}
}
return TimeStampedJob(newJob, currentTimeStamp)
}
private fun launchAsyncUpdate(
dispatcher: CoroutineDispatcher,
currentTimeStamp: TimeStamp,
doResolve: suspend () -> DependenciesResolver.ResolveResult
): Job = launch(dispatcher + project.cancelOnDisposal) {
val result = try {
doResolve()
} catch (t: Throwable) {
t.asResolveFailure(scriptDef)
}
processResult(currentTimeStamp, result)
}
private fun processResult(
currentTimeStamp: TimeStamp,
result: DependenciesResolver.ResolveResult
) {
val lastTimeStamp = lastRequest?.job?.timeStamp
val isLastSentRequest = lastTimeStamp == null || lastTimeStamp == currentTimeStamp
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++)
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.scriptDependencies
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import kotlin.script.experimental.dependencies.ScriptDependencies
class FromFileAttributeScriptDependenciesLoader(
file: VirtualFile,
scriptDef: KotlinScriptDefinition,
project: Project
) : ScriptDependenciesLoader(file, scriptDef, project, true) {
override fun loadDependencies() {
val deserializedDependencies = file.scriptDependencies ?: return
saveToCache(deserializedDependencies)
}
private fun saveToCache(deserialized: ScriptDependencies) {
val rootsChanged = cache.hasNotCachedRoots(deserialized)
cache.save(file, deserialized)
if (rootsChanged) {
notifyRootsChanged()
}
}
override fun shouldUseBackgroundThread() = false
}
@@ -0,0 +1,122 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.application.ApplicationManager
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.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.SLRUMap
import kotlinx.coroutines.experimental.launch
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesCache
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.scriptDependencies
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.script.*
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.DependenciesResolver
import kotlin.script.experimental.dependencies.ScriptDependencies
import kotlin.script.experimental.dependencies.ScriptReport
abstract class ScriptDependenciesLoader(
protected val file: VirtualFile,
protected val scriptDef: KotlinScriptDefinition,
protected val project: Project,
private val shouldNotifyRootsChanged: Boolean
) {
companion object {
private val loaders = SLRUMap<VirtualFile, ScriptDependenciesLoader>(10, 10)
fun updateDependencies(
file: VirtualFile,
scriptDef: KotlinScriptDefinition,
project: Project,
shouldNotifyRootsChanged: Boolean
) {
val existingLoader = loaders[file]
if (existingLoader != null) return existingLoader.updateDependencies()
val newLoader = when (scriptDef.dependencyResolver) {
is AsyncDependenciesResolver,
is LegacyResolverWrapper -> AsyncScriptDependenciesLoader(file, scriptDef, project)
else -> SyncScriptDependenciesLoader(file, scriptDef, project, shouldNotifyRootsChanged)
}
loaders.put(file, newLoader)
newLoader.updateDependencies()
}
}
fun updateDependencies() {
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 shouldUseBackgroundThread(): Boolean
protected val contentLoader = ScriptContentLoader(project)
protected val cache: ScriptDependenciesCache = ServiceManager.getService(project, ScriptDependenciesCache::class.java)
protected fun processResult(result: DependenciesResolver.ResolveResult) {
saveDependencies(result)
}
private fun saveDependencies(result: DependenciesResolver.ResolveResult) {
ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports)
val newDependencies = result.dependencies?.adjustByDefinition(scriptDef) ?: ScriptDependencies.Empty
val rootsChanged = cache.hasNotCachedRoots(newDependencies)
if (cache.save(file, newDependencies)) {
if (result.reports.any { it.severity == ScriptReport.Severity.FATAL }) {
file.scriptDependencies = null
} else {
file.scriptDependencies = newDependencies
}
}
if (rootsChanged) {
notifyRootsChanged()
}
loaders.remove(file)
}
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
protected fun notifyRootsChanged() {
if (!shouldNotifyRootsChanged) return
val rootsChangesRunnable = {
runWriteAction {
if (project.isDisposed) return@runWriteAction
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) {
rootsChangesRunnable()
} else {
launch(EDT(project)) {
rootsChangesRunnable()
}
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.script.KotlinScriptDefinition
class SyncScriptDependenciesLoader(
file: VirtualFile,
scriptDef: KotlinScriptDefinition,
project: Project,
shouldNotifyRootsChanged: Boolean
) : ScriptDependenciesLoader(file, scriptDef, project, shouldNotifyRootsChanged) {
override fun loadDependencies() {
val result = contentLoader.loadContentsAndResolveDependencies(scriptDef, file)
processResult(result)
}
override fun shouldUseBackgroundThread(): Boolean = false
}