JBAI-11103 Raw version of user-dependent features implementation

Updated some names, got rid of a redundant UserFactor class.

Some more cleaning & redundancy removing

Updated some names, got rid of a redundant UserFactor class.

Some more cleaning & redundancy removing

Class naming conflicts resolved

Minor logic change in ARFactors

Replaced the custom `Day` and `DayImpl` classes with library `LocalDate` for date representation

AR factor minar change

AR Factors implementation bug fix

fixup! JBAI-11103 changed basic properties to getters and added some documentation


(cherry picked from commit c3951f4df63cc556f5ebe963426e9ee593150761)

IJ-CR-159351

GitOrigin-RevId: 5f4b3a085483981e890e37cf2e9a2f232fed3f4a
This commit is contained in:
Artem Savelev
2025-03-05 15:15:37 +01:00
committed by intellij-monorepo-bot
parent 62095078ba
commit d1bba1490c
18 changed files with 628 additions and 2 deletions

View File

@@ -7,7 +7,7 @@
<option name="MAIN_CLASS_NAME" value="com.intellij.idea.Main" />
<module name="intellij.idea.community.main" />
<shortenClasspath name="ARGS_FILE" />
<option name="VM_PARAMETERS" value="-Xmx2g -XX:ReservedCodeCacheSize=240m -XX:SoftRefLRUPolicyMSPerMB=50 -XX:MaxJavaStackTraceDepth=10000 -ea -Dsun.io.useCanonCaches=false -Dapple.laf.useScreenMenuBar=true -Dsun.awt.disablegrab=true -Didea.jre.check=true -Didea.is.internal=true -Didea.debug.mode=true -Djdk.attach.allowAttachSelf -Dfus.internal.test.mode=true -Dkotlinx.coroutines.debug=off -Djdk.module.illegalAccess.silent=true -Didea.config.path=../config/idea -Didea.system.path=../system/idea -Didea.initially.ask.config=true --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED --add-opens=java.desktop/com.apple.laf=ALL-UNNAMED --add-opens=java.desktop/com.sun.java.swing.plaf.gtk=ALL-UNNAMED --add-opens=java.desktop/java.awt.dnd.peer=ALL-UNNAMED --add-opens=java.desktop/java.awt.event=ALL-UNNAMED --add-opens=java.desktop/java.awt.image=ALL-UNNAMED --add-opens=java.desktop/java.awt.peer=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED --add-opens=java.desktop/javax.swing.plaf.basic=ALL-UNNAMED --add-opens=java.desktop/javax.swing.text=ALL-UNNAMED --add-opens=java.desktop/javax.swing.text.html=ALL-UNNAMED --add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED --add-opens=java.desktop/sun.awt.datatransfer=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.desktop/sun.awt.windows=ALL-UNNAMED --add-opens=java.desktop/sun.awt=ALL-UNNAMED --add-opens=java.desktop/sun.font=ALL-UNNAMED --add-opens=java.desktop/sun.java2d=ALL-UNNAMED --add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED --add-opens=java.desktop/sun.lwawt=ALL-UNNAMED --add-opens=java.desktop/sun.swing=ALL-UNNAMED --add-opens=jdk.attach/sun.tools.attach=ALL-UNNAMED --add-opens=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED --add-opens=jdk.jdi/com.sun.tools.jdi=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -Didea.platform.prefix=Idea -Djava.system.class.loader=com.intellij.util.lang.PathClassLoader" />
<option name="VM_PARAMETERS" value="-Xmx2g -XX:ReservedCodeCacheSize=240m -XX:SoftRefLRUPolicyMSPerMB=50 -XX:MaxJavaStackTraceDepth=10000 -ea -Dsun.io.useCanonCaches=false -Dapple.laf.useScreenMenuBar=true -Dsun.awt.disablegrab=true -Didea.jre.check=true -Didea.is.internal=true -Didea.debug.mode=true -Djdk.attach.allowAttachSelf -Dfus.internal.test.mode=true -Dkotlinx.coroutines.debug=off -Djdk.module.illegalAccess.silent=true -Didea.config.path=../config/idea -Didea.system.path=../system/idea -Didea.initially.ask.config=true --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED --add-opens=java.desktop/com.apple.laf=ALL-UNNAMED --add-opens=java.desktop/com.sun.java.swing.plaf.gtk=ALL-UNNAMED --add-opens=java.desktop/java.awt.dnd.peer=ALL-UNNAMED --add-opens=java.desktop/java.awt.event=ALL-UNNAMED --add-opens=java.desktop/java.awt.image=ALL-UNNAMED --add-opens=java.desktop/java.awt.peer=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED --add-opens=java.desktop/javax.swing.plaf.basic=ALL-UNNAMED --add-opens=java.desktop/javax.swing.text=ALL-UNNAMED --add-opens=java.desktop/javax.swing.text.html=ALL-UNNAMED --add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED --add-opens=java.desktop/sun.awt.datatransfer=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.desktop/sun.awt.windows=ALL-UNNAMED --add-opens=java.desktop/sun.awt=ALL-UNNAMED --add-opens=java.desktop/sun.font=ALL-UNNAMED --add-opens=java.desktop/sun.java2d=ALL-UNNAMED --add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED --add-opens=java.desktop/sun.lwawt=ALL-UNNAMED --add-opens=java.desktop/sun.swing=ALL-UNNAMED --add-opens=jdk.attach/sun.tools.attach=ALL-UNNAMED --add-opens=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED --add-opens=jdk.jdi/com.sun.tools.jdi=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -Didea.platform.prefix=Idea -Djava.system.class.loader=com.intellij.util.lang.PathClassLoader -Dfus.internal.test.mode=true" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/bin" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="60786" />

