diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCache.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCache.kt new file mode 100644 index 00000000000..36887e6d0e6 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCache.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.fir.low.level.api.fir.caches + +import org.jetbrains.kotlin.fir.caches.FirCache +import java.util.concurrent.ConcurrentHashMap + +internal class FirThreadSafeCache( + private val createValue: (KEY, CONTEXT) -> VALUE +) : FirCache() { + private val map = ConcurrentHashMap() + + override fun getValue(key: KEY, context: CONTEXT): VALUE = + map.computeIfAbsentWithNullableValue(key) { createValue(it, context) } + + override fun getValueIfComputed(key: KEY): VALUE? = + map[key]?.nullValueToNull() +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCacheWithPostCompute.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCacheWithPostCompute.kt new file mode 100644 index 00000000000..57c6e458008 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCacheWithPostCompute.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.fir.low.level.api.fir.caches + +import org.jetbrains.kotlin.fir.caches.FirCache +import java.util.concurrent.ConcurrentHashMap + +internal class FirThreadSafeCacheWithPostCompute( + private val createValue: (KEY, CONTEXT) -> Pair, + private val postCompute: (KEY, VALUE, DATA) -> Unit +) : FirCache() { + private val map = ConcurrentHashMap>() + + @Suppress("UNCHECKED_CAST") + override fun getValue(key: KEY, context: CONTEXT): VALUE = + map.computeIfAbsent(key) { + ValueWithPostCompute( + key, + calculate = { createValue(it, context) }, + postCompute = postCompute + ) + }.getValue() + + override fun getValueIfComputed(key: KEY): VALUE? = + map[key]?.getValueIfComputed() +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCachesFactory.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCachesFactory.kt new file mode 100644 index 00000000000..5d9d95fbfa0 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/FirThreadSafeCachesFactory.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.fir.low.level.api.fir.caches + +import org.jetbrains.kotlin.fir.caches.* + +object FirThreadSafeCachesFactory : FirCachesFactory() { + override fun createCache(createValue: (KEY, CONTEXT) -> VALUE): FirCache = + FirThreadSafeCache(createValue) + + override fun createCacheWithPostCompute( + createValue: (KEY, CONTEXT) -> Pair, + postCompute: (KEY, VALUE, DATA) -> Unit + ): FirCache = + FirThreadSafeCacheWithPostCompute(createValue, postCompute) +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/NullValue.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/NullValue.kt new file mode 100644 index 00000000000..67b4e2863c5 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/NullValue.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.fir.low.level.api.fir.caches + +import java.util.concurrent.ConcurrentMap + +internal object NullValue + +@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") +internal inline fun Any.nullValueToNull(): VALUE = when (this) { + NullValue -> null + else -> this +} as VALUE + +internal inline fun ConcurrentMap.computeIfAbsentWithNullableValue( + key: KEY, + crossinline compute: (KEY) -> Any? +): RESULT { + val value = computeIfAbsent(key) { k -> compute(k) ?: NullValue } + return value.nullValueToNull() +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/ValueWithPostCompute.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/ValueWithPostCompute.kt new file mode 100644 index 00000000000..2b988334853 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/fir/caches/ValueWithPostCompute.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.fir.low.level.api.fir.caches + +/** + * Lazily calculated value which runs postCompute in the same thread, + * assuming that postCompute may try to read that value inside current thread, + * So in the period then value is calculated but post compute was not finished, + * only thread that initiated the calculating may see the value, + * other threads will have to wait wait until that value is calculated + */ +internal class ValueWithPostCompute( + /** + * We need at least one final field to be written in constructor to guarantee safe initialization of our [ValueWithPostCompute] + */ + private val key: KEY, + calculate: (KEY) -> Pair, + postCompute: (KEY, VALUE, DATA) -> Unit, +) { + private var _calculate: ((KEY) -> Pair)? = calculate + private var _postCompute: ((KEY, VALUE, DATA) -> Unit)? = postCompute + + /** + * can be in one of the following three states: + * [ValueIsNotComputed] -- value is not initialized and thread are now executing [_postCompute] + * [ExceptionWasThrownDuringValueComputation] -- exception was thrown during value computation, it will be rethrown on every value access + * [ValueIsPostComputingNow] -- thread with threadId has computed the value and only it can access it during post compute + * some value of type [VALUE] -- value is computed and post compute was executed, values is visible for all threads + * + * Value may be set only under [LazyValueWithPostCompute] intrinsic lock hold + * And may be read from any thread + */ + @Volatile + private var value: Any? = ValueIsNotComputed + + @Suppress("UNCHECKED_CAST") + fun getValue(): VALUE { + when (val stateSnapshot = value) { + is ValueIsPostComputingNow -> { + if (stateSnapshot.threadId == Thread.currentThread().id) { + return stateSnapshot.value as VALUE + } else { + synchronized(this) { // wait until other thread which holds the lock now computes the value + return value as VALUE + } + } + } + ValueIsNotComputed -> synchronized(this) { + // if we entered synchronized section that's mean that the value is not yet calculated and was not started to be calculated + // or the some other thread calculated the value while we were waiting to acquire the lock + + if (value != ValueIsNotComputed) { // some other thread calculated our value + return value as VALUE + } + val calculatedValue = try { + val (calculated, data) = _calculate!!(key) + value = ValueIsPostComputingNow(calculated, Thread.currentThread().id) // only current thread may see the value + _postCompute!!(key, calculated, data) + calculated + } catch (e: Throwable) { + value = ExceptionWasThrownDuringValueComputation(e) + throw e + } + _calculate = null + _postCompute = null + value = calculatedValue + return calculatedValue + } + is ExceptionWasThrownDuringValueComputation -> { + throw stateSnapshot.error + } + else -> { + return value as VALUE + } + } + } + + @Suppress("UNCHECKED_CAST") + fun getValueIfComputed(): VALUE? = when (val snapshot = value) { + ValueIsNotComputed -> null + is ValueIsPostComputingNow -> null + is ExceptionWasThrownDuringValueComputation -> throw snapshot.error + else -> value as VALUE + } + + private class ValueIsPostComputingNow(val value: Any?, val threadId: Long) + private class ExceptionWasThrownDuringValueComputation(val error: Throwable) + private object ValueIsNotComputed +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionFactory.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionFactory.kt index 99ede8b4444..c7ee4898822 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionFactory.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionFactory.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.fir.BuiltinTypes import org.jetbrains.kotlin.fir.PrivateSessionConstructor import org.jetbrains.kotlin.fir.SessionConfiguration import org.jetbrains.kotlin.fir.backend.jvm.FirJvmTypeMapper +import org.jetbrains.kotlin.fir.caches.FirCachesFactory import org.jetbrains.kotlin.fir.checkers.registerCommonCheckers import org.jetbrains.kotlin.fir.dependenciesWithoutSelf import org.jetbrains.kotlin.fir.java.JavaSymbolProvider @@ -33,6 +34,7 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.IdeFirPhaseManager import org.jetbrains.kotlin.idea.fir.low.level.api.IdeSessionComponents import org.jetbrains.kotlin.idea.fir.low.level.api.file.builder.FirFileBuilder import org.jetbrains.kotlin.idea.fir.low.level.api.file.builder.ModuleFileCacheImpl +import org.jetbrains.kotlin.idea.fir.low.level.api.fir.caches.FirThreadSafeCachesFactory import org.jetbrains.kotlin.idea.fir.low.level.api.lazy.resolve.FirLazyDeclarationResolver import org.jetbrains.kotlin.idea.fir.low.level.api.providers.FirModuleWithDependenciesSymbolProvider import org.jetbrains.kotlin.idea.fir.low.level.api.providers.FirIdeProvider @@ -71,9 +73,9 @@ internal object FirIdeSessionFactory { val cache = ModuleFileCacheImpl(this) val firPhaseManager = IdeFirPhaseManager(FirLazyDeclarationResolver(firFileBuilder), cache, sessionInvalidator) + registerIdeComponents() registerCommonComponents(languageVersionSettings) registerResolveComponents() - registerIdeComponents() val provider = FirIdeProvider( project, @@ -156,9 +158,9 @@ internal object FirIdeSessionFactory { val kotlinClassFinder = VirtualFileFinderFactory.getInstance(project).create(searchScope) FirIdeLibrariesSession(moduleInfo, project, searchScope, builtinTypes).apply { + registerIdeComponents() registerCommonComponents(languageVersionSettings) registerJavaSpecificResolveComponents() - registerIdeComponents() val javaSymbolProvider = JavaSymbolProvider(this, project, searchScope) @@ -198,8 +200,8 @@ internal object FirIdeSessionFactory { languageVersionSettings: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT ): FirIdeBuiltinsAndCloneableSession { return FirIdeBuiltinsAndCloneableSession(project, builtinTypes).apply { - registerCommonComponents(languageVersionSettings) registerIdeComponents() + registerCommonComponents(languageVersionSettings) val kotlinScopeProvider = KotlinScopeProvider(::wrapScopeWithJvmMapped) register( @@ -218,5 +220,6 @@ internal object FirIdeSessionFactory { private fun FirIdeSession.registerIdeComponents() { register(IdeSessionComponents::class, IdeSessionComponents.create(this)) + register(FirCachesFactory::class, FirThreadSafeCachesFactory) } }