LLM-18251: introduce adaptive debounce for cloud completion

(cherry picked from commit 730bbb63918e8fd8b82ef2cb8720f6a8a2007006)


(cherry picked from commit d71601c2661630be1024b5f9b9451e7dc678d34b)

IJ-MR-175246

GitOrigin-RevId: 95dc7c63e9de04256335f48cfb8a26f87cec5c0d
This commit is contained in:
Roman Chertishchev
2025-09-07 16:21:27 +02:00
committed by intellij-monorepo-bot
parent 9966f71675
commit dde4de27ea
3 changed files with 96 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 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
import com.intellij.codeInsight.inline.completion.debounce.InlineCompletionAdaptiveDebounceListener
import com.intellij.codeInsight.inline.completion.editor.InlineCompletionEditorType
import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement
import com.intellij.codeInsight.inline.completion.listeners.InlineSessionWiseCaretListener
@@ -84,6 +85,9 @@ abstract class InlineCompletionHandler @ApiStatus.Internal constructor(
invalidationListeners.addListener(logsListener)
addEventListener(UserFactorsListener())
// Adaptive debounce listener: tracks acceptance/rejection outcomes for the last 10 minutes
addEventListener(InlineCompletionAdaptiveDebounceListener(editor))
Disposer.register(parentDisposable, /* child = */ executor)
}

View File

@@ -0,0 +1,41 @@
// 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.debounce
import com.intellij.codeInsight.inline.completion.InlineCompletionEventAdapter
import com.intellij.codeInsight.inline.completion.InlineCompletionEventType
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionUsageTracker.ShownEvents.FinishType
import com.intellij.openapi.editor.Editor
/**
* Listener that observes inline completion lifecycle and records
* accept/reject finish type into [InlineCompletionFinishedCompletionsStorage].
*/
internal class InlineCompletionAdaptiveDebounceListener(private val editor: Editor) : InlineCompletionEventAdapter {
private fun serviceOrNull(): InlineCompletionFinishedCompletionsStorage? = editor.project?.let { InlineCompletionFinishedCompletionsStorage.getInstance(it) }
override fun onAfterInsert(event: InlineCompletionEventType.AfterInsert) {
serviceOrNull()?.record(InlineCompletionFinishedCompletionsStorage.Result.ACCEPTED)
}
override fun onHide(event: InlineCompletionEventType.Hide) {
val result = when (event.finishType) {
FinishType.CARET_CHANGED,
FinishType.EDITOR_REMOVED,
FinishType.FOCUS_LOST,
FinishType.TYPED,
FinishType.EMPTY,
FinishType.ERROR,
FinishType.OTHER,
FinishType.BACKSPACE_PRESSED,
FinishType.KEY_PRESSED,
FinishType.INVALIDATED,
FinishType.SELECTED,
FinishType.MOUSE_PRESSED,
FinishType.DOCUMENT_CHANGED,
-> InlineCompletionFinishedCompletionsStorage.Result.OTHER
FinishType.ESCAPE_PRESSED -> InlineCompletionFinishedCompletionsStorage.Result.REJECTED
}
serviceOrNull()?.record(result)
}
}

View File

@@ -0,0 +1,51 @@
// 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.debounce
import com.intellij.codeInsight.inline.completion.debounce.InlineCompletionFinishedCompletionsStorage.Companion.EXPIRATION_MS
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
import java.util.*
/**
* Project-level container of recent inline-completions for adaptive debounce tuning.
*
* Retains completions for the last [EXPIRATION_MS] milliseconds (10 minutes) and discards older ones.
*/
@Service(Service.Level.PROJECT)
@ApiStatus.Internal
class InlineCompletionFinishedCompletionsStorage {
enum class Result { ACCEPTED, REJECTED, OTHER }
data class FinishedCompletion(val result: Result, val finishTime: Long)
private val previousCompletions = ArrayDeque<FinishedCompletion>()
@Synchronized
fun record(result: Result) {
val now = System.currentTimeMillis()
cleanupExpired(now)
previousCompletions.addLast(FinishedCompletion(result, now))
}
/** Returns a snapshot of recent actions with timestamps (most recent at the end). */
@Synchronized
fun getRecentCompletions(): List<FinishedCompletion> {
cleanupExpired(System.currentTimeMillis())
return ArrayList(previousCompletions)
}
private fun cleanupExpired(now: Long) {
val threshold = now - EXPIRATION_MS
while (true) {
val first = previousCompletions.peekFirst() ?: break
if (first.finishTime < threshold) previousCompletions.removeFirst() else break
}
}
companion object {
private const val EXPIRATION_MS: Long = 10 * 60 * 1000 // 10 minutes
fun getInstance(project: Project): InlineCompletionFinishedCompletionsStorage = project.service()
}
}