server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeDiffTracker.ts (91 lines of code) (raw):
import { distance } from 'fastest-levenshtein'
import { Position } from '@aws/language-server-runtimes/server-interface'
import { Features } from '../types'
import { getErrorMessage, getUnmodifiedAcceptedTokens } from '../../shared/utils'
export interface AcceptedSuggestionEntry {
fileUrl: string
time: number
originalString: string
startPosition: Position
endPosition: Position
customizationArn?: string
}
export interface CodeDiffTrackerOptions {
flushInterval?: number
timeElapsedThreshold?: number
maxQueueSize?: number
}
/**
* This class calculates the percentage of user modification after a time threshold and emits metric
* The current calculation method is (Levenshtein edit distance / acceptedSuggestion.length).
*/
export class CodeDiffTracker<T extends AcceptedSuggestionEntry = AcceptedSuggestionEntry> {
/**
* time indication the flush frequency of which the checks are
*/
private static readonly FLUSH_INTERVAL = 1000 * 60 // 1 minute
/**
* time threshold before measuring the modification after accepted into the editor
*/
private static readonly TIME_ELAPSED_THRESHOLD = 1000 * 60 * 5 // 5 minutes
private static readonly DEFAULT_MAX_QUEUE_SIZE = 10000
#eventQueue: T[]
#interval?: NodeJS.Timeout
#workspace: Features['workspace']
#logging: Features['logging']
#recordMetric: (
entry: T,
codeModificationPercentage: number,
unmodifiedAcceptedCharacterCount: number
) => Promise<void>
#flushInterval: number
#timeElapsedThreshold: number
#maxQueueSize: number
/**
* This function calculates the Levenshtein edit distance of currString from original accepted String
* then return a percentage against the length of accepted string (capped by 1)
* @param currString the current string in the same location as the previously accepted suggestion
* @param acceptedString the accepted suggestion that was inserted into the editor
*/
public static checkDiff(currString?: string, acceptedString?: string): number {
if (!currString || !acceptedString) {
return 1
}
const diff = distance(currString, acceptedString)
return Math.min(1, diff / acceptedString.length)
}
constructor(
workspace: Features['workspace'],
logging: Features['logging'],
recordMetric: (
entry: T,
codeModificationPercentage: number,
unmodifiedAcceptedCharacterCount: number
) => Promise<void>,
options?: CodeDiffTrackerOptions
) {
this.#eventQueue = []
this.#workspace = workspace
this.#logging = logging
this.#recordMetric = recordMetric
this.#flushInterval = options?.flushInterval ?? CodeDiffTracker.FLUSH_INTERVAL
this.#timeElapsedThreshold = options?.timeElapsedThreshold ?? CodeDiffTracker.TIME_ELAPSED_THRESHOLD
this.#maxQueueSize = options?.maxQueueSize ?? CodeDiffTracker.DEFAULT_MAX_QUEUE_SIZE
}
public enqueue(suggestion: T) {
this.#eventQueue.push(suggestion)
// remove the oldest entries if the queue if full
while (this.#eventQueue.length > this.#maxQueueSize) {
this.#eventQueue.shift()
}
// ensure there is an active interval
this.#startInterval()
}
public async shutdown() {
this.#clearInterval()
try {
await this.flush()
} catch (e) {
this.#logging.log(`Error encountered while performing the final flush: ${e}`)
}
}
// Used for accessing the codeDiffTracker eventQueue in unit tests
public get eventQueue() {
return this.#eventQueue
}
private async flush() {
const newEventQueue: T[] = []
// emit the ones that reach the time limit and start a new queue with remaining
for (const suggestion of this.#eventQueue) {
if (Date.now() - suggestion.time >= this.#timeElapsedThreshold) {
await this.#emitTelemetryOnSuggestion(suggestion as T)
} else {
newEventQueue.push(suggestion as T)
}
}
this.#eventQueue = newEventQueue
// shutdown the interval when queue is empty
if (this.#eventQueue.length === 0) {
this.#clearInterval()
}
}
async #emitTelemetryOnSuggestion(suggestion: T) {
try {
const document = suggestion.fileUrl && (await this.#workspace.getTextDocument(suggestion.fileUrl))
if (document) {
const currString = document.getText({
start: suggestion.startPosition,
end: suggestion.endPosition,
})
const percentage = CodeDiffTracker.checkDiff(currString, suggestion.originalString)
const unmodifiedAcceptedCharacterCount = getUnmodifiedAcceptedTokens(
suggestion.originalString,
currString
)
await this.#recordMetric(suggestion, percentage, unmodifiedAcceptedCharacterCount)
}
} catch (e) {
this.#logging.log(`Exception Thrown from CodeDiffTracker: ${e}`)
}
}
#startInterval() {
if (!this.#interval) {
const recursiveSetTimeout = () => {
this.#interval = setTimeout(async () => {
try {
await this.flush()
} catch (e) {
this.#logging.log(`flush failed: ${getErrorMessage(e)}`)
} finally {
recursiveSetTimeout()
}
}, this.#flushInterval)
}
recursiveSetTimeout()
}
}
#clearInterval() {
clearTimeout(this.#interval)
this.#interval = undefined
}
}