diff --git a/CHANGELOG.md b/CHANGELOG.md index a44b88e0..d34cfa35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +### Added +- Intelligent file caching to improve performance when processing session files +- Cache management with automatic size limits and cleanup of non-existent files +- Cache hit/miss rate logging for performance monitoring + +### Changed +- Session file processing now uses cached data when files haven't been modified +- Reduced file I/O operations during periodic updates for better performance + +- Initial release - Automated VSIX build and release workflow ## [0.0.1] - Initial Release @@ -15,4 +25,4 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Automatic updates every 5 minutes - Click to refresh functionality - Smart estimation using character-based analysis -- Detailed view with comprehensive statistics \ No newline at end of file +- Detailed view with comprehensive statistics diff --git a/README.md b/README.md index 2a0b1596..e22778a3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A VS Code extension that shows your daily and monthly GitHub Copilot estimated t - **Automatic Updates**: Refreshes every 5 minutes to show the latest usage - **Click to Refresh**: Click the status bar item to manually refresh the token count - **Smart Estimation**: Uses character-based analysis with model-specific ratios for token estimation +- **Intelligent Caching**: Caches processed session files to speed up subsequent updates when files haven't changed ## Status Bar Display @@ -21,6 +22,16 @@ Hovering on the status bar item shows a detailed breakdown of token usage: Clicking the status bar item opens a detailed view with comprehensive statistics: ![Detailed View](docs/images/03%20Detail%20panel.png) +## Performance Optimization + +The extension uses intelligent caching to improve performance: + +- **File Modification Tracking**: Only re-processes session files when they have been modified since the last read +- **Efficient Cache Management**: Stores calculated token counts, interaction counts, and model usage data for each file +- **Memory Management**: Automatically limits cache size to prevent memory issues (maximum 1000 cached files) +- **Cache Statistics**: Logs cache hit/miss rates to help monitor performance improvements + +This caching significantly reduces the time needed for periodic updates, especially when you have many chat session files. ## Known Issues diff --git a/src/extension.ts b/src/extension.ts index 855116e4..eee8e00c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -37,12 +37,20 @@ interface DetailedStats { lastUpdated: Date; } +interface SessionFileCache { + tokens: number; + interactions: number; + modelUsage: ModelUsage; + mtime: number; // file modification time as timestamp +} + class CopilotTokenTracker implements vscode.Disposable { private statusBarItem: vscode.StatusBarItem; private updateInterval: NodeJS.Timeout | undefined; private initialDelayTimeout: NodeJS.Timeout | undefined; private detailsPanel: vscode.WebviewPanel | undefined; private outputChannel: vscode.OutputChannel; + private sessionFileCache: Map = new Map(); private tokenEstimators: { [key: string]: number } = { 'gpt-4': 0.25, 'gpt-4.1': 0.25, @@ -81,6 +89,45 @@ class CopilotTokenTracker implements vscode.Disposable { } } + // Cache management methods + private isCacheValid(filePath: string, currentMtime: number): boolean { + const cached = this.sessionFileCache.get(filePath); + return cached !== undefined && cached.mtime === currentMtime; + } + + private getCachedSessionData(filePath: string): SessionFileCache | undefined { + return this.sessionFileCache.get(filePath); + } + + private setCachedSessionData(filePath: string, data: SessionFileCache): void { + this.sessionFileCache.set(filePath, data); + + // Limit cache size to prevent memory issues (keep last 1000 files) + if (this.sessionFileCache.size > 1000) { + const entries = Array.from(this.sessionFileCache.entries()); + // Remove oldest entries (simple FIFO approach) + const toRemove = entries.slice(0, this.sessionFileCache.size - 1000); + for (const [key] of toRemove) { + this.sessionFileCache.delete(key); + } + } + } + + private clearExpiredCache(): void { + // Remove cache entries for files that no longer exist + const filesToCheck = Array.from(this.sessionFileCache.keys()); + for (const filePath of filesToCheck) { + try { + if (!fs.existsSync(filePath)) { + this.sessionFileCache.delete(filePath); + } + } catch (error) { + // File access error, remove from cache + this.sessionFileCache.delete(filePath); + } + } + } + constructor() { @@ -234,7 +281,7 @@ class CopilotTokenTracker implements vscode.Disposable { // Only process files modified in the current month if (fileStats.mtime >= monthStart) { - const tokens = await this.estimateTokensFromSession(sessionFile); + const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime()); monthTokens += tokens; @@ -266,6 +313,9 @@ class CopilotTokenTracker implements vscode.Disposable { const monthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage }; try { + // Clean expired cache entries + this.clearExpiredCache(); + const sessionFiles = await this.getCopilotSessionFiles(); this.log(`Processing ${sessionFiles.length} session files for detailed stats`); @@ -273,14 +323,27 @@ class CopilotTokenTracker implements vscode.Disposable { this.warn('No session files found - this might indicate an issue in GitHub Codespaces or different VS Code configuration'); } + let cacheHits = 0; + let cacheMisses = 0; + for (const sessionFile of sessionFiles) { try { const fileStats = fs.statSync(sessionFile); if (fileStats.mtime >= monthStart) { - const tokens = await this.estimateTokensFromSession(sessionFile); - const interactions = await this.countInteractionsInSession(sessionFile); - const modelUsage = await this.getModelUsageFromSession(sessionFile); + // Check if data is cached before making calls + const wasCached = this.isCacheValid(sessionFile, fileStats.mtime.getTime()); + + const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime()); + const interactions = await this.countInteractionsInSessionCached(sessionFile, fileStats.mtime.getTime()); + const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, fileStats.mtime.getTime()); + + // Update cache statistics + if (wasCached) { + cacheHits++; + } else { + cacheMisses++; + } this.log(`Session ${path.basename(sessionFile)}: ${tokens} tokens, ${interactions} interactions`); @@ -308,6 +371,8 @@ class CopilotTokenTracker implements vscode.Disposable { this.warn(`Error processing session file ${sessionFile}: ${fileError}`); } } + + this.log(`Cache performance - Hits: ${cacheHits}, Misses: ${cacheMisses}, Hit Rate: ${sessionFiles.length > 0 ? ((cacheHits / sessionFiles.length) * 100).toFixed(1) : 0}%`); } catch (error) { this.error('Error calculating detailed stats:', error); } @@ -404,6 +469,45 @@ class CopilotTokenTracker implements vscode.Disposable { return modelUsage; } + // Cached versions of session file reading methods + private async getSessionFileDataCached(sessionFilePath: string, mtime: number): Promise { + // Check if we have valid cached data + const cached = this.getCachedSessionData(sessionFilePath); + if (cached && cached.mtime === mtime) { + return cached; + } + + // Cache miss - read and process the file once to get all data + const tokens = await this.estimateTokensFromSession(sessionFilePath); + const interactions = await this.countInteractionsInSession(sessionFilePath); + const modelUsage = await this.getModelUsageFromSession(sessionFilePath); + + const sessionData: SessionFileCache = { + tokens, + interactions, + modelUsage, + mtime + }; + + this.setCachedSessionData(sessionFilePath, sessionData); + return sessionData; + } + + private async estimateTokensFromSessionCached(sessionFilePath: string, mtime: number): Promise { + const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime); + return sessionData.tokens; + } + + private async countInteractionsInSessionCached(sessionFile: string, mtime: number): Promise { + const sessionData = await this.getSessionFileDataCached(sessionFile, mtime); + return sessionData.interactions; + } + + private async getModelUsageFromSessionCached(sessionFile: string, mtime: number): Promise { + const sessionData = await this.getSessionFileDataCached(sessionFile, mtime); + return sessionData.modelUsage; + } + private checkCopilotExtension(): void { this.log('Checking GitHub Copilot extension status'); @@ -1155,6 +1259,8 @@ class CopilotTokenTracker implements vscode.Disposable { } this.statusBarItem.dispose(); this.outputChannel.dispose(); + // Clear cache on disposal + this.sessionFileCache.clear(); } }