ScriptDependenciesCache: limit simultaneously cached script count

Do not schedule updates for dependencies of scripts that were not requested
   (opened in the editor or such) for some time
This fixes a problem when vcs updates changing scripts can trigger
   undesired script dependencies updates
   (probable cause of script files never highlighted correctly in some cases)
Refactor: use VirtualFile instead of String and move code from GradleScriptTemplateProvider
This commit is contained in:
Pavel V. Talanov
2017-10-19 15:29:54 +03:00
parent f67e01db1c
commit df74fb23f8
3 changed files with 44 additions and 34 deletions
@@ -24,8 +24,10 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder import com.intellij.psi.PsiElementFinder
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import com.intellij.psi.search.NonClasspathDirectoriesScope import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.SLRUMap
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.idea.core.util.EDT
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read import kotlin.concurrent.read
import kotlin.concurrent.write import kotlin.concurrent.write
@@ -35,14 +37,16 @@ import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.isAccessible
import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.ScriptDependencies
private const val MAX_SCRIPTS_CACHED = 15
class ScriptDependenciesCache(private val project: Project) { class ScriptDependenciesCache(private val project: Project) {
private val cacheLock = ReentrantReadWriteLock() private val cacheLock = ReentrantReadWriteLock()
private val cache = hashMapOf<String, ScriptDependencies>() private val cache = SLRUMap<VirtualFile, ScriptDependencies>(0, MAX_SCRIPTS_CACHED)
operator fun get(virtualFile: VirtualFile): ScriptDependencies? = cacheLock.read { cache[virtualFile.path] } operator fun get(virtualFile: VirtualFile): ScriptDependencies? = cacheLock.read { cache[virtualFile] }
val allScriptsClasspath by ClearableLazyValue(cacheLock) { val allScriptsClasspath by ClearableLazyValue(cacheLock) {
val files = cache.values.flatMap { it.classpath }.distinct() val files = cache.entrySet().flatMap { it.value.classpath }.distinct()
ScriptDependenciesManager.toVfsRoots(files) ScriptDependenciesManager.toVfsRoots(files)
} }
@@ -51,15 +55,13 @@ class ScriptDependenciesCache(private val project: Project) {
} }
val allLibrarySources by ClearableLazyValue(cacheLock) { val allLibrarySources by ClearableLazyValue(cacheLock) {
ScriptDependenciesManager.toVfsRoots(cache.values.flatMap { it.sources }.distinct()) ScriptDependenciesManager.toVfsRoots(cache.entrySet().flatMap { it.value.sources }.distinct())
} }
val allLibrarySourcesScope by ClearableLazyValue(cacheLock) { val allLibrarySourcesScope by ClearableLazyValue(cacheLock) {
NonClasspathDirectoriesScope(allLibrarySources) NonClasspathDirectoriesScope(allLibrarySources)
} }
fun <T> inspectCache(body: (Map<String, ScriptDependencies>) -> T): T = cacheLock.read { body(cache) }
private fun onChange(file: VirtualFile?) { private fun onChange(file: VirtualFile?) {
this::allScriptsClasspath.clearValue() this::allScriptsClasspath.clearValue()
this::allScriptsClasspathScope.clearValue() this::allScriptsClasspathScope.clearValue()
@@ -102,10 +104,9 @@ class ScriptDependenciesCache(private val project: Project) {
} }
fun save(virtualFile: VirtualFile, new: ScriptDependencies): Boolean { fun save(virtualFile: VirtualFile, new: ScriptDependencies): Boolean {
val path = virtualFile.path
val old = cacheLock.write { val old = cacheLock.write {
val old = cache[path] val old = cache[virtualFile]
cache[path] = new cache.put(virtualFile, new)
old old
} }
val changed = new != old val changed = new != old
@@ -118,13 +119,36 @@ class ScriptDependenciesCache(private val project: Project) {
fun delete(virtualFile: VirtualFile): Boolean { fun delete(virtualFile: VirtualFile): Boolean {
val changed = cacheLock.write { val changed = cacheLock.write {
cache.remove(virtualFile.path) != null cache.remove(virtualFile) != null
} }
if (changed) { if (changed) {
onChange(virtualFile) onChange(virtualFile)
} }
return changed return changed
} }
fun combineDependencies(filePredicate: (VirtualFile) -> Boolean): ScriptDependencies = cacheLock.read {
val sources = mutableListOf<File>()
val binaries = mutableListOf<File>()
val imports = mutableListOf<String>()
val scripts = mutableListOf<File>()
val relevantEntries = cache.entrySet().filter { filePredicate(it.key) }.map { it.value }
relevantEntries.forEach {
sources += it.sources
binaries += it.classpath
imports += it.imports
scripts += it.scripts
}
return ScriptDependencies(
classpath = binaries.distinct(),
sources = sources.distinct(),
imports = imports.distinct(),
scripts = scripts.distinct(),
javaHome = relevantEntries.map { it.javaHome }.firstOrNull()
)
}
} }
private fun <R> KProperty0<R>.clearValue() { private fun <R> KProperty0<R>.clearValue() {
@@ -84,7 +84,7 @@ internal class ScriptDependenciesUpdater(
tryUsingDefault(file) tryUsingDefault(file)
} }
updateCache(listOf(file)) performUpdate(file)
return cache[file] ?: ScriptDependencies.Empty return cache[file] ?: ScriptDependencies.Empty
} }
@@ -108,17 +108,20 @@ internal class ScriptDependenciesUpdater(
} }
} }
private fun updateCache(files: Iterable<VirtualFile>) = private fun requestUpdate(files: Iterable<VirtualFile>) =
files.map { file -> files.map { file ->
if (!file.isValid) { if (!file.isValid) {
return cache.delete(file) return cache.delete(file)
} }
else if (cache[file] != null) { // only update dependencies for scripts that were touched recently
performUpdate(file)
}
else { else {
updateForFile(file) false
} }
}.contains(true) }.contains(true)
private fun updateForFile(file: VirtualFile): Boolean { private fun performUpdate(file: VirtualFile): Boolean {
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) ?: return false val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) ?: return false
return when (scriptDef.dependencyResolver) { return when (scriptDef.dependencyResolver) {
@@ -263,7 +266,7 @@ internal class ScriptDependenciesUpdater(
return return
} }
if (updateCache(events.mapNotNull { if (requestUpdate(events.mapNotNull {
// The check is partly taken from the BuildManager.java // The check is partly taken from the BuildManager.java
it.file?.takeIf { it.file?.takeIf {
// the isUnitTestMode check fixes ScriptConfigurationHighlighting & Navigation tests, since they are not trigger proper update mechanims // the isUnitTestMode check fixes ScriptConfigurationHighlighting & Navigation tests, since they are not trigger proper update mechanims
@@ -181,23 +181,6 @@ class GradleScriptDefaultDependenciesProvider(
return previouslyAnalyzedScriptsCombinedDependencies().takeUnless { it.classpath.isEmpty() } return previouslyAnalyzedScriptsCombinedDependencies().takeUnless { it.classpath.isEmpty() }
} }
private fun previouslyAnalyzedScriptsCombinedDependencies(): ScriptDependencies { private fun previouslyAnalyzedScriptsCombinedDependencies() =
val sources = mutableListOf<File>() scriptDependenciesCache.combineDependencies { it.name == KOTLIN_BUILD_FILE_NAME }
val binaries = mutableListOf<File>()
val imports = mutableListOf<String>()
scriptDependenciesCache.inspectCache { cache ->
cache.entries.filter { it.key.endsWith(KOTLIN_BUILD_FILE_NAME) }
.map { it.value }.forEach {
sources += it.sources
binaries += it.classpath
imports += it.imports
}
}
return ScriptDependencies(
classpath = binaries.distinct(),
sources = sources.distinct(),
imports = imports.distinct()
)
}
} }