Skip to content

Commit 7735255

Browse files
authored
Merge pull request #171 from rajbos/cache-fix
Fix cache loading
2 parents e196fe1 + f646fd4 commit 7735255

File tree

3 files changed

+62
-32
lines changed

3 files changed

+62
-32
lines changed

src/backend/facade.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface BackendFacadeDeps {
3434
estimateTokensFromText: (text: string, model: string) => number;
3535
getModelFromRequest: (request: ChatRequest) => string;
3636
// Cache integration for performance
37-
getSessionFileDataCached?: (sessionFilePath: string, mtime: number) => Promise<SessionFileCache>;
37+
getSessionFileDataCached?: (sessionFilePath: string, mtime: number, fileSize: number) => Promise<SessionFileCache>;
3838
}
3939

4040
export class BackendFacade {

src/backend/services/syncService.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface SyncServiceDeps {
5959
estimateTokensFromText: (text: string, model: string) => number;
6060
getModelFromRequest: (request: ChatRequest) => string;
6161
// Cache integration for performance
62-
getSessionFileDataCached?: (sessionFilePath: string, mtime: number) => Promise<SessionFileCache>;
62+
getSessionFileDataCached?: (sessionFilePath: string, mtime: number, fileSize: number) => Promise<SessionFileCache>;
6363
// UI refresh callback after successful sync
6464
updateTokenStats?: () => Promise<void>;
6565
}
@@ -176,6 +176,7 @@ export class SyncService {
176176
private async processCachedSessionFile(
177177
sessionFile: string,
178178
fileMtimeMs: number,
179+
fileSize: number,
179180
workspaceId: string,
180181
machineId: string,
181182
userId: string | undefined,
@@ -184,7 +185,7 @@ export class SyncService {
184185
now: Date
185186
): Promise<boolean> {
186187
try {
187-
const cachedData = await this.deps.getSessionFileDataCached!(sessionFile, fileMtimeMs);
188+
const cachedData = await this.deps.getSessionFileDataCached!(sessionFile, fileMtimeMs, fileSize);
188189

189190
// Validate cached data structure to prevent injection/corruption
190191
if (!cachedData || typeof cachedData !== 'object') {
@@ -450,9 +451,11 @@ export class SyncService {
450451
// Note: We still parse the file to get accurate day keys from timestamps,
451452
// but use cached token counts for performance
452453
if (useCachedData) {
454+
const fileStat = await fs.promises.stat(sessionFile);
453455
const cacheSuccess = await this.processCachedSessionFile(
454456
sessionFile,
455457
fileMtimeMs,
458+
fileStat.size,
456459
workspaceId,
457460
machineId,
458461
userId,

src/extension.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ interface SessionFileCache {
7272
interactions: number;
7373
modelUsage: ModelUsage;
7474
mtime: number; // file modification time as timestamp
75+
size?: number; // file size in bytes (optional for backward compatibility)
7576
usageAnalysis?: SessionUsageAnalysis; // New analysis data
7677
}
7778

@@ -195,7 +196,7 @@ interface SessionLogData {
195196

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

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

316317
// Cache management methods
317-
private isCacheValid(filePath: string, currentMtime: number): boolean {
318+
/**
319+
* Checks if the cache is valid for a file by comparing mtime and size.
320+
* If the cache entry is missing size (old format), treat as invalid so it will be upgraded.
321+
*/
322+
private isCacheValid(filePath: string, currentMtime: number, currentSize: number): boolean {
318323
const cached = this.sessionFileCache.get(filePath);
319-
return cached !== undefined && cached.mtime === currentMtime;
324+
if (!cached) {
325+
return false;
326+
}
327+
// If size is missing (old cache), treat as invalid so it will be upgraded
328+
if (typeof cached.size !== 'number') {
329+
return false;
330+
}
331+
return cached.mtime === currentMtime && cached.size === currentSize;
320332
}
321333

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

326-
private setCachedSessionData(filePath: string, data: SessionFileCache): void {
338+
/**
339+
* Sets the cache entry for a session file, including file size.
340+
*/
341+
private setCachedSessionData(filePath: string, data: SessionFileCache, fileSize?: number): void {
342+
if (typeof fileSize === 'number') {
343+
data.size = fileSize;
344+
}
327345
this.sessionFileCache.set(filePath, data);
328346

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

463-
// Update every 5 minutes and save cache
481+
// Update every 5 minutes (cache is saved automatically after each update)
464482
this.updateInterval = setInterval(() => {
465483
this.updateTokenStats(true); // Silent update from timer
466-
this.saveCacheToStorage();
467484
}, 5 * 60 * 1000);
468485
}
469486

@@ -579,6 +596,12 @@ class CopilotTokenTracker implements vscode.Disposable {
579596
this.log(`Updated stats - Today: ${detailedStats.today.tokens}, Month: ${detailedStats.month.tokens}`);
580597
// Store the stats for reuse without recalculation
581598
this.lastDetailedStats = detailedStats;
599+
600+
// Save cache to ensure it's persisted for next run (don't await to avoid blocking UI)
601+
this.saveCacheToStorage().catch(err => {
602+
this.warn(`Failed to save cache: ${err}`);
603+
});
604+
582605
return detailedStats;
583606
} catch (error) {
584607
this.error('Error updating token stats:', error);
@@ -606,7 +629,7 @@ class CopilotTokenTracker implements vscode.Disposable {
606629

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

611634
monthTokens += tokens;
612635

@@ -676,20 +699,20 @@ class CopilotTokenTracker implements vscode.Disposable {
676699

677700
// For files within current month, check if data is cached to avoid redundant reads
678701
const mtime = fileStats.mtime.getTime();
679-
const wasCached = this.isCacheValid(sessionFile, mtime);
702+
const fileSize = fileStats.size;
703+
const wasCached = this.isCacheValid(sessionFile, mtime, fileSize);
680704

681705
// Get interactions count (uses cache if available)
682-
const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime);
683-
706+
const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime, fileSize);
684707
// Skip empty sessions (no interactions = just opened chat panel, no messages sent)
685708
if (interactions === 0) {
686709
skippedFiles++;
687710
continue;
688711
}
689712

690713
// Get remaining data (all use cache if available)
691-
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime);
692-
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime);
714+
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime, fileSize);
715+
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime, fileSize);
693716
const editorType = this.getEditorTypeFromPath(sessionFile);
694717

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

874897
// Only process files modified in the last 30 days
875898
if (fileStats.mtime >= thirtyDaysAgo) {
876-
const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime());
877-
const interactions = await this.countInteractionsInSessionCached(sessionFile, fileStats.mtime.getTime());
878-
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, fileStats.mtime.getTime());
899+
const mtime = fileStats.mtime.getTime();
900+
const fileSize = fileStats.size;
901+
const tokens = await this.estimateTokensFromSessionCached(sessionFile, mtime, fileSize);
902+
const interactions = await this.countInteractionsInSessionCached(sessionFile, mtime, fileSize);
903+
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, mtime, fileSize);
879904
const editorType = this.getEditorTypeFromPath(sessionFile);
880905

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

10211046
// Check if file is within the last 30 days (widest range)
10221047
if (fileStats.mtime >= last30DaysStart) {
1023-
const analysis = await this.getUsageAnalysisFromSessionCached(sessionFile, fileStats.mtime.getTime());
1048+
const mtime = fileStats.mtime.getTime();
1049+
const fileSize = fileStats.size;
1050+
const analysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtime, fileSize);
10241051

10251052
// Add to last 30 days stats
10261053
last30DaysStats.sessions++;
@@ -1727,10 +1754,10 @@ class CopilotTokenTracker implements vscode.Disposable {
17271754
}
17281755

17291756
// Cached versions of session file reading methods
1730-
private async getSessionFileDataCached(sessionFilePath: string, mtime: number): Promise<SessionFileCache> {
1757+
private async getSessionFileDataCached(sessionFilePath: string, mtime: number, fileSize: number): Promise<SessionFileCache> {
17311758
// Check if we have valid cached data
17321759
const cached = this.getCachedSessionData(sessionFilePath);
1733-
if (cached && cached.mtime === mtime) {
1760+
if (cached && cached.mtime === mtime && cached.size === fileSize) {
17341761
this._cacheHits++;
17351762
return cached;
17361763
}
@@ -1750,27 +1777,27 @@ class CopilotTokenTracker implements vscode.Disposable {
17501777
usageAnalysis
17511778
};
17521779

1753-
this.setCachedSessionData(sessionFilePath, sessionData);
1780+
this.setCachedSessionData(sessionFilePath, sessionData, fileSize);
17541781
return sessionData;
17551782
}
17561783

1757-
private async estimateTokensFromSessionCached(sessionFilePath: string, mtime: number): Promise<number> {
1758-
const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime);
1784+
private async estimateTokensFromSessionCached(sessionFilePath: string, mtime: number, fileSize: number): Promise<number> {
1785+
const sessionData = await this.getSessionFileDataCached(sessionFilePath, mtime, fileSize);
17591786
return sessionData.tokens;
17601787
}
17611788

1762-
private async countInteractionsInSessionCached(sessionFile: string, mtime: number): Promise<number> {
1763-
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
1789+
private async countInteractionsInSessionCached(sessionFile: string, mtime: number, fileSize: number): Promise<number> {
1790+
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime, fileSize);
17641791
return sessionData.interactions;
17651792
}
17661793

1767-
private async getModelUsageFromSessionCached(sessionFile: string, mtime: number): Promise<ModelUsage> {
1768-
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
1794+
private async getModelUsageFromSessionCached(sessionFile: string, mtime: number, fileSize: number): Promise<ModelUsage> {
1795+
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime, fileSize);
17691796
return sessionData.modelUsage;
17701797
}
17711798

1772-
private async getUsageAnalysisFromSessionCached(sessionFile: string, mtime: number): Promise<SessionUsageAnalysis> {
1773-
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime);
1799+
private async getUsageAnalysisFromSessionCached(sessionFile: string, mtime: number, fileSize: number): Promise<SessionUsageAnalysis> {
1800+
const sessionData = await this.getSessionFileDataCached(sessionFile, mtime, fileSize);
17741801
const analysis = sessionData.usageAnalysis || {
17751802
toolCalls: { total: 0, byTool: {} },
17761803
modeUsage: { ask: 0, edit: 0, agent: 0 },
@@ -2255,7 +2282,7 @@ class CopilotTokenTracker implements vscode.Disposable {
22552282
let usageAnalysis: SessionUsageAnalysis | undefined;
22562283
try {
22572284
const mtimeMs = new Date(details.modified).getTime();
2258-
usageAnalysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtimeMs);
2285+
usageAnalysis = await this.getUsageAnalysisFromSessionCached(sessionFile, mtimeMs, details.size);
22592286
} catch (usageError) {
22602287
this.warn(`Error loading usage analysis for ${sessionFile}: ${usageError}`);
22612288
}
@@ -4145,7 +4172,7 @@ export function activate(context: vscode.ExtensionContext) {
41454172
getCopilotSessionFiles: () => (tokenTracker as any).getCopilotSessionFiles(),
41464173
estimateTokensFromText: (text: string, model?: string) => (tokenTracker as any).estimateTokensFromText(text, model),
41474174
getModelFromRequest: (req: any) => (tokenTracker as any).getModelFromRequest(req),
4148-
getSessionFileDataCached: (p: string, m: number) => (tokenTracker as any).getSessionFileDataCached(p, m)
4175+
getSessionFileDataCached: (p: string, m: number, s: number) => (tokenTracker as any).getSessionFileDataCached(p, m, s)
41494176
});
41504177

41514178
const backendHandler = new BackendCommandHandler({

0 commit comments

Comments
 (0)