View File

@@ -40,5 +40,6 @@
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="fleet.util.core" />
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
</component>
</module>

View File

@@ -7,6 +7,7 @@ import com.intellij.codeInsight.inline.completion.listeners.typing.InlineComplet
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionLogsListener
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionUsageTracker
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionUsageTracker.ShownEvents.FinishType
import com.intellij.codeInsight.inline.completion.logs.UserFactorsListener
import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext
import com.intellij.codeInsight.inline.completion.session.InlineCompletionInvalidationListener
import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession
@@ -80,6 +81,8 @@ abstract class InlineCompletionHandler @ApiStatus.Internal constructor(
val logsListener = InlineCompletionLogsListener(editor)
addEventListener(logsListener)
invalidationListeners.addListener(logsListener)
val userFactorsListener = UserFactorsListener()
addEventListener(userFactorsListener)
}
/**

View File

@@ -4,6 +4,11 @@ package com.intellij.codeInsight.inline.completion.logs
import com.intellij.codeInsight.inline.completion.InlineCompletionRequest
import com.intellij.codeInsight.inline.completion.features.InlineCompletionFeaturesCollector
import com.intellij.codeInsight.inline.completion.features.InlineCompletionFeaturesScopeAnalyzer.ScopeType
import com.intellij.codeInsight.inline.completion.logs.statistics.DECAY_DURATIONS
import com.intellij.codeInsight.inline.completion.logs.statistics.UserFactorDescriptions
import kotlin.time.Duration
import com.intellij.codeInsight.inline.completion.logs.statistics.UserFactorStorage
import com.intellij.codeInsight.inline.completion.logs.statistics.timeSince
import com.intellij.internal.statistic.eventLog.events.EventField
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.EventPair
@@ -24,7 +29,34 @@ internal object InlineCompletionContextLogs {
val featureCollectorBased = InlineCompletionFeaturesCollector.get(request.file.language)?.let {
captureFeatureCollectorBased(request.file, request.startOffset, it, element)
}
return simple + typingFeatures + featureCollectorBased.orEmpty()
val userFeatures = captureUserStatisticsFactors()
return simple + typingFeatures + featureCollectorBased.orEmpty() + userFeatures
}
private fun captureUserStatisticsFactors(): List<EventPair<*>> = buildList {
val storage = UserFactorStorage.getInstance()
val accRateFactorsReader = storage.getFactorReader(UserFactorDescriptions.ACCEPTANCE_RATE_FACTORS)
// Add decaying features
for (duration in DECAY_DURATIONS) {
val (selectionField, showupField, acceptanceField) = Logs.DECAYING_FEATURES[duration] ?: continue
add(selectionField with accRateFactorsReader.selectionCountDecayedBy(duration))
add(showupField with accRateFactorsReader.showUpCountDecayedBy(duration))
add(acceptanceField with accRateFactorsReader.smoothedAcceptanceRate(duration))
}
add(Logs.PREV_SELECTED with (accRateFactorsReader.prevSelected()?.let { it != 0.0 } ?: false))
add(Logs.TIME_SINCE_LAST_SHOWUP with (accRateFactorsReader.lastShowUpTimeToday()?.let(::timeSince) ?: 0))
add(Logs.TIME_SINCE_LAST_SELECTION with (accRateFactorsReader.lastSelectionTimeToday()?.let(::timeSince) ?: 0))
val finishRatiosReader = storage.getFactorReader(UserFactorDescriptions.COMPLETION_FINISH_TYPE)
if(finishRatiosReader.getTotalCount() > 0) {
val total = finishRatiosReader.getTotalCount()
add(Logs.SELECTED_RATIO with (finishRatiosReader.getCountByKey("selected")) / total)
add(Logs.INVALIDATED_RATIO with (finishRatiosReader.getCountByKey("invalidated")) / total)
add(Logs.EXPLICIT_CANCEL_RATIO with (finishRatiosReader.getCountByKey("explicitCancel")) / total)
}
val prefixLengthReader = storage.getFactorReader(UserFactorDescriptions.PREFIX_LENGTH_ON_COMPLETION)
add(Logs.MOST_FREQUENT_PREFIX_LENGTH with ((prefixLengthReader.getCountsByPrefixLength().maxByOrNull { it.value }?.key) ?: 0))
add(Logs.AVERAGE_PREFIX_LENGTH with ((prefixLengthReader.getAveragePrefixLength()) ?: 0.0))
}
private fun captureSimple(psiFile: PsiFile, editor: Editor, offset: Int, element: PsiElement?): List<EventPair<*>> {
@@ -230,6 +262,7 @@ internal object InlineCompletionContextLogs {
}
private object Logs : PhasedLogs(InlineCompletionLogsContainer.Phase.INLINE_API_STARTING) {
private fun Duration.toDescription(): String = toString().replace(" ", "")
val ELEMENT_PREFIX_LENGTH = register(EventFields.Int("element_prefix_length"))
val LINE_NUMBER = register(EventFields.Int("line_number"))
val COLUMN_NUMBER = register(EventFields.Int("column_number"))
@@ -289,6 +322,28 @@ internal object InlineCompletionContextLogs {
register(it)
}
val PREV_SELECTED = register(EventFields.Boolean("prev_selected"))
val TIME_SINCE_LAST_SELECTION = register(EventFields.Long("time_since_last_selection", "Duration from previous selected event."))
val TIME_SINCE_LAST_SHOWUP = register(EventFields.Long("time_since_last_showup", "Duration from previous showup event."))
val SELECTED_RATIO = register(EventFields.Double("selected_ratio"))
val INVALIDATED_RATIO = register(EventFields.Double("invalidated_ratio"))
val EXPLICIT_CANCEL_RATIO = register(EventFields.Double("explicit_cancel_ratio"))
val DECAYING_FEATURES: Map<Duration, List<EventField<Double>>> = DECAY_DURATIONS.associateWith { duration ->
listOf(
register(EventFields.Double("selection_decayed_by_${duration.toDescription()}",
"Selection count with exponential decay over ${duration.toDescription()}")),
register(EventFields.Double("showup_decayed_by_${duration.toDescription()}",
"Show up count with exponential decay over ${duration.toDescription()}")),
register(EventFields.Double("acceptance_rate_smoothed_by_${duration.toDescription()}",
"Acceptance rate smoothed over ${duration.toDescription()}"))
)
}
val AVERAGE_PREFIX_LENGTH = register(EventFields.Double("average_prefix_length"))
val MOST_FREQUENT_PREFIX_LENGTH = register(EventFields.Int("most_frequent_prefix_length"))
private fun <T> scopeFeatures(createFeatureDeclaration: (String) -> EventField<T>): List<EventField<T>> {
return listOf(
register(createFeatureDeclaration("caret")),

View File

@@ -0,0 +1,82 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs
import com.intellij.codeInsight.inline.completion.InlineCompletionEventType
import com.intellij.codeInsight.inline.completion.InlineCompletionEventAdapter
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionUsageTracker.ShownEvents.FinishType
import com.intellij.codeInsight.inline.completion.logs.statistics.UserFactorDescriptions
import com.intellij.codeInsight.inline.completion.logs.statistics.UserFactorStorage
private val EXPLICIT_CANCEL_TYPES = setOf(
FinishType.MOUSE_PRESSED,
FinishType.CARET_CHANGED,
FinishType.ESCAPE_PRESSED
)
private val SELECTED_TYPE = FinishType.SELECTED
private val INVALIDATED_TYPE = FinishType.INVALIDATED
internal class UserFactorsListener() : InlineCompletionEventAdapter {
/**
* This field is not thread-safe, please access it only on EDT.
*/
private var holder = Holder()
/**
* Fields inside [Holder] are not thread-safe, please access them only on EDT.
*/
private class Holder() {
var wasShown: Boolean = false
var prefixLength: Int = 0
}
override fun onRequest(event: InlineCompletionEventType.Request) {
holder = Holder()
val element = if (event.request.startOffset == 0) null else event.request.file.findElementAt(event.request.startOffset - 1)
holder.prefixLength = if (element != null) (event.request.startOffset - element.textOffset) else 0
}
override fun onShow(event: InlineCompletionEventType.Show) {
holder.wasShown = true
}
override fun onHide(event: InlineCompletionEventType.Hide) {
if(holder.wasShown && event.finishType != SELECTED_TYPE) {
UserFactorStorage.apply( UserFactorDescriptions.ACCEPTANCE_RATE_FACTORS) {
it.fireLookupElementShowUp()
}
}
if(holder.wasShown) {
when (event.finishType) {
in EXPLICIT_CANCEL_TYPES -> {
UserFactorStorage.apply( UserFactorDescriptions.COMPLETION_FINISH_TYPE) {
it.fireExplicitCancel()
}
}
SELECTED_TYPE -> {
UserFactorStorage.apply( UserFactorDescriptions.ACCEPTANCE_RATE_FACTORS) {
it.fireLookupElementSelected()
}
UserFactorStorage.apply( UserFactorDescriptions.COMPLETION_FINISH_TYPE) {
it.fireSelected()
}
UserFactorStorage.apply(UserFactorDescriptions.PREFIX_LENGTH_ON_COMPLETION) {
it.fireCompletionPerformed(holder.prefixLength)
}
}
INVALIDATED_TYPE -> {
UserFactorStorage.apply( UserFactorDescriptions.COMPLETION_FINISH_TYPE) {
it.fireInvalidated()
}
}
else -> {
UserFactorStorage.apply( UserFactorDescriptions.COMPLETION_FINISH_TYPE) {
it.fireOther()
}
}
}
}
}
}

View File

@@ -0,0 +1,116 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import java.time.Instant
import java.time.LocalDate
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.DurationUnit
private const val PREV_SELECTED = "prev_selected"
private const val SELECTION = "selection"
private const val SHOW_UP = "show_up"
private const val GLOBAL_ACCEPTANCE_RATE = 0.3
private const val GLOBAL_ALPHA = 10
val DECAY_DURATIONS: List<Duration> = listOf(1.hours, 1.days, 7.days)
fun lastTimeName(name: String): String = "last_${name}_time"
fun decayingCountName(name: String, decayDuration: Duration): String = "${name}_count_decayed_by_$decayDuration"
class AccRateFactorsReader(factor: DailyAggregatedDoubleFactor) : UserFactorReaderBase(factor) {
fun lastSelectionTimeToday(): Double? = getTodayFactor(lastTimeName(SELECTION))
fun lastShowUpTimeToday(): Double? = getTodayFactor(lastTimeName(SHOW_UP))
fun prevSelected(): Double? = getTodayFactor(PREV_SELECTED)
private fun getTodayFactor(name: String) = factor.onDate(LocalDate.now())?.get(name)
fun smoothedAcceptanceRate(decayDuration: Duration): Double {
val timestamp = currentTimestamp()
return globallySmoothedRatio(selectionCountDecayedBy(decayDuration, timestamp), showUpCountDecayedBy(decayDuration, timestamp))
}
fun selectionCountDecayedBy(decayDuration: Duration, timestamp: Long = currentTimestamp()): Double =
factor.aggregateDecayingCount(SELECTION, decayDuration, timestamp)
fun showUpCountDecayedBy(decayDuration: Duration, timestamp: Long = currentTimestamp()): Double =
factor.aggregateDecayingCount(SHOW_UP, decayDuration, timestamp)
}
//private fun currentEpochSeconds() = Instant.now().epochSecond.toDouble()
private fun currentTimestamp() = Instant.now().toEpochMilli()
class AccRateFactorsUpdater(factor: MutableDoubleFactor) : UserFactorUpdaterBase(factor) {
fun fireLookupElementSelected() {
val timestamp = currentTimestamp()
for (duration in DECAY_DURATIONS) {
factor.increment(SELECTION, duration, timestamp)
factor.increment(SHOW_UP, duration, timestamp)
}
factor.updateOnDate(LocalDate.now()) {
this[lastTimeName(SELECTION)] = timestamp.toDouble()
this[lastTimeName(SHOW_UP)] = timestamp.toDouble()
}
factor.prevSelected(true)
}
fun fireLookupElementShowUp() {
val timestamp = currentTimestamp()
for (duration in DECAY_DURATIONS) {
factor.increment(SHOW_UP, duration, timestamp)
}
factor.updateOnDate(LocalDate.now()) {
this[lastTimeName(SHOW_UP)] = timestamp.toDouble()
}
factor.prevSelected(false)
}
}
fun DailyAggregatedDoubleFactor.aggregateDecayingCount(name: String, decayDuration: Duration, timestamp: Long): Double {
var result = 0.0
for (day in availableDays()) {
result += get(name, decayDuration, day, timestamp) ?: 0.0
}
return result
}
fun DailyAggregatedDoubleFactor.get(name: String, decayDuration: Duration, day: LocalDate, timestamp: Long): Double? {
val onDate = onDate(day) ?: return null
val lastTimeName = lastTimeName(name)
val decayingCountName = decayingCountName(name, decayDuration)
val lastTime = onDate[lastTimeName] ?: return null
val decayingCount = onDate[decayingCountName]
return decayingCount.decay(timestamp - lastTime, decayDuration)
}
fun MutableDoubleFactor.increment(name: String, decayDuration: Duration, timestamp: Long) {
updateOnDate(LocalDate.now()) {
val lastTimeName = lastTimeName(name)
val decayingCountName = decayingCountName(name, decayDuration)
this[decayingCountName] = this[lastTimeName]?.let { this[decayingCountName].decay(timestamp - it, decayDuration) + 1 } ?: 1.0
//this[lastTimeName] = timestamp.toDouble()
}
}
private fun MutableDoubleFactor.prevSelected(boolean: Boolean) {
updateOnDate(LocalDate.now()) {
this[PREV_SELECTED] = if (boolean) 1.0 else 0.0
}
}
private fun Double?.decay(duration: Double, decayDuration: Duration) =
if (this == null) 0.0
else if (duration * this == 0.0) this
else 0.5.pow(duration / decayDuration.toDouble(DurationUnit.MILLISECONDS)) * this
private fun globallySmoothedRatio(quotient: Double?, divisor: Double?) =
if (divisor == null) GLOBAL_ACCEPTANCE_RATE
else ((quotient ?: 0.0) + GLOBAL_ACCEPTANCE_RATE * GLOBAL_ALPHA) / (divisor + GLOBAL_ALPHA)
//private fun timeSince(epochSeconds: Double) = (Instant.now().epochSecond - epochSeconds.toLong()).toString()
internal fun timeSince(epochSeconds: Double) = Instant.now().toEpochMilli() - epochSeconds.toLong()

View File

@@ -0,0 +1,13 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
@Service
@State(name = "ApplicationInlineFactors", storages = [(Storage(value = "inline.factors.xml", roamingType = RoamingType.DISABLED))], reportStatistic = false)
class ApplicationInlineFactorStorage : UserFactorStorageBase()

View File

@@ -0,0 +1,22 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
private const val explicitCancelKey = "explicitCancel"
private const val selectedKey = "selected"
private const val invalidatedKey = "invalidated"
private const val otherKey = "other"
class CompletionFinishTypeReader(private val factor: DailyAggregatedDoubleFactor) : FactorReader {
fun getCountByKey(key: String): Double = factor.aggregateSum()[key] ?: 0.0
fun getTotalCount(): Double =
getCountByKey(explicitCancelKey) + getCountByKey(selectedKey) + getCountByKey(invalidatedKey) + getCountByKey(otherKey)
}
class CompletionFinishTypeUpdater(private val factor: MutableDoubleFactor) : FactorUpdater {
fun fireExplicitCancel(): Boolean = factor.incrementOnToday(explicitCancelKey)
fun fireSelected(): Boolean = factor.incrementOnToday(selectedKey)
fun fireInvalidated(): Boolean = factor.incrementOnToday(invalidatedKey)
fun fireOther(): Boolean = factor.incrementOnToday(otherKey)
}

View File

@@ -0,0 +1,33 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import java.time.LocalDate
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.iterator
interface DailyAggregatedDoubleFactor {
fun availableDays(): List<LocalDate>
fun onDate(date: LocalDate): Map<String, Double>?
}
interface MutableDoubleFactor : DailyAggregatedDoubleFactor {
fun incrementOnToday(key: String): Boolean
fun updateOnDate(date: LocalDate, updater: MutableMap<String, Double>.() -> Unit): Boolean
}
private fun DailyAggregatedDoubleFactor.aggregateBy(reduce: (Double, Double) -> Double): Map<String, Double> {
val result = mutableMapOf<String, Double>()
for (onDate in availableDays().mapNotNull(this::onDate)) {
for ((key, value) in onDate) {
result.compute(key) { _, old -> if (old == null) value else reduce(old, value) }
}
}
return result
}
fun DailyAggregatedDoubleFactor.aggregateSum(): Map<String, Double> = aggregateBy { d1, d2 -> d1 + d2 }

View File

@@ -0,0 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
interface FactorReader

View File

@@ -0,0 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
interface FactorUpdater

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
class PrefixLengthReader(factor: DailyAggregatedDoubleFactor) : UserFactorReaderBase(factor) {
fun getCountsByPrefixLength(): Map<Int, Double> {
return factor.aggregateSum().asIterable().associate { (key, value) -> key.toInt() to value }
}
fun getAveragePrefixLength(): Double? {
val lengthToCount = getCountsByPrefixLength()
if (lengthToCount.isEmpty()) return null
val totalChars = lengthToCount.asSequence().sumOf { it.key * it.value }
val completionCount = lengthToCount.asSequence().sumOf { it.value }
if (completionCount == 0.0) return null
return totalChars / completionCount
}
}
class PrefixLengthUpdater(factor: MutableDoubleFactor) : UserFactorUpdaterBase(factor) {
fun fireCompletionPerformed(prefixLength: Int) {
factor.incrementOnToday(prefixLength.toString())
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.StoragePathMacros
/**
* @author Vitaliy.Bibaev
*/
@Service(Service.Level.PROJECT)
@State(name = "ProjectInlineFactors", storages = [Storage(StoragePathMacros.CACHE_FILE)], reportStatistic = false)
class ProjectUserFactorStorage : UserFactorStorageBase()

View File

@@ -0,0 +1,6 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
abstract class UserFactorReaderBase(protected val factor: DailyAggregatedDoubleFactor) : FactorReader
abstract class UserFactorUpdaterBase(protected val factor: MutableDoubleFactor) : FactorUpdater

View File

@@ -0,0 +1,10 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
interface UserFactorDescription<out U : FactorUpdater, out R : FactorReader> {
val factorId: String
val updaterFactory: (MutableDoubleFactor) -> U
val readerFactory: (DailyAggregatedDoubleFactor) -> R
}

View File

@@ -0,0 +1,35 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
object UserFactorDescriptions {
private val IDS: MutableSet<String> = mutableSetOf()
val COMPLETION_FINISH_TYPE: UserFactorDescription<CompletionFinishTypeUpdater, CompletionFinishTypeReader> =
Descriptor.register("completionFinishedType", ::CompletionFinishTypeUpdater, ::CompletionFinishTypeReader)
val PREFIX_LENGTH_ON_COMPLETION: UserFactorDescription<PrefixLengthUpdater, PrefixLengthReader> =
Descriptor.register("prefixLength", ::PrefixLengthUpdater, ::PrefixLengthReader)
val ACCEPTANCE_RATE_FACTORS: UserFactorDescription<AccRateFactorsUpdater, AccRateFactorsReader> =
Descriptor.register("acceptanceRateFactors", ::AccRateFactorsUpdater, ::AccRateFactorsReader)
//val TIME_BETWEEN_TYPING: UserFactorDescription<TimeBetweenTypingUpdater, TimeBetweenTypingReader> =
// Descriptor.register("timeBetweenTyping", ::TimeBetweenTypingUpdater, ::TimeBetweenTypingReader)
fun isKnownFactor(id: String): Boolean = id in IDS
private class Descriptor<out U : FactorUpdater, out R : FactorReader> private constructor(
override val factorId: String,
override val updaterFactory: (MutableDoubleFactor) -> U,
override val readerFactory: (DailyAggregatedDoubleFactor) -> R,
) : UserFactorDescription<U, R> {
companion object {
fun <U : FactorUpdater, R : FactorReader> register(
factorId: String,
updaterFactory: (MutableDoubleFactor) -> U,
readerFactory: (DailyAggregatedDoubleFactor) -> R,
): UserFactorDescription<U, R> {
assert(!isKnownFactor(factorId)) { "Descriptor with id '$factorId' already exists" }
IDS.add(factorId)
return Descriptor(factorId, updaterFactory, readerFactory)
}
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
/**
* @author Vitaliy.Bibaev
*/
interface UserFactorStorage {
companion object {
fun getInstance(): UserFactorStorage = service<ApplicationInlineFactorStorage>()
fun getInstance(project: Project): UserFactorStorage = project.service<ProjectUserFactorStorage>()
fun <U : FactorUpdater> applyOnBoth(project: Project, description: UserFactorDescription<U, *>, updater: (U) -> Unit) {
updater(getInstance().getFactorUpdater(description))
updater(getInstance(project).getFactorUpdater(description))
}
fun <U : FactorUpdater> apply(description: UserFactorDescription<U, *>, updater: (U) -> Unit) {
updater(getInstance().getFactorUpdater(description))
}
}
fun <U : FactorUpdater> getFactorUpdater(description: UserFactorDescription<U, *>): U
fun <R : FactorReader> getFactorReader(description: UserFactorDescription<*, R>): R
}

View File

@@ -0,0 +1,176 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.completion.logs.statistics
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.diagnostic.logger
import java.text.DecimalFormat
import org.jdom.Element
import java.text.ParseException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
abstract class UserFactorStorageBase : UserFactorStorage, PersistentStateComponent<Element> {
private companion object {
val DOUBLE_VALUE_FORMATTER = DecimalFormat().apply {
maximumFractionDigits = 12
minimumFractionDigits = 1
isGroupingUsed = false
}
val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
}
private val state = CollectorState()
override fun <U : FactorUpdater> getFactorUpdater(description: UserFactorDescription<U, *>): U =
description.updaterFactory.invoke(getAggregateFactor(description.factorId))
override fun <R : FactorReader> getFactorReader(description: UserFactorDescription<*, R>): R =
description.readerFactory.invoke(getAggregateFactor(description.factorId))
override fun getState(): Element {
val element = Element("component")
state.writeState(element)
return element
}
override fun loadState(newState: Element) {
state.applyState(newState)
}
private fun getAggregateFactor(factorId: String): MutableDoubleFactor =
state.aggregateFactors.computeIfAbsent(factorId) { DailyAggregateFactor() }
private class CollectorState {
val aggregateFactors: MutableMap<String, DailyAggregateFactor> = HashMap()
fun applyState(element: Element) {
aggregateFactors.clear()
for (child in element.children) {
val factorId = child.getAttributeValue("id")
if (child.name == "factor" && factorId != null && UserFactorDescriptions.isKnownFactor(factorId)) {
val factor = DailyAggregateFactor.restore(child)
if (factor != null) aggregateFactors[factorId] = factor
}
}
}
fun writeState(element: Element) {
for ((id, factor) in aggregateFactors.asSequence().sortedBy { it.key }) {
val factorElement = Element("factor")
factorElement.setAttribute("id", id)
factor.writeState(factorElement)
element.addContent(factorElement)
}
}
}
class DailyAggregateFactor private constructor(private val aggregates: SortedMap<LocalDate, DailyData> = sortedMapOf())
: MutableDoubleFactor {
constructor() : this(sortedMapOf())
init {
ensureLimit()
}
companion object {
private const val DAYS_LIMIT = 10
fun restore(element: Element): DailyAggregateFactor? {
val data = sortedMapOf<LocalDate, DailyData>()
for (child in element.children) {
val date = child.getAttributeValue("date")
val day = LocalDate.parse(date, DATE_FORMATTER)
if (child.name == "dailyData" && day != null) {
val dailyData = DailyData.restore(child)
if (dailyData != null) data.put(day, dailyData)
}
}
if (data.isEmpty()) return null
return DailyAggregateFactor(data)
}
}
fun writeState(element: Element) {
for ((day, data) in aggregates) {
val dailyDataElement = Element("dailyData")
dailyDataElement.setAttribute("date", day.format(DATE_FORMATTER))
data.writeState(dailyDataElement)
element.addContent(dailyDataElement)
}
}
override fun availableDays(): List<LocalDate> = aggregates.keys.toList()
override fun incrementOnToday(key: String): Boolean {
return updateOnDate(LocalDate.now()) {
compute(key) { _, oldValue -> if (oldValue == null) 1.0 else oldValue + 1.0 }
}
}
override fun onDate(date: LocalDate): Map<String, Double>? = aggregates[date]?.data
override fun updateOnDate(date: LocalDate, updater: MutableMap<String, Double>.() -> Unit): Boolean {
val old = aggregates[date]
if (old != null) {
updater.invoke(old.data)
return true
}
if (aggregates.size < DAYS_LIMIT || aggregates.firstKey() < date) {
val data = DailyData()
updater.invoke(data.data)
aggregates.put(date, data)
ensureLimit()
return true
}
return false
}
private fun ensureLimit() {
while (aggregates.size > DAYS_LIMIT) {
aggregates.remove(aggregates.firstKey())
}
}
}
private class DailyData(val data: MutableMap<String, Double> = HashMap()) {
companion object {
private val LOG = logger<DailyData>()
fun restore(element: Element): DailyData? {
val data = mutableMapOf<String, Double>()
for (child in element.children) {
if (child.name == "observation") {
val dataKey = child.getAttributeValue("name")
val dataValue = child.getAttributeValue("value")
// skip all if any observation is inconsistent
val value = try {
DOUBLE_VALUE_FORMATTER.parse(dataValue).toDouble()
}
catch (e: ParseException) {
LOG.error(e)
return null
}
data[dataKey] = value
}
}
if (data.isEmpty()) return null
return DailyData(data)
}
}
fun writeState(element: Element) {
for ((key, value) in data.asSequence().sortedBy { it.key }) {
val observation = Element("observation")
observation.setAttribute("name", key)
observation.setAttribute("value", DOUBLE_VALUE_FORMATTER.format(value))
element.addContent(observation)
}
}
}
}