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
2 changes: 1 addition & 1 deletion src/backend/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface BackendFacadeDeps {
estimateTokensFromText: (text: string, model: string) => number;
getModelFromRequest: (request: ChatRequest) => string;
// Cache integration for performance
getSessionFileDataCached?: (sessionFilePath: string, mtime: number) => Promise<SessionFileCache>;
getSessionFileDataCached?: (sessionFilePath: string, mtime: number, fileSize: number) => Promise<SessionFileCache>;
}

export class BackendFacade {
Expand Down
7 changes: 5 additions & 2 deletions src/backend/services/syncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface SyncServiceDeps {
estimateTokensFromText: (text: string, model: string) => number;
getModelFromRequest: (request: ChatRequest) => string;
// Cache integration for performance
getSessionFileDataCached?: (sessionFilePath: string, mtime: number) => Promise<SessionFileCache>;
getSessionFileDataCached?: (sessionFilePath: string, mtime: number, fileSize: number) => Promise<SessionFileCache>;
// UI refresh callback after successful sync
updateTokenStats?: () => Promise<void>;
}
Expand Down Expand Up @@ -176,6 +176,7 @@ export class SyncService {
private async processCachedSessionFile(
sessionFile: string,
fileMtimeMs: number,
fileSize: number,
workspaceId: string,
machineId: string,
userId: string | undefined,
Expand All @@ -184,7 +185,7 @@ export class SyncService {
now: Date
): Promise<boolean> {
try {
const cachedData = await this.deps.getSessionFileDataCached!(sessionFile, fileMtimeMs);
const cachedData = await this.deps.getSessionFileDataCached!(sessionFile, fileMtimeMs, fileSize);

// Validate cached data structure to prevent injection/corruption
if (!cachedData || typeof cachedData !== 'object') {
Expand Down Expand Up @@ -450,9 +451,11 @@ export class SyncService {
// Note: We still parse the file to get accurate day keys from timestamps,
// but use cached token counts for performance
if (useCachedData) {
const fileStat = await fs.promises.stat(sessionFile);
const cacheSuccess = await this.processCachedSessionFile(
sessionFile,
fileMtimeMs,
fileStat.size,
workspaceId,
machineId,
userId,
Expand Down
85 changes: 56 additions & 29 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ interface SessionFileCache {
interactions: number;
modelUsage: ModelUsage;
mtime: number; // file modification time as timestamp
size?: number; // file size in bytes (optional for backward compatibility)
usageAnalysis?: SessionUsageAnalysis; // New analysis data
}

Expand Down Expand Up @@ -195,7 +196,7 @@ interface SessionLogData {

class CopilotTokenTracker implements vscode.Disposable {
// Cache version - increment this when making changes that require cache invalidation
private static readonly CACHE_VERSION = 9; // Added implicitSelection to ContextReferenceUsage (2026-02-02)
private static readonly CACHE_VERSION = 10; // Add file size to cache for faster validation (2026-02-02)

private diagnosticsPanel?: vscode.WebviewPanel;
// Tracks whether the diagnostics panel has already received its session files
Expand Down Expand Up @@ -314,16 +315,33 @@ class CopilotTokenTracker implements vscode.Disposable {
}

// Cache management methods
private isCacheValid(filePath: string, currentMtime: number): boolean {
/**
* Checks if the cache is valid for a file by comparing mtime and size.
* If the cache entry is missing size (old format), treat as invalid so it will be upgraded.
*/
private isCacheValid(filePath: string, currentMtime: number, currentSize: number): boolean {
const cached = this.sessionFileCache.get(filePath);
return cached !== undefined && cached.mtime === currentMtime;
if (!cached) {
return false;
}
// If size is missing (old cache), treat as invalid so it will be upgraded
if (typeof cached.size !== 'number') {
return false;
}
return cached.mtime === currentMtime && cached.size === currentSize;
}

private getCachedSessionData(filePath: string): SessionFileCache | undefined {
return this.sessionFileCache.get(filePath);
}

private setCachedSessionData(filePath: string, data: SessionFileCache): void {
/**
* Sets the cache entry for a session file, including file size.
*/
private setCachedSessionData(filePath: string, data: SessionFileCache, fileSize?: number): void {
if (typeof fileSize === 'number') {
data.size = fileSize;
}
this.sessionFileCache.set(filePath, data);

// Limit cache size to prevent memory issues (keep last 1000 files)
Expand Down Expand Up @@ -460,10 +478,9 @@ class CopilotTokenTracker implements vscode.Disposable {
// Smart initial update with delay for extension loading
this.scheduleInitialUpdate();

// Update every 5 minutes and save cache
// Update every 5 minutes (cache is saved automatically after each update)
this.updateInterval = setInterval(() => {
this.updateTokenStats(true); // Silent update from timer
this.saveCacheToStorage();
}, 5 * 60 * 1000);
}

Expand Down Expand Up @@ -579,6 +596,12 @@ class CopilotTokenTracker implements vscode.Disposable {
this.log(`Updated stats - Today: ${detailedStats.today.tokens}, Month: ${detailedStats.month.tokens}`);
// Store the stats for reuse without recalculation
this.lastDetailedStats = detailedStats;

// Save cache to ensure it's persisted for next run (don't await to avoid blocking UI)
this.saveCacheToStorage().catch(err => {
this.warn(`Failed to save cache: ${err}`);
});

return detailedStats;
} catch (error) {
this.error('Error updating token stats:', error);
Expand Down Expand Up @@ -606,7 +629,7 @@ class CopilotTokenTracker implements vscode.Disposable {

// Only process files modified in the current month
if (fileStats.mtime >= monthStart) {
const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime());
const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime(), fileStats.size);

monthTokens += tokens;

Expand Down Expand Up @@ -676,20 +699,20 @@ class CopilotTokenTracker implements vscode.Disposable {

// For files within current month, check if data is cached to avoid redundant reads
const mtime = fileStats.mtime.getTime();
const wasCached = this.isCacheValid(sessionFile, mtime);
const fileSize = fileStats.size;
const wasCached = this.isCacheValid(sessionFile, mtime, fileSize);

// Get interactions count (uses cache if available)
const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime);

const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime, fileSize);
// Skip empty sessions (no interactions = just opened chat panel, no messages sent)
if (interactions === 0) {
skippedFiles++;
continue;
}

// Get remaining data (all use cache if available)
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime);
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime);
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime, fileSize);
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime, fileSize);
const editorType = this.getEditorTypeFromPath(sessionFile);

// For date filtering, get lastInteraction from session details
Expand Down Expand Up @@ -873,9 +896,11 @@ class CopilotTokenTracker implements vscode.Disposable {

// Only process files modified in the last 30 days
if (fileStats.mtime >= thirtyDaysAgo) {
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());
const mtime = fileStats.mtime.getTime();
const fileSize = fileStats.size;
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime, fileSize);
const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime, fileSize);
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime, fileSize);
const editorType = this.getEditorTypeFromPath(sessionFile);

// Get the date in YYYY-MM-DD format
Expand Down Expand Up @@ -1020,7 +1045,9 @@ class CopilotTokenTracker implements vscode.Disposable {

// Check if file is within the last 30 days (widest range)
if (fileStats.mtime >= last30DaysStart) {
const analysis = await this.getUsageAnalysisFromSessionCached(sessionFile, fileStats.mtime.getTime());
const mtime = fileStats.mtime.getTime();
const fileSize = fileStats.size;
const analysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtime, fileSize);

// Add to last 30 days stats
last30DaysStats.sessions++;
Expand Down Expand Up @@ -1727,10 +1754,10 @@ class CopilotTokenTracker implements vscode.Disposable {
}

// Cached versions of session file reading methods
private async getSessionFileDataCached(sessionFilePath: string, mtime: number): Promise<SessionFileCache> {
private async getSessionFileDataCached(sessionFilePath: string, mtime: number, fileSize: number): Promise<SessionFileCache> {
// Check if we have valid cached data
const cached = this.getCachedSessionData(sessionFilePath);
if (cached && cached.mtime === mtime) {
if (cached && cached.mtime === mtime && cached.size === fileSize) {
this._cacheHits++;
return cached;
}
Expand All @@ -1750,27 +1777,27 @@ class CopilotTokenTracker implements vscode.Disposable {
usageAnalysis
};

this.setCachedSessionData(sessionFilePath, sessionData);
this.setCachedSessionData(sessionFilePath, sessionData, fileSize);
return sessionData;
}

private async estimateTokensFromSessionCached(sessionFilePath: string, mtime: number): Promise<number> {
const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime);
private async estimateTokensFromSessionCached(sessionFilePath: string, mtime: number, fileSize: number): Promise<number> {
const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime, fileSize);
return sessionData.tokens;
}

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

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

private async getUsageAnalysisFromSessionCached(sessionFile: string, mtime: number): Promise<SessionUsageAnalysis> {
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
private async getUsageAnalysisFromSessionCached(sessionFile: string, mtime: number, fileSize: number): Promise<SessionUsageAnalysis> {
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime, fileSize);
const analysis = sessionData.usageAnalysis || {
toolCalls: { total: 0, byTool: {} },
modeUsage: { ask: 0, edit: 0, agent: 0 },
Expand Down Expand Up @@ -2255,7 +2282,7 @@ class CopilotTokenTracker implements vscode.Disposable {
let usageAnalysis: SessionUsageAnalysis | undefined;
try {
const mtimeMs = new Date(details.modified).getTime();
usageAnalysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtimeMs);
usageAnalysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtimeMs, details.size);
} catch (usageError) {
this.warn(`Error loading usage analysis for ${sessionFile}: ${usageError}`);
}
Expand Down Expand Up @@ -4145,7 +4172,7 @@ export function activate(context: vscode.ExtensionContext) {
getCopilotSessionFiles: () => (tokenTracker as any).getCopilotSessionFiles(),
estimateTokensFromText: (text: string, model?: string) => (tokenTracker as any).estimateTokensFromText(text, model),
getModelFromRequest: (req: any) => (tokenTracker as any).getModelFromRequest(req),
getSessionFileDataCached: (p: string, m: number) => (tokenTracker as any).getSessionFileDataCached(p, m)
getSessionFileDataCached: (p: string, m: number, s: number) => (tokenTracker as any).getSessionFileDataCached(p, m, s)
});

const backendHandler = new BackendCommandHandler({
Expand Down
Loading