KSCM: rework async requests

This commit is contained in:
Pavel V. Talanov
2017-06-14 18:49:20 +03:00
parent 3c511f3f40
commit 930d502b12
@@ -38,8 +38,12 @@ import org.jetbrains.annotations.TestOnly
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.*
import java.io.File import java.io.File
import java.lang.Math.max
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.supplyAsync
import java.util.concurrent.Executors.newFixedThreadPool
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Supplier
import kotlin.concurrent.read import kotlin.concurrent.read
import kotlin.concurrent.write import kotlin.concurrent.write
import kotlin.script.dependencies.KotlinScriptExternalDependencies import kotlin.script.dependencies.KotlinScriptExternalDependencies
@@ -59,6 +63,7 @@ class KotlinScriptConfigurationManager(
) { ) {
private val cacheLock = ReentrantReadWriteLock() private val cacheLock = ReentrantReadWriteLock()
private val threadPool = newFixedThreadPool(max(1, Runtime.getRuntime().availableProcessors() / 2))
init { init {
reloadScriptDefinitions() reloadScriptDefinitions()
@@ -88,7 +93,7 @@ class KotlinScriptConfigurationManager(
} }
private val allScriptsClasspathCache = ClearableLazyValue(cacheLock) { private val allScriptsClasspathCache = ClearableLazyValue(cacheLock) {
val files = cache.values.flatMap { it.classpath }.distinct() val files = cache.values.flatMap { it?.dependencies?.classpath ?: emptyList() }.distinct()
toVfsRoots(files) toVfsRoots(files)
} }
@@ -97,7 +102,7 @@ class KotlinScriptConfigurationManager(
} }
private val allLibrarySourcesCache = ClearableLazyValue(cacheLock) { private val allLibrarySourcesCache = ClearableLazyValue(cacheLock) {
toVfsRoots(cache.values.flatMap { it.sources }.distinct()) toVfsRoots(cache.values.flatMap { it?.dependencies?.sources ?: emptyList() }.distinct())
} }
private val allLibrarySourcesScope = ClearableLazyValue(cacheLock) { private val allLibrarySourcesScope = ClearableLazyValue(cacheLock) {
@@ -138,17 +143,23 @@ class KotlinScriptConfigurationManager(
scriptDefinitionProvider.setScriptDefinitions(def) scriptDefinitionProvider.setScriptDefinitions(def)
} }
private val cache = hashMapOf<String, KotlinScriptExternalDependencies>() private class TimeStampedRequest(val future: CompletableFuture<Unit>, val timeStamp: TimeStamp)
// TODO: this is very primitive way of synchronizing these updates
private var lastStamp: TimeStamp = TimeStamps.next() private class DataAndRequest(
val dependencies: KotlinScriptExternalDependencies?,
val modificationStamp: Long?,
val requestInProgress: TimeStampedRequest? = null
)
private val cache = hashMapOf<String, DataAndRequest?>()
fun <TF : Any> getExternalImports(file: TF): KotlinScriptExternalDependencies = cacheLock.read { fun <TF : Any> getExternalImports(file: TF): KotlinScriptExternalDependencies = cacheLock.read {
val path = getFilePath(file) val path = getFilePath(file)
cache[path]?.let { return it } cache[path]?.dependencies?.let { return it }
updateExternalImportsCache(listOf(file)) updateExternalImportsCache(listOf(file))
return cache[path] ?: NoDependencies return cache[path]?.dependencies ?: NoDependencies
} }
private fun <TF: Any> updateExternalImportsCache(files: Iterable<TF>) = cacheLock.write { private fun <TF: Any> updateExternalImportsCache(files: Iterable<TF>) = cacheLock.write {
@@ -171,52 +182,102 @@ class KotlinScriptConfigurationManager(
return updateSync(file, scriptDef) return updateSync(file, scriptDef)
} }
fun <TF : Any> updateAsync(file: TF, scriptDefinition: KotlinScriptDefinitionFromAnnotatedTemplate): Boolean { private fun <TF : Any> updateAsync(file: TF, scriptDefinition: KotlinScriptDefinitionFromAnnotatedTemplate): Boolean {
val path = getFilePath(file) val path = getFilePath(file)
val oldDependencies = cache[path] val oldDataAndRequest = cache[path]
if (!shouldSendNewRequest(file, oldDataAndRequest)) {
return false
}
oldDataAndRequest?.requestInProgress?.future?.cancel(true)
val (currentTimeStamp, newFuture) = sendRequest(path, scriptDefinition, file, oldDataAndRequest)
cache[path] = DataAndRequest(
oldDataAndRequest?.dependencies,
fileModificationStamp(file),
TimeStampedRequest(newFuture, currentTimeStamp)
)
return false // not changed immediately
}
private fun <TF : Any> sendRequest(
path: String,
scriptDefinition: KotlinScriptDefinitionFromAnnotatedTemplate,
file: TF,
oldDataAndRequest: DataAndRequest?
): Pair<TimeStamp, CompletableFuture<Unit>> {
val currentTimeStamp = TimeStamps.next()
val scriptContents = scriptDefinition.makeScriptContents(file, project) val scriptContents = scriptDefinition.makeScriptContents(file, project)
val updateStamp = TimeStamps.next() val newFuture = supplyAsync(Supplier {
val newDependencies = scriptDefinition.getDependenciesFor(oldDataAndRequest?.dependencies, scriptContents)
CompletableFuture.supplyAsync { cacheLock.read {
val newDependencies = scriptDefinition.getDependenciesFor(oldDependencies, scriptContents) val lastTimeStamp = cache[path]?.requestInProgress?.timeStamp
cacheLock.write { if (lastTimeStamp == currentTimeStamp) {
if (updateStamp >= lastStamp) { if (cacheSync(newDependencies, oldDataAndRequest?.dependencies, path, file as? VirtualFile)) {
lastStamp = updateStamp
if (cacheNewDependencies(newDependencies, oldDependencies, path)) {
invalidateLocalCaches() invalidateLocalCaches()
notifyRootsChanged() notifyRootsChanged()
} }
} }
} }
} }, threadPool)
return false // not changed immediately return Pair(currentTimeStamp, newFuture)
} }
private fun <TF : Any> shouldSendNewRequest(file: TF, oldDataAndRequest: DataAndRequest?): Boolean {
val currentStamp = fileModificationStamp(file)
val previousStamp = oldDataAndRequest?.modificationStamp
if (currentStamp == null || currentStamp != previousStamp) {
return true
}
return oldDataAndRequest.requestInProgress == null
}
private fun <TF : Any> fileModificationStamp(file: TF) = (file as? VirtualFile)?.modificationStamp
private fun <TF : Any> updateSync(file: TF, scriptDef: KotlinScriptDefinition): Boolean { private fun <TF : Any> updateSync(file: TF, scriptDef: KotlinScriptDefinition): Boolean {
val path = getFilePath(file) val path = getFilePath(file)
val oldDeps = cache[path] val oldDeps = cache[path]?.dependencies
val deps = scriptDef.getDependenciesFor(file, project, oldDeps) val deps = scriptDef.getDependenciesFor(file, project, oldDeps)
return cacheNewDependencies(deps, oldDeps, path) return cacheSync(deps, oldDeps, path, file as? VirtualFile)
} }
private fun cacheNewDependencies( private fun cacheSync(
new: KotlinScriptExternalDependencies?, new: KotlinScriptExternalDependencies?,
old: KotlinScriptExternalDependencies?, old: KotlinScriptExternalDependencies?,
path: String path: String,
file: VirtualFile?
): Boolean { ): Boolean {
return when { return when {
new != null && (old == null || !(new.match(old))) -> { new != null && (old == null || !(new.match(old))) -> {
// changed or new // changed or new
cache.put(path, new) save(path, new, file, persist = true)
true
}
new != null -> {
save(path, new, file, persist = false)
false
} // same
cache.remove(path) != null -> {
save(path, null, file, persist = true)
true true
} }
new != null -> false // same
cache.remove(path) != null -> true
else -> false // unknown else -> false // unknown
} }
} }
private fun save(path: String, new: KotlinScriptExternalDependencies?, virtualFile: VirtualFile?, persist: Boolean) {
cacheLock.write {
cache.put(path, DataAndRequest(new, virtualFile?.modificationStamp))
}
if (persist && new != null && virtualFile != null) {
ScriptDependenciesFileAttribute.write(virtualFile, new)
}
}
private fun invalidateLocalCaches() { private fun invalidateLocalCaches() {
allScriptsClasspathCache.clear() allScriptsClasspathCache.clear()
allScriptsClasspathScope.clear() allScriptsClasspathScope.clear()