Skip to content

Commit f5a7643

Browse files
authored
[CodeWhisperer] %code tracker enhancement (#3273)
1 parent bc52b74 commit f5a7643

File tree

3 files changed

+161
-71
lines changed

3 files changed

+161
-71
lines changed

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

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry
66
import com.intellij.openapi.Disposable
77
import com.intellij.openapi.application.ApplicationManager
88
import com.intellij.openapi.application.runReadAction
9+
import com.intellij.openapi.editor.Document
910
import com.intellij.openapi.editor.RangeMarker
1011
import com.intellij.openapi.editor.event.DocumentEvent
1112
import com.intellij.openapi.util.Key
@@ -33,16 +34,23 @@ import kotlin.math.roundToInt
3334
abstract class CodeWhispererCodeCoverageTracker(
3435
private val timeWindowInSec: Long,
3536
private val language: CodewhispererLanguage,
36-
private val acceptedTokens: AtomicInteger,
37-
private val totalTokens: AtomicInteger,
38-
private val rangeMarkers: MutableList<RangeMarker>
37+
private val rangeMarkers: MutableList<RangeMarker>,
38+
private val fileToTokens: MutableMap<Document, CodeCoverageTokens>
3939
) : Disposable {
4040
val percentage: Int?
4141
get() = if (totalTokensSize != 0) calculatePercentage(acceptedTokensSize, totalTokensSize) else null
4242
val acceptedTokensSize: Int
43-
get() = acceptedTokens.get()
43+
get() = fileToTokens.map {
44+
it.value.acceptedTokens.get()
45+
}.fold(0) { acc, next ->
46+
acc + next
47+
}
4448
val totalTokensSize: Int
45-
get() = totalTokens.get()
49+
get() = fileToTokens.map {
50+
it.value.totalTokens.get()
51+
}.fold(0) { acc, next ->
52+
acc + next
53+
}
4654
val acceptedRecommendationsCount: Int
4755
get() = rangeMarkers.size
4856
private val isActive: AtomicBoolean = AtomicBoolean(false)
@@ -81,7 +89,7 @@ abstract class CodeWhispererCodeCoverageTracker(
8189
LOG.debug { "event with isWholeTextReplaced flag: $event" }
8290
if (event.oldTimeStamp == 0L) return
8391
}
84-
addAndGetTotalTokens(event.newLength - event.oldLength)
92+
incrementTotalTokens(event.document, event.newLength - event.oldLength)
8593
}
8694

8795
internal fun extractRangeMarkerString(rangeMarker: RangeMarker): String? = runReadAction {
@@ -116,26 +124,36 @@ abstract class CodeWhispererCodeCoverageTracker(
116124
}
117125
}
118126

119-
private fun addAndGetAcceptedTokens(delta: Int): Int =
120-
if (!isTelemetryEnabled()) acceptedTokensSize
121-
else acceptedTokens.addAndGet(delta)
127+
private fun incrementAcceptedTokens(document: Document, delta: Int) {
128+
var tokens = fileToTokens[document]
129+
if (tokens == null) {
130+
tokens = CodeCoverageTokens()
131+
fileToTokens[document] = tokens
132+
}
133+
tokens.apply {
134+
acceptedTokens.addAndGet(delta)
135+
}
136+
}
122137

123-
private fun addAndGetTotalTokens(delta: Int): Int =
124-
if (!isTelemetryEnabled()) totalTokensSize
125-
else {
126-
val result = totalTokens.addAndGet(delta)
127-
if (result < 0) totalTokens.set(0)
128-
result
138+
private fun incrementTotalTokens(document: Document, delta: Int) {
139+
var tokens = fileToTokens[document]
140+
if (tokens == null) {
141+
tokens = CodeCoverageTokens()
142+
fileToTokens[document] = tokens
143+
}
144+
tokens.apply {
145+
totalTokens.addAndGet(delta)
146+
if (totalTokens.get() < 0) totalTokens.set(0)
129147
}
148+
}
130149

131150
private fun reset() {
132151
startTime = Instant.now()
133-
totalTokens.set(0)
134-
acceptedTokens.set(0)
135152
rangeMarkers.clear()
153+
fileToTokens.clear()
136154
}
137155

138-
private fun emitCodeWhispererCodeContribution() {
156+
internal fun emitCodeWhispererCodeContribution() {
139157
rangeMarkers.forEach { rangeMarker ->
140158
if (!rangeMarker.isValid) return@forEach
141159
// if users add more code upon the recommendation generated from CodeWhisperer, we consider those added part as userToken but not CwsprTokens
@@ -150,7 +168,9 @@ abstract class CodeWhispererCodeCoverageTracker(
150168
return@forEach
151169
}
152170
val delta = getAcceptedTokensDelta(originalRecommendation, modifiedRecommendation)
153-
addAndGetAcceptedTokens(delta)
171+
runReadAction {
172+
incrementAcceptedTokens(rangeMarker.document, delta)
173+
}
154174
}
155175

156176
// percentage == null means totalTokens == 0 and users are not editing the document, thus we shouldn't emit telemetry for this
@@ -210,7 +230,15 @@ abstract class CodeWhispererCodeCoverageTracker(
210230
class DefaultCodeWhispererCodeCoverageTracker(language: CodewhispererLanguage) : CodeWhispererCodeCoverageTracker(
211231
5 * TOTAL_SECONDS_IN_MINUTE,
212232
language,
213-
acceptedTokens = AtomicInteger(0),
214-
totalTokens = AtomicInteger(0),
215-
mutableListOf()
233+
mutableListOf(),
234+
mutableMapOf()
216235
)
236+
237+
class CodeCoverageTokens(totalTokens: Int = 0, acceptedTokens: Int = 0) {
238+
val totalTokens: AtomicInteger
239+
val acceptedTokens: AtomicInteger
240+
init {
241+
this.totalTokens = AtomicInteger(totalTokens)
242+
this.acceptedTokens = AtomicInteger(acceptedTokens)
243+
}
244+
}

jetbrains-core/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer
55

66
import com.intellij.openapi.application.ApplicationManager
77
import com.intellij.openapi.command.WriteCommandAction
8+
import com.intellij.openapi.editor.Document
89
import com.intellij.openapi.editor.Editor
910
import com.intellij.openapi.editor.RangeMarker
1011
import com.intellij.openapi.editor.event.DocumentEvent
@@ -24,6 +25,7 @@ import org.mockito.internal.verification.Times
2425
import org.mockito.kotlin.any
2526
import org.mockito.kotlin.argumentCaptor
2627
import org.mockito.kotlin.atLeastOnce
28+
import org.mockito.kotlin.doNothing
2729
import org.mockito.kotlin.doReturn
2830
import org.mockito.kotlin.mock
2931
import org.mockito.kotlin.spy
@@ -46,6 +48,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionConte
4648
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager.Companion.CODEWHISPERER_USER_ACTION_PERFORMED
4749
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
4850
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
51+
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeCoverageTokens
4952
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker
5053
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
5154
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_SECONDS_IN_MINUTE
@@ -55,16 +58,14 @@ import software.aws.toolkits.jetbrains.settings.AwsSettings
5558
import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule
5659
import software.aws.toolkits.telemetry.CodewhispererCompletionType
5760
import software.aws.toolkits.telemetry.CodewhispererLanguage
58-
import java.util.concurrent.atomic.AtomicInteger
5961

6062
class CodeWhispererCodeCoverageTrackerTest {
6163
private class TestCodePercentageTracker(
6264
timeWindowInSec: Long,
6365
language: CodewhispererLanguage,
64-
acceptedTokens: AtomicInteger = AtomicInteger(0),
65-
totalTokens: AtomicInteger = AtomicInteger(0),
66-
rangeMarkers: MutableList<RangeMarker> = mutableListOf()
67-
) : CodeWhispererCodeCoverageTracker(timeWindowInSec, language, acceptedTokens, totalTokens, rangeMarkers)
66+
rangeMarkers: MutableList<RangeMarker> = mutableListOf(),
67+
codeCoverageTokens: MutableMap<Document, CodeCoverageTokens> = mutableMapOf()
68+
) : CodeWhispererCodeCoverageTracker(timeWindowInSec, language, rangeMarkers, codeCoverageTokens)
6869

6970
private class TestTelemetryService(
7071
publisher: TelemetryPublisher = NoOpPublisher(),
@@ -153,7 +154,7 @@ class CodeWhispererCodeCoverageTrackerTest {
153154

154155
@Test
155156
fun `test tracker is listening to document changes and increment totalTokens - add new code`() {
156-
val pythonTracker = spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python, AtomicInteger(0), AtomicInteger(0)))
157+
val pythonTracker = spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python))
157158
CodeWhispererCodeCoverageTracker.getInstancesMap()[CodewhispererLanguage.Python] = pythonTracker
158159
pythonTracker.activateTrackerIfNotActive()
159160

@@ -180,9 +181,12 @@ class CodeWhispererCodeCoverageTrackerTest {
180181

181182
@Test
182183
fun `test tracker is listening to document changes and increment totalTokens - delete code`() {
183-
fixture.configureByText(pythonFileName, pythonTestLeftContext)
184-
val pythonTracker =
185-
spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python, AtomicInteger(0), AtomicInteger(pythonTestLeftContext.length)))
184+
val pythonTracker = TestCodePercentageTracker(
185+
TOTAL_SECONDS_IN_MINUTE,
186+
CodewhispererLanguage.Python,
187+
codeCoverageTokens = mutableMapOf(fixture.editor.document to CodeCoverageTokens(pythonTestLeftContext.length, 0))
188+
)
189+
186190
CodeWhispererCodeCoverageTracker.getInstancesMap()[CodewhispererLanguage.Python] = pythonTracker
187191
pythonTracker.activateTrackerIfNotActive()
188192
assertThat(pythonTracker.totalTokensSize).isEqualTo(pythonTestLeftContext.length)
@@ -197,8 +201,6 @@ class CodeWhispererCodeCoverageTrackerTest {
197201
assertThat(pythonTracker.totalTokensSize).isEqualTo(pythonTestLeftContext.length - 3)
198202
}
199203

200-
// TODO: investigate what cause this test throw NPE when running whole test suite and enable test case
201-
// @Ignore
202204
@Test
203205
fun `test msg CODEWHISPERER_USER_ACTION_PERFORMED will add rangeMarker in the list`() {
204206
val pythonTracker = spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, language = CodewhispererLanguage.Python))
@@ -222,22 +224,20 @@ class CodeWhispererCodeCoverageTrackerTest {
222224
}
223225

224226
@Test
225-
fun `test 0 token will return 0 percent`() {
227+
fun `test 0 totalTokens will return null`() {
226228
val javaTracker = spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, language = CodewhispererLanguage.Java))
227229
CodeWhispererCodeCoverageTracker.getInstancesMap()[CodewhispererLanguage.Java] = javaTracker
228230
assertThat(javaTracker.percentage).isNull()
229231
}
230232

231233
@Test
232234
fun `test flush() will reset tokens and reschedule next telemetry sending`() {
233-
val pythonTracker = spy(
234-
TestCodePercentageTracker(
235-
TOTAL_SECONDS_IN_MINUTE,
236-
CodewhispererLanguage.Python,
237-
AtomicInteger("bar".length),
238-
AtomicInteger("foobar".length)
239-
)
235+
val pythonTracker = TestCodePercentageTracker(
236+
TOTAL_SECONDS_IN_MINUTE,
237+
CodewhispererLanguage.Python,
238+
codeCoverageTokens = mutableMapOf(mock<Document>() to CodeCoverageTokens("foobar".length, "bar".length))
240239
)
240+
241241
pythonTracker.activateTrackerIfNotActive()
242242
assertThat(pythonTracker.activeRequestCount()).isEqualTo(1)
243243
assertThat(pythonTracker.acceptedTokensSize).isEqualTo("bar".length)
@@ -251,71 +251,88 @@ class CodeWhispererCodeCoverageTrackerTest {
251251
}
252252

253253
@Test
254-
fun `test flush() will emit correct telemetry event -- user delete whole accepted reommendation`() {
254+
fun `test when rangeMarker is not vaild, acceptedToken will not be updated`() {
255255
// when user delete whole recommendation, rangeMarker will be isValid = false
256256
val rangeMarkerMock: RangeMarker = mock()
257257
whenever(rangeMarkerMock.isValid).thenReturn(false)
258+
val pythonTracker = spy(
259+
TestCodePercentageTracker(
260+
TOTAL_SECONDS_IN_MINUTE,
261+
CodewhispererLanguage.Python,
262+
mutableListOf(rangeMarkerMock),
263+
)
264+
) {
265+
onGeneric { getAcceptedTokensDelta(any(), any()) } doReturn 100
266+
}
258267

259-
val pythonTracker = TestCodePercentageTracker(
260-
TOTAL_SECONDS_IN_MINUTE,
261-
CodewhispererLanguage.Python,
262-
acceptedTokens = AtomicInteger(0),
263-
totalTokens = AtomicInteger(20),
264-
mutableListOf(rangeMarkerMock)
265-
)
266268
pythonTracker.activateTrackerIfNotActive()
267-
268269
pythonTracker.forceTrackerFlush()
269270

270-
val metricCaptor = argumentCaptor<MetricEvent>()
271-
verify(batcher, Times(1)).enqueue(metricCaptor.capture())
272-
CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount(
273-
metricCaptor.allValues,
274-
CODE_PERCENTAGE,
275-
1,
276-
CWSPR_PERCENTAGE to "0",
277-
CWSPR_Language to CodewhispererLanguage.Python.toString(),
278-
CWSPR_ACCEPTED_TOKENS to "0",
279-
CWSPR_TOTAL_TOKENS to "20"
271+
verify(pythonTracker, Times(0)).getAcceptedTokensDelta(any(), any())
272+
}
273+
274+
@Test
275+
fun `test flush() will call emitTelemetry automatically schedule next call`() {
276+
val pythonTracker = spy(
277+
TestCodePercentageTracker(
278+
TOTAL_SECONDS_IN_MINUTE,
279+
CodewhispererLanguage.Python,
280+
)
280281
)
282+
doNothing().whenever(pythonTracker).emitCodeWhispererCodeContribution()
283+
284+
pythonTracker.activateTrackerIfNotActive()
285+
assertThat(pythonTracker.activeRequestCount()).isEqualTo(1)
286+
pythonTracker.forceTrackerFlush()
287+
288+
verify(pythonTracker, Times(1)).emitCodeWhispererCodeContribution()
289+
assertThat(pythonTracker.activeRequestCount()).isEqualTo(1)
281290
}
282291

283292
@Test
284-
fun `test flush() will emit telemetry event`() {
293+
fun `test emitCodeWhispererCodeContribution`() {
285294
val rangeMarkerMock1 = mock<RangeMarker> {
286295
on { isValid } doReturn true
287296
on { getUserData(any<Key<String>>()) } doReturn "foo"
297+
on { document } doReturn fixture.editor.document
288298
}
289299

290300
val pythonTracker = spy(
291301
TestCodePercentageTracker(
292302
TOTAL_SECONDS_IN_MINUTE,
293303
CodewhispererLanguage.Python,
294-
AtomicInteger(10),
295-
AtomicInteger(100),
296-
mutableListOf(rangeMarkerMock1)
304+
mutableListOf(rangeMarkerMock1),
305+
mutableMapOf(fixture.editor.document to CodeCoverageTokens(totalTokens = 100, acceptedTokens = 0))
297306
)
298307
) {
299308
onGeneric { extractRangeMarkerString(any()) } doReturn "fou"
309+
onGeneric { getAcceptedTokensDelta(any(), any()) } doReturn 99
300310
}
301311

302-
pythonTracker.activateTrackerIfNotActive()
303-
pythonTracker.forceTrackerFlush()
312+
pythonTracker.emitCodeWhispererCodeContribution()
313+
304314
val metricCaptor = argumentCaptor<MetricEvent>()
305315
verify(batcher, Times(1)).enqueue(metricCaptor.capture())
306316
CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount(
307317
metricCaptor.allValues,
308318
CODE_PERCENTAGE,
309319
1,
320+
CWSPR_PERCENTAGE to "99",
321+
CWSPR_ACCEPTED_TOKENS to "99",
322+
CWSPR_TOTAL_TOKENS to "100"
310323
)
311324
}
312325

313326
@Test
314327
fun `test flush() won't emit telemetry event when users not enabling telemetry`() {
315328
AwsSettings.getInstance().isTelemetryEnabled = false
316-
val pythonTracker = TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python, AtomicInteger(10), AtomicInteger(20))
329+
val pythonTracker = spy(TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python))
330+
doNothing().whenever(pythonTracker).emitCodeWhispererCodeContribution()
331+
332+
pythonTracker.activateTrackerIfNotActive()
317333
pythonTracker.forceTrackerFlush()
318-
verify(batcher, Times(0)).enqueue(any())
334+
335+
verify(pythonTracker, Times(0)).emitCodeWhispererCodeContribution()
319336
}
320337

321338
@Test
@@ -364,8 +381,8 @@ class CodeWhispererCodeCoverageTrackerTest {
364381
}
365382

366383
@Test
367-
fun `test flush() won't emit telemetry when users are not editing the document`() {
368-
val pythonTracker = TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python, AtomicInteger(0), AtomicInteger(0))
384+
fun `test flush() won't emit telemetry when users are not editing the document (totalTokens == 0)`() {
385+
val pythonTracker = TestCodePercentageTracker(TOTAL_SECONDS_IN_MINUTE, CodewhispererLanguage.Python)
369386
pythonTracker.activateTrackerIfNotActive()
370387
assertThat(pythonTracker.activeRequestCount()).isEqualTo(1)
371388
pythonTracker.forceTrackerFlush()

0 commit comments

Comments
 (0)