Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
- Detailed view with comprehensive statistics
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
114 changes: 110 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, SessionFileCache> = new Map();
private tokenEstimators: { [key: string]: number } = {
'gpt-4': 0.25,
'gpt-4.1': 0.25,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -266,21 +313,37 @@ 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`);

if (sessionFiles.length === 0) {
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`);

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<SessionFileCache> {
// 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<number> {
const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime);
return sessionData.tokens;
}

private async countInteractionsInSessionCached(sessionFile: string, mtime: number): Promise<number> {
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
return sessionData.interactions;
}

private async getModelUsageFromSessionCached(sessionFile: string, mtime: number): Promise<ModelUsage> {
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
return sessionData.modelUsage;
}

private checkCopilotExtension(): void {
this.log('Checking GitHub Copilot extension status');

Expand Down Expand Up @@ -1155,6 +1259,8 @@ class CopilotTokenTracker implements vscode.Disposable {
}
this.statusBarItem.dispose();
this.outputChannel.dispose();
// Clear cache on disposal
this.sessionFileCache.clear();
}
}

Expand Down
Loading