Skip to content

Commit c4a00d5

Browse files
authored
CodeWhisperer percentage code completion implementation (#3213)
1 parent f9632b1 commit c4a00d5

15 files changed

+736
-44
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ kotlin = "1.6.21"
1818
kotlinCoroutines = "1.5.0"
1919
mockito = "4.6.1"
2020
mockitoKotlin = "4.0.0"
21-
telemetryGenerator = "1.0.53"
21+
telemetryGenerator = "1.0.58"
2222
testLogger = "3.1.0"
2323
testRetry = "1.2.1"
2424
slf4j = "1.7.36"

jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ with what features/services are supported.
236236
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow" />
237237

238238
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager" />
239-
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTracker"/>
239+
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker"/>
240240
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager"/>
241241
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererLanguageManager"/>
242242
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus"/>

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import com.intellij.openapi.editor.event.DocumentListener
88
import com.intellij.openapi.editor.event.EditorFactoryEvent
99
import com.intellij.openapi.editor.event.EditorFactoryListener
1010
import com.intellij.openapi.editor.impl.EditorImpl
11+
import com.intellij.psi.PsiDocumentManager
12+
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.toProgrammingLanguage
13+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.ProgrammingLanguage
1114
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
15+
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker
16+
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.toCodeWhispererLanguage
1217

1318
class CodeWhispererEditorListener : EditorFactoryListener {
1419
override fun editorCreated(event: EditorFactoryEvent) {
@@ -18,6 +23,12 @@ class CodeWhispererEditorListener : EditorFactoryListener {
1823
object : DocumentListener {
1924
override fun documentChanged(event: DocumentEvent) {
2025
CodeWhispererInvocationStatus.getInstance().documentChanged()
26+
editor.project?.let { project ->
27+
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)?.toProgrammingLanguage() ?. let { languageName ->
28+
val language = ProgrammingLanguage(languageName).toCodeWhispererLanguage()
29+
CodeWhispererCodeCoverageTracker.getInstance(language).documentChanged(event)
30+
}
31+
}
2132
}
2233
},
2334
editor.disposable

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class CodeWhispererEditorManager {
5858
)
5959
ApplicationManager.getApplication().messageBus.syncPublisher(
6060
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
61-
).afterAccept(states, sessionContext)
61+
).afterAccept(states, sessionContext, rangeMarker)
6262
}
6363
}
6464

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object CodeWhispererEditorUtil {
3333
val programmingLanguage = ProgrammingLanguage(psiFile.toProgrammingLanguage())
3434
return FileContextInfo(caretContext, fileName, programmingLanguage)
3535
}
36-
private fun PsiFile.toProgrammingLanguage() = lowerCase(this.fileType.name)
36+
fun PsiFile.toProgrammingLanguage() = lowerCase(this.fileType.name)
3737

3838
private fun extractCaretContext(editor: Editor): CaretContext {
3939
val document = editor.document

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/CodeWhispererExplorerActionManager.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ internal class CodeWhispererExplorerActionManager : PersistentStateComponent<Cod
110110
setManualEnabled(true)
111111
setAutoEnabled(true)
112112
setHasAcceptedTermsOfService(true)
113-
114113
refreshCodeWhispererNode(project)
115114
}
116115
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.intellij.openapi.application.ApplicationManager
1515
import com.intellij.openapi.command.WriteCommandAction
1616
import com.intellij.openapi.components.service
1717
import com.intellij.openapi.editor.Editor
18+
import com.intellij.openapi.editor.RangeMarker
1819
import com.intellij.openapi.editor.actionSystem.EditorActionManager
1920
import com.intellij.openapi.editor.actionSystem.TypedAction
2021
import com.intellij.openapi.editor.colors.EditorColors
@@ -594,5 +595,5 @@ interface CodeWhispererUserActionListener {
594595
fun navigatePrevious(states: InvocationContext) {}
595596
fun navigateNext(states: InvocationContext) {}
596597
fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) {}
597-
fun afterAccept(states: InvocationContext, sessionContext: SessionContext) {}
598+
fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {}
598599
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry
5+
6+
import com.intellij.openapi.Disposable
7+
import com.intellij.openapi.application.ApplicationManager
8+
import com.intellij.openapi.editor.RangeMarker
9+
import com.intellij.openapi.editor.event.DocumentEvent
10+
import com.intellij.openapi.editor.impl.event.DocumentEventImpl
11+
import com.intellij.util.Alarm
12+
import com.intellij.util.AlarmFactory
13+
import org.jetbrains.annotations.TestOnly
14+
import software.aws.toolkits.core.utils.debug
15+
import software.aws.toolkits.core.utils.getLogger
16+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
17+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
18+
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
19+
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener
20+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_SECONDS_IN_MINUTE
21+
import software.aws.toolkits.telemetry.CodewhispererLanguage
22+
import software.aws.toolkits.telemetry.CodewhispererTelemetry
23+
import java.time.Duration
24+
import java.time.Instant
25+
import java.util.concurrent.atomic.AtomicBoolean
26+
import java.util.concurrent.atomic.AtomicInteger
27+
import kotlin.math.roundToInt
28+
29+
abstract class CodeWhispererCodeCoverageTracker(
30+
private val timeWindowInSec: Long,
31+
private val language: CodewhispererLanguage,
32+
private val acceptedTokens: AtomicInteger,
33+
private val totalTokens: AtomicInteger,
34+
private val rangeMarkers: MutableList<RangeMarker>
35+
) : Disposable {
36+
val percentage: Int
37+
get() = if (totalTokensSize != 0) calculatePercentage(acceptedTokensSize, totalTokensSize) else 0
38+
val acceptedTokensSize: Int
39+
get() = acceptedTokens.get()
40+
val totalTokensSize: Int
41+
get() = totalTokens.get()
42+
val acceptedRecommendationsCount: Int
43+
get() = rangeMarkers.size
44+
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
45+
private val isShuttingDown = AtomicBoolean(false)
46+
private var startTime: Instant = Instant.now()
47+
48+
init {
49+
val conn = ApplicationManager.getApplication().messageBus.connect()
50+
conn.subscribe(
51+
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
52+
object : CodeWhispererUserActionListener {
53+
override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {
54+
if (states.requestContext.fileContextInfo.programmingLanguage.toCodeWhispererLanguage() != language) return
55+
rangeMarkers.add(rangeMarker)
56+
}
57+
}
58+
)
59+
scheduleCodeWhispererCodeCoverageTracker()
60+
}
61+
62+
fun documentChanged(event: DocumentEvent) {
63+
// When open a file for the first time, IDE will also emit DocumentEvent for loading with `isWholeTextReplaced = true`
64+
// Added this condition to filter out those events
65+
if (event.isWholeTextReplaced) {
66+
LOG.debug { "event with isWholeTextReplaced flag: $event" }
67+
(event as? DocumentEventImpl)?.let {
68+
if (it.initialStartOffset == 0 && it.initialOldLength == event.document.textLength) return
69+
}
70+
}
71+
addAndGetTotalTokens(event.newLength - event.oldLength)
72+
}
73+
74+
private fun flush() {
75+
try {
76+
if (isTelemetryEnabled()) emitCodeWhispererCodeContribution()
77+
} finally {
78+
reset()
79+
scheduleCodeWhispererCodeCoverageTracker()
80+
}
81+
}
82+
83+
private fun scheduleCodeWhispererCodeCoverageTracker() {
84+
if (!alarm.isDisposed && !isShuttingDown.get()) {
85+
alarm.addRequest({ flush() }, Duration.ofSeconds(timeWindowInSec).toMillis())
86+
}
87+
}
88+
89+
private fun addAndGetAcceptedTokens(delta: Int): Int =
90+
if (!isTelemetryEnabled()) acceptedTokensSize
91+
else acceptedTokens.addAndGet(delta)
92+
93+
private fun addAndGetTotalTokens(delta: Int): Int =
94+
if (!isTelemetryEnabled()) totalTokensSize
95+
else {
96+
val result = totalTokens.addAndGet(delta)
97+
if (result < 0) totalTokens.set(0)
98+
result
99+
}
100+
101+
private fun reset() {
102+
startTime = Instant.now()
103+
totalTokens.set(0)
104+
acceptedTokens.set(0)
105+
rangeMarkers.clear()
106+
}
107+
108+
private fun emitCodeWhispererCodeContribution() {
109+
rangeMarkers.forEach {
110+
if (!it.isValid) return@forEach
111+
addAndGetAcceptedTokens(it.endOffset - it.startOffset)
112+
}
113+
114+
CodewhispererTelemetry.codePercentage(
115+
project = null,
116+
acceptedTokensSize,
117+
language,
118+
percentage,
119+
startTime.toString(),
120+
totalTokensSize
121+
)
122+
}
123+
124+
@TestOnly
125+
fun forceTrackerFlush() {
126+
alarm.drainRequestsInTest()
127+
}
128+
129+
@TestOnly
130+
fun activeRequestCount() = alarm.activeRequestCount
131+
132+
override fun dispose() {
133+
if (isShuttingDown.getAndSet(true)) {
134+
return
135+
}
136+
flush()
137+
}
138+
139+
companion object {
140+
private val LOG = getLogger<CodeWhispererCodeCoverageTracker>()
141+
private val instances: MutableMap<CodewhispererLanguage, CodeWhispererCodeCoverageTracker> = mutableMapOf()
142+
143+
fun calculatePercentage(acceptedTokens: Int, totalTokens: Int): Int = ((acceptedTokens.toDouble() * 100) / totalTokens).roundToInt()
144+
fun getInstance(language: CodewhispererLanguage): CodeWhispererCodeCoverageTracker = when (val instance = instances[language]) {
145+
null -> {
146+
val newTracker = DefaultCodeWhispererCodeCoverageTracker(language)
147+
instances[language] = newTracker
148+
newTracker
149+
}
150+
else -> instance
151+
}
152+
153+
@TestOnly
154+
fun getInstancesMap(): MutableMap<CodewhispererLanguage, CodeWhispererCodeCoverageTracker> {
155+
assert(ApplicationManager.getApplication().isUnitTestMode)
156+
return instances
157+
}
158+
}
159+
}
160+
161+
class DefaultCodeWhispererCodeCoverageTracker(language: CodewhispererLanguage) : CodeWhispererCodeCoverageTracker(
162+
5 * TOTAL_SECONDS_IN_MINUTE,
163+
language,
164+
acceptedTokens = AtomicInteger(0),
165+
totalTokens = AtomicInteger(0),
166+
mutableListOf()
167+
)

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionConte
1818
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
1919
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
2020
import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings
21+
import software.aws.toolkits.jetbrains.settings.AwsSettings
2122
import software.aws.toolkits.telemetry.CodewhispererLanguage
2223
import software.aws.toolkits.telemetry.CodewhispererSuggestionState
2324
import software.aws.toolkits.telemetry.CodewhispererTelemetry
@@ -74,14 +75,6 @@ class CodeWhispererTelemetryService {
7475
)
7576
}
7677

77-
private fun ProgrammingLanguage.toCodeWhispererLanguage() = when (languageName) {
78-
CodewhispererLanguage.Python.toString() -> CodewhispererLanguage.Python
79-
CodewhispererLanguage.Java.toString() -> CodewhispererLanguage.Java
80-
CodewhispererLanguage.Javascript.toString() -> CodewhispererLanguage.Javascript
81-
"plain_text" -> CodewhispererLanguage.Plaintext
82-
else -> CodewhispererLanguage.Unknown
83-
}
84-
8578
private fun sendUserDecisionEvent(
8679
requestId: String,
8780
requestContext: RequestContext,
@@ -155,7 +148,7 @@ class CodeWhispererTelemetryService {
155148
val (project, _, triggerTypeInfo) = requestContext
156149
val (sessionId, completionType) = responseContext
157150
val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toCodeWhispererLanguage()
158-
CodeWhispererTracker.getInstance(project).enqueue(
151+
CodeWhispererUserModificationTracker.getInstance(project).enqueue(
159152
AcceptedSuggestionEntry(
160153
time, vFile, range, suggestion, sessionId, requestId, selectedIndex,
161154
triggerTypeInfo.triggerType, completionType,
@@ -210,3 +203,13 @@ class CodeWhispererTelemetryService {
210203
CodewhispererSuggestionState.Reject
211204
}
212205
}
206+
207+
fun ProgrammingLanguage.toCodeWhispererLanguage() = when (languageName) {
208+
CodewhispererLanguage.Python.toString() -> CodewhispererLanguage.Python
209+
CodewhispererLanguage.Java.toString() -> CodewhispererLanguage.Java
210+
CodewhispererLanguage.Javascript.toString() -> CodewhispererLanguage.Javascript
211+
"plain_text" -> CodewhispererLanguage.Plaintext
212+
else -> CodewhispererLanguage.Unknown
213+
}
214+
215+
fun isTelemetryEnabled(): Boolean = AwsSettings.getInstance().isTelemetryEnabled
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ data class AcceptedSuggestionEntry(
4343
val codewhispererRuntimeSource: String?
4444
)
4545

46-
class CodeWhispererTracker(private val project: Project) : Disposable {
46+
class CodeWhispererUserModificationTracker(private val project: Project) : Disposable {
4747
private val acceptedSuggestions = LinkedBlockingDeque<AcceptedSuggestionEntry>(DEFAULT_MAX_QUEUE_SIZE)
4848
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
4949

@@ -157,9 +157,9 @@ class CodeWhispererTracker(private val project: Project) : Disposable {
157157
private val checker = Levenshtein()
158158
private val TELEMETRY_ENABLED = System.getProperty(TELEMETRY_KEY)?.toBoolean() ?: true
159159

160-
private val LOG = getLogger<CodeWhispererTracker>()
160+
private val LOG = getLogger<CodeWhispererUserModificationTracker>()
161161

162-
fun getInstance(project: Project) = project.service<CodeWhispererTracker>()
162+
fun getInstance(project: Project) = project.service<CodeWhispererUserModificationTracker>()
163163
}
164164

165165
override fun dispose() {

0 commit comments

Comments
 (0)