-
Notifications
You must be signed in to change notification settings - Fork 747
feat(nep): Data Instrumentation #7109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jpinkney-aws
merged 29 commits into
aws:master
from
tomcat323:NEP/DataInstrumentationLaunch
May 5, 2025
Merged
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
808d95e
Added Prediction folder, integrated with API, status bar visualier
tomcat323 ced1ec6
refactor supplemental context generation into diffGenerator
tomcat323 475c5dd
clean up pakcage.json
tomcat323 440c44e
update supplemental context logic according to science feedback
tomcat323 ea601c9
not include debug webview in release
tomcat323 2bfa873
refactor config to constants.ts
tomcat323 5aa4c51
added tests for prediction tracker
tomcat323 9cdb6f5
clean up
tomcat323 a371128
remove tracker dispose in activation
tomcat323 dabf670
remove unused imports
tomcat323 9476765
refactored filePath to getter functions
tomcat323 d233c0c
refactor load from storage, use reduce in getTotalCount
tomcat323 fa6b40b
remove predictionTypes from request
tomcat323 b935964
enforce supplemental context char limit
tomcat323 25cc9f9
address comments, deprecate fs usage, use in memory content tracking
tomcat323 18603fa
clean up logging, fix predictionTracker tests
tomcat323 6c3d6ab
Merge branch 'master' into NEP/DataInstrumentationLaunch
tomcat323 c683cb8
refactor paths for windows test fail
tomcat323 7d25d3f
fix uri path leading seperator
tomcat323 0446ccb
add more try-catch for error handling
tomcat323 1a9b305
remove unused maxfiles constant
tomcat323 4250b65
add tests for diff context generation
tomcat323 ed30502
Merge branch 'master' into NEP/DataInstrumentationLaunch
tomcat323 6261398
resolve merge conflict in settings
tomcat323 adfe12e
renamed files, removed redundent checks
tomcat323 092d0b7
Rename PredictionTracker.ts to predictionTracker.ts
tomcat323 71384d6
Rename PredictionKeyStrokeHandler.ts to predictionKeyStrokeHandler.ts
tomcat323 399d67d
Rename PredictionTracker.test.ts to predictionTracker.test.ts
tomcat323 d94891d
clean up
tomcat323 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
packages/core/src/codewhisperer/nextEditPrediction/PredictionKeyStrokeHandler.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| /*! | ||
jpinkney-aws marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as vscode from 'vscode' | ||
| import { PredictionTracker } from './PredictionTracker' | ||
|
|
||
| /** | ||
| * Monitors document changes in the editor and track them for prediction. | ||
| */ | ||
| export class PredictionKeyStrokeHandler { | ||
| private disposables: vscode.Disposable[] = [] | ||
| private tracker: PredictionTracker | ||
| private shadowCopies: Map<string, string> = new Map() | ||
|
|
||
| /** | ||
| * Creates a new PredictionKeyStrokeHandler | ||
| * @param context The extension context | ||
| * @param tracker The prediction tracker instance | ||
| * @param config Configuration options | ||
| */ | ||
| constructor(tracker: PredictionTracker) { | ||
| this.tracker = tracker | ||
|
|
||
| // Initialize shadow copies for currently visible editors when extension starts | ||
| this.initializeVisibleDocuments() | ||
|
|
||
| // Register event handlers | ||
| this.registerVisibleDocumentListener() | ||
| this.registerTextDocumentChangeListener() | ||
| } | ||
|
|
||
| /** | ||
| * Initializes shadow copies for all currently visible text editors | ||
| */ | ||
| private initializeVisibleDocuments(): void { | ||
| const editors = vscode.window.visibleTextEditors | ||
|
|
||
| for (const editor of editors) { | ||
| if (editor.document.uri.scheme === 'file') { | ||
| this.updateShadowCopy(editor.document) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers listeners for visibility events to maintain shadow copies of document content | ||
| * Only store and update shadow copies for currently visible editors | ||
| * And remove shadow copies for files that are no longer visible | ||
| * And edits are processed only if a shadow copy exists | ||
| * This avoids the memory problem if hidden files are bulk edited, i.e. with global find/replace | ||
| */ | ||
| private registerVisibleDocumentListener(): void { | ||
| // Track when documents become visible (switched to) | ||
| const visibleDisposable = vscode.window.onDidChangeVisibleTextEditors((editors) => { | ||
| const currentVisibleFiles = new Set<string>() | ||
|
|
||
| for (const editor of editors) { | ||
| if (editor.document.uri.scheme === 'file') { | ||
| const filePath = editor.document.uri.fsPath | ||
| currentVisibleFiles.add(filePath) | ||
| this.updateShadowCopy(editor.document) | ||
| } | ||
| } | ||
|
|
||
| for (const filePath of this.shadowCopies.keys()) { | ||
| if (!currentVisibleFiles.has(filePath)) { | ||
| this.shadowCopies.delete(filePath) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| this.disposables.push(visibleDisposable) | ||
| } | ||
|
|
||
| private updateShadowCopy(document: vscode.TextDocument): void { | ||
| if (document.uri.scheme === 'file') { | ||
jpinkney-aws marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.shadowCopies.set(document.uri.fsPath, document.getText()) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers listener for text document changes to send to tracker | ||
| */ | ||
| private registerTextDocumentChangeListener(): void { | ||
| // Listen for document changes | ||
| const changeDisposable = vscode.workspace.onDidChangeTextDocument(async (event) => { | ||
| const filePath = event.document.uri.fsPath | ||
| const prevContent = this.shadowCopies.get(filePath) | ||
|
|
||
| // Skip if there are no content changes or if the file is not visible | ||
| if ( | ||
| event.contentChanges.length === 0 || | ||
| event.document.uri.scheme !== 'file' || | ||
| prevContent === undefined | ||
| ) { | ||
| return | ||
| } | ||
|
|
||
| await this.tracker.processEdit(event.document, prevContent) | ||
| this.updateShadowCopy(event.document) | ||
| }) | ||
|
|
||
| this.disposables.push(changeDisposable) | ||
| } | ||
|
|
||
| /** | ||
| * Disposes of all resources used by this handler | ||
| */ | ||
| public dispose(): void { | ||
| for (const disposable of this.disposables) { | ||
| disposable.dispose() | ||
| } | ||
| this.disposables = [] | ||
| } | ||
| } | ||
241 changes: 241 additions & 0 deletions
241
packages/core/src/codewhisperer/nextEditPrediction/PredictionTracker.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as vscode from 'vscode' | ||
| import { getLogger } from '../../shared/logger/logger' | ||
| import * as diffGenerator from './diffContextGenerator' | ||
| import * as codewhispererClient from '../client/codewhisperer' | ||
| import { predictionTrackerDefaultConfig } from '../models/constants' | ||
| import globals from '../../shared/extensionGlobals' | ||
|
|
||
| // defaul values are stored in codewhisperer/model/constants | ||
| export interface FileTrackerConfig { | ||
| maxFiles: number | ||
| maxStorageSizeKb: number | ||
| debounceIntervalMs: number | ||
| maxAgeMs: number | ||
| maxSupplementalContext: number | ||
| } | ||
|
|
||
| /** | ||
| * Represents a snapshot of a file at a specific point in time | ||
| */ | ||
| export interface FileSnapshot { | ||
| filePath: string | ||
| size: number | ||
| timestamp: number | ||
| content: string | ||
| } | ||
|
|
||
| export class PredictionTracker { | ||
| private snapshots: Map<string, FileSnapshot[]> = new Map() | ||
| private logger = getLogger('nextEditPrediction') | ||
| readonly config: FileTrackerConfig | ||
| private storageSize: number = 0 | ||
|
|
||
| constructor(extensionContext: vscode.ExtensionContext, config?: Partial<FileTrackerConfig>) { | ||
| this.config = { | ||
| ...predictionTrackerDefaultConfig, | ||
| ...config, | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Processes an edit to a document and takes a snapshot if needed | ||
| * @param document The document being edited | ||
| * @param previousContent The content of the document before the edit | ||
| */ | ||
| public async processEdit(document: vscode.TextDocument, previousContent: string): Promise<void> { | ||
tomcat323 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const filePath = document.uri.fsPath | ||
|
|
||
| if (!document.uri.scheme.startsWith('file')) { | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| // Get existing snapshots for this file | ||
| const fileSnapshots = this.snapshots.get(filePath) || [] | ||
| const timestamp = globals.clock.Date.now() | ||
|
|
||
| // Anti-throttling, only add snap shot after the debounce is cleared | ||
| const shouldAddSnapshot = | ||
| fileSnapshots.length === 0 || | ||
| timestamp - fileSnapshots[fileSnapshots.length - 1].timestamp > this.config.debounceIntervalMs | ||
|
|
||
| if (!shouldAddSnapshot) { | ||
| return | ||
| } | ||
|
|
||
| const content = previousContent | ||
| const size = Buffer.byteLength(content, 'utf8') | ||
| const snapshot: FileSnapshot = { | ||
| filePath, | ||
| size, | ||
| timestamp, | ||
| content, | ||
| } | ||
|
|
||
| fileSnapshots.push(snapshot) | ||
| this.snapshots.set(filePath, fileSnapshots) | ||
| this.storageSize += size | ||
| this.logger.debug( | ||
| `Snapshot taken for file: ${filePath}, total snapshots: ${this.getTotalSnapshotCount()}, total size: ${Math.round(this.storageSize / 1024)} KB` | ||
| ) | ||
|
|
||
| await this.enforceMemoryLimits() | ||
| this.enforceTimeLimits(snapshot) | ||
| } catch (err) { | ||
| this.logger.error(`Failed to save snapshot: ${err}`) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sets up a timeout to delete the given snapshot after it exceeds the max age | ||
| */ | ||
| private enforceTimeLimits(snapshot: FileSnapshot): void { | ||
| const fileSnapshots = this.snapshots.get(snapshot.filePath) | ||
| if (fileSnapshots === undefined) { | ||
| return | ||
| } | ||
|
|
||
| setTimeout(() => { | ||
| // find the snapshot and remove it | ||
| const index = fileSnapshots.indexOf(snapshot) | ||
| if (index !== -1) { | ||
| fileSnapshots.splice(index, 1) | ||
| this.storageSize -= snapshot.size | ||
| if (fileSnapshots.length === 0) { | ||
| this.snapshots.delete(snapshot.filePath) | ||
| } | ||
| this.logger.debug( | ||
| `Snapshot deleted (aged out) for file: ${snapshot.filePath}, remaining snapshots: ${this.getTotalSnapshotCount()}, new size: ${Math.round(this.storageSize / 1024)} KB` | ||
| ) | ||
| } | ||
| }, this.config.maxAgeMs) | ||
| } | ||
|
|
||
| /** | ||
| * Enforces memory limits by removing old snapshots if necessary | ||
| */ | ||
| private async enforceMemoryLimits(): Promise<void> { | ||
| while (this.storageSize > this.config.maxStorageSizeKb * 1024) { | ||
| const oldestFile = this.findOldestFile() | ||
tomcat323 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (!oldestFile) { | ||
| break | ||
| } | ||
|
|
||
| const fileSnapshots = this.snapshots.get(oldestFile) | ||
| if (!fileSnapshots || fileSnapshots.length === 0) { | ||
| this.snapshots.delete(oldestFile) | ||
| continue | ||
| } | ||
|
|
||
| const removedSnapshot = fileSnapshots.shift() | ||
| if (removedSnapshot) { | ||
| this.storageSize -= removedSnapshot.size | ||
| this.logger.debug( | ||
| `Snapshot deleted (memory limit) for file: ${removedSnapshot.filePath}, remaining snapshots: ${this.getTotalSnapshotCount()}, new size: ${Math.round(this.storageSize / 1024)} KB` | ||
| ) | ||
| } | ||
|
|
||
| if (fileSnapshots.length === 0) { | ||
| this.snapshots.delete(oldestFile) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Finds the file with the oldest snapshot | ||
| * @returns The file path of the oldest snapshot | ||
| */ | ||
| private findOldestFile(): string | undefined { | ||
| let oldestTime = Number.MAX_SAFE_INTEGER | ||
| let oldestFile: string | undefined | ||
|
|
||
| for (const [filePath, snapshots] of this.snapshots.entries()) { | ||
| if (snapshots.length === 0) { | ||
| continue | ||
| } | ||
|
|
||
| const oldestSnapshot = snapshots[0] | ||
| if (oldestSnapshot.timestamp < oldestTime) { | ||
| oldestTime = oldestSnapshot.timestamp | ||
| oldestFile = filePath | ||
| } | ||
| } | ||
|
|
||
| return oldestFile | ||
| } | ||
|
|
||
| /** | ||
| * Gets all snapshots for a specific file | ||
| * @param filePath The path to the file | ||
| * @returns Array of snapshots for the file | ||
| */ | ||
| public getFileSnapshots(filePath: string): FileSnapshot[] { | ||
| return this.snapshots.get(filePath) || [] | ||
| } | ||
|
|
||
| /** | ||
| * Gets all tracked files | ||
| * @returns Array of file paths | ||
| */ | ||
| public getTrackedFiles(): string[] { | ||
| return Array.from(this.snapshots.keys()) | ||
| } | ||
|
|
||
| public getTotalSnapshotCount(): number { | ||
tomcat323 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
tomcat323 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return Array.from(this.snapshots.values()).reduce((count, snapshots) => count + snapshots.length, 0) | ||
| } | ||
|
|
||
| public async getSnapshotContent(snapshot: FileSnapshot): Promise<string> { | ||
| return snapshot.content | ||
| } | ||
|
|
||
| /** | ||
| * Generates unified diffs between adjacent snapshots of a file | ||
| * and between the newest snapshot and the current file content | ||
| * | ||
| * @returns Array of SupplementalContext objects containing diffs between snapshots and current content | ||
| */ | ||
| public async generatePredictionSupplementalContext(): Promise<codewhispererClient.SupplementalContext[]> { | ||
tomcat323 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const activeEditor = vscode.window.activeTextEditor | ||
| if (activeEditor === undefined) { | ||
| return [] | ||
| } | ||
| const filePath = activeEditor.document.uri.fsPath | ||
| const currentContent = activeEditor.document.getText() | ||
| const snapshots = this.getFileSnapshots(filePath) | ||
|
|
||
| if (snapshots.length === 0) { | ||
| return [] | ||
| } | ||
|
|
||
| // Create SnapshotContent array from snapshots | ||
| const snapshotContents: diffGenerator.SnapshotContent[] = snapshots.map((snapshot) => ({ | ||
| filePath: snapshot.filePath, | ||
| content: snapshot.content, | ||
| timestamp: snapshot.timestamp, | ||
| })) | ||
|
|
||
| // Use the diffGenerator module to generate supplemental contexts | ||
| return diffGenerator.generateDiffContexts( | ||
| filePath, | ||
| currentContent, | ||
| snapshotContents, | ||
| this.config.maxSupplementalContext | ||
| ) | ||
| } catch (err) { | ||
| // this ensures we are not breaking inline requests | ||
| this.logger.error(`Failed to generate prediction supplemental context: ${err}`) | ||
| return [] | ||
| } | ||
| } | ||
|
|
||
| public getTotalSize() { | ||
| return this.storageSize | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this these NEP modules live in the same dir as the other "trackers"?
https://github.com/aws/aws-toolkit-vscode/tree/master/packages/core/src/codewhisperer/tracker
are they really not sharing any concepts at all? this PR is all new code.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are not shared IMO, the existing tracker collects statistical metrics for telemetry, the NEP tracker tracks content changes. Their hyper parameters are also very different.
Since the new tracker is populating the
supplementalContextfield, maybe we can also consider https://github.com/aws/aws-toolkit-vscode/tree/master/packages/core/src/codewhisperer/util/supplementalContext?