diff --git a/plugins/kotlin/base/statistics/kotlin.base.statistics.iml b/plugins/kotlin/base/statistics/kotlin.base.statistics.iml index 163b106510de..11a7f02fc59e 100644 --- a/plugins/kotlin/base/statistics/kotlin.base.statistics.iml +++ b/plugins/kotlin/base/statistics/kotlin.base.statistics.iml @@ -1,5 +1,27 @@ + + + + + + + + + + + + + + $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar + + + + + + + @@ -19,5 +41,6 @@ + \ No newline at end of file diff --git a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinJ2KOnboardingFUSCollector.kt b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinJ2KOnboardingFUSCollector.kt index 36c62f30588a..86313621cd45 100644 --- a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinJ2KOnboardingFUSCollector.kt +++ b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinJ2KOnboardingFUSCollector.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.kotlin.idea.statistics import com.intellij.internal.statistic.eventLog.EventLogGroup @@ -29,6 +29,7 @@ import org.jetbrains.kotlin.idea.compiler.configuration.KotlinIdePlugin import org.jetbrains.kotlin.idea.configuration.BuildSystemType import org.jetbrains.kotlin.idea.configuration.buildSystemType import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction import org.jetbrains.kotlin.utils.addToStdlib.safeAs import kotlin.math.absoluteValue import kotlin.random.Random @@ -299,6 +300,7 @@ class KotlinOnboardingJ2KSessionService(private val project: Project, private va /** * Caches if the project contains a Kotlin file. */ + @OptIn(UnsafeCastFunction::class) internal suspend fun hasKotlinFiles(): Boolean { hasKotlinFile?.let { return it } return readAction { diff --git a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinLibraryUsageImportProcessor.kt b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinLibraryUsageImportProcessor.kt index 614f6ff57fc0..3851f582c8dd 100644 --- a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinLibraryUsageImportProcessor.kt +++ b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/KotlinLibraryUsageImportProcessor.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.kotlin.idea.statistics import com.intellij.internal.statistic.libraryUsage.LibraryUsageImportProcessor @@ -8,9 +8,11 @@ import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector +import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction import org.jetbrains.kotlin.utils.addToStdlib.safeAs class KotlinLibraryUsageImportProcessor : LibraryUsageImportProcessor { + @OptIn(UnsafeCastFunction::class) override fun imports(file: PsiFile): List = file.safeAs()?.importDirectives.orEmpty() override fun isSingleElementImport(import: KtImportDirective): Boolean = !import.isAllUnder diff --git a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/compilationError/KotlinCompilationErrorProcessedFilesTimeStampRecorder.kt b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/compilationError/KotlinCompilationErrorProcessedFilesTimeStampRecorder.kt index 06a3b579445a..da422aff83d5 100644 --- a/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/compilationError/KotlinCompilationErrorProcessedFilesTimeStampRecorder.kt +++ b/plugins/kotlin/base/statistics/src/org/jetbrains/kotlin/idea/statistics/compilationError/KotlinCompilationErrorProcessedFilesTimeStampRecorder.kt @@ -1,19 +1,13 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.kotlin.idea.statistics.compilationError import com.intellij.openapi.components.* import com.intellij.openapi.project.Project -import com.intellij.openapi.util.SimpleModificationTracker import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.io.DigestUtil import com.intellij.util.io.hashToHexString -import com.intellij.util.xmlb.annotations.Tag -import com.intellij.util.xmlb.annotations.XMap -import org.jetbrains.annotations.ApiStatus +import kotlinx.serialization.Serializable import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write @Service(Service.Level.PROJECT) @State( @@ -22,75 +16,70 @@ import kotlin.concurrent.write reportStatistic = false, reloadable = false, ) -@ApiStatus.Internal -class KotlinCompilationErrorProcessedFilesTimeStampRecorder : - PersistentStateComponentWithModificationTracker { +internal class KotlinCompilationErrorProcessedFilesTimeStampRecorder : + SerializablePersistentStateComponent(EMPTY_STATE) { - override fun initializeComponent() = lock.write { - if (dropOutdatedTimestamps()) { - tracker.incModificationCount() + override fun initializeComponent() { + val currentTime = System.currentTimeMillis() + if (state.timestamps.values.any { currentTime.isHourPassedSince(it) }) { + updateState { state -> + val timestamps = HashMap(state.timestamps) + dropOutdatedTimestamps(currentTime = currentTime, timestamps = timestamps) + if (timestamps.isEmpty()) EMPTY_STATE else MyState(timestamps = timestamps) + } } } - private val lock = ReentrantReadWriteLock() - private var state = MyState() - private val tracker = SimpleModificationTracker() - override fun getState(): MyState = lock.read { MyState(HashMap(state.timestamps)) } - override fun getStateModificationCount(): Long = tracker.modificationCount - - override fun loadState(state: MyState) = lock.write { - this.state = state - } - + @Serializable class MyState( - @XMap - @JvmField - val timestamps: MutableMap = HashMap() - ) : BaseState() { - data class Key( - // Use md5 instead of the actual file path to make local storage smaller. Users' paths can be huge - @Tag("filePathMd5") - val filePathMd5: String, - @Tag("compilationErrorId") - val compilationErrorId: String - ) - } + @JvmField val timestamps: Map = emptyMap() + ) fun keepOnlyIfHourPassedAndRecordTimestamps(vFile: VirtualFile, compilationErrorIds: List): List { if (compilationErrorIds.isEmpty()) return emptyList() val hash = pathMd5Hash(vFile) - lock.write { + val result = mutableListOf() + updateState { state -> + val timestamps = HashMap(state.timestamps) val currentTime = System.currentTimeMillis() - val result = compilationErrorIds.asSequence() + result.clear() + compilationErrorIds.asSequence() .distinct() /* `isHourPassed` + `recordTimestamp` don't allow the compilation error to happen for the second time in this pipeline, so `distinct` is just a small optimization */ - .filter { isHourPassed(hash, currentTime, it) } - .onEach { recordTimestamp(hash, currentTime, it) } - .toList() - if (dropOutdatedTimestamps(currentTime) || result.isNotEmpty()) { - tracker.incModificationCount() - } - return result + .filter { isHourPassed(md5Hash = hash, currentTime = currentTime, compilationErrorId = it, timestamps = timestamps) } + .onEach { recordTimestamp(md5Hash = hash, currentTime = currentTime, compilationErrorId = it, timestamps = timestamps) } + .toCollection(result) + dropOutdatedTimestamps(currentTime, timestamps) + if (timestamps.isEmpty()) EMPTY_STATE else MyState(timestamps = timestamps) } + return result } - private fun isHourPassed(md5Hash: String, currentTime: Long, compilationErrorId: String): Boolean { - val fileTime = state.timestamps[MyState.Key(md5Hash, compilationErrorId)] ?: return true - return currentTime.isHourPassedSince(fileTime) - } - - private fun recordTimestamp(md5Hash: String, currentTime: Long, compilationErrorId: String) { - state.timestamps[MyState.Key(md5Hash, compilationErrorId)] = currentTime - } - - private fun dropOutdatedTimestamps(currentTime: Long = System.currentTimeMillis()): Boolean = - state.timestamps.values.removeIf { currentTime.isHourPassedSince(it) } - companion object { fun getInstance(project: Project): KotlinCompilationErrorProcessedFilesTimeStampRecorder = project.service() } } +private val EMPTY_STATE = KotlinCompilationErrorProcessedFilesTimeStampRecorder.MyState() + +private fun isHourPassed(md5Hash: String, currentTime: Long, compilationErrorId: String, timestamps: Map): Boolean { + val fileTime = timestamps[createCompositeKey(md5Hash, compilationErrorId)] ?: return true + return currentTime.isHourPassedSince(fileTime) +} + +// Use md5 instead of the actual file path to make local storage smaller. +// Users' paths can be huge. +// Key - combination of filePathMd5 + compilationErrorId, to avoid non-primitive key. +private fun createCompositeKey(md5Hash: String, compilationErrorId: String) = "$md5Hash-$compilationErrorId" + +private fun recordTimestamp(md5Hash: String, currentTime: Long, compilationErrorId: String, timestamps: MutableMap) { + timestamps[createCompositeKey(md5Hash, compilationErrorId)] = currentTime +} + +private fun dropOutdatedTimestamps(currentTime: Long, timestamps: MutableMap): Boolean { + return timestamps.values.removeIf { currentTime.isHourPassedSince(it) } +} + private fun pathMd5Hash(virtualFile: VirtualFile): String = hashToHexString(virtualFile.path, DigestUtil.md5()) private fun Long.isHourPassedSince(lastTime: Long): Boolean = TimeUnit.MILLISECONDS.toHours(this - lastTime) >= 1 \ No newline at end of file