@@ -37,12 +37,20 @@ interface DetailedStats {
3737 lastUpdated : Date ;
3838}
3939
40+ interface SessionFileCache {
41+ tokens : number ;
42+ interactions : number ;
43+ modelUsage : ModelUsage ;
44+ mtime : number ; // file modification time as timestamp
45+ }
46+
4047class CopilotTokenTracker implements vscode . Disposable {
4148 private statusBarItem : vscode . StatusBarItem ;
4249 private updateInterval : NodeJS . Timeout | undefined ;
4350 private initialDelayTimeout : NodeJS . Timeout | undefined ;
4451 private detailsPanel : vscode . WebviewPanel | undefined ;
4552 private outputChannel : vscode . OutputChannel ;
53+ private sessionFileCache : Map < string , SessionFileCache > = new Map ( ) ;
4654 private tokenEstimators : { [ key : string ] : number } = {
4755 'gpt-4' : 0.25 ,
4856 'gpt-4.1' : 0.25 ,
@@ -81,6 +89,45 @@ class CopilotTokenTracker implements vscode.Disposable {
8189 }
8290 }
8391
92+ // Cache management methods
93+ private isCacheValid ( filePath : string , currentMtime : number ) : boolean {
94+ const cached = this . sessionFileCache . get ( filePath ) ;
95+ return cached !== undefined && cached . mtime === currentMtime ;
96+ }
97+
98+ private getCachedSessionData ( filePath : string ) : SessionFileCache | undefined {
99+ return this . sessionFileCache . get ( filePath ) ;
100+ }
101+
102+ private setCachedSessionData ( filePath : string , data : SessionFileCache ) : void {
103+ this . sessionFileCache . set ( filePath , data ) ;
104+
105+ // Limit cache size to prevent memory issues (keep last 1000 files)
106+ if ( this . sessionFileCache . size > 1000 ) {
107+ const entries = Array . from ( this . sessionFileCache . entries ( ) ) ;
108+ // Remove oldest entries (simple FIFO approach)
109+ const toRemove = entries . slice ( 0 , this . sessionFileCache . size - 1000 ) ;
110+ for ( const [ key ] of toRemove ) {
111+ this . sessionFileCache . delete ( key ) ;
112+ }
113+ }
114+ }
115+
116+ private clearExpiredCache ( ) : void {
117+ // Remove cache entries for files that no longer exist
118+ const filesToCheck = Array . from ( this . sessionFileCache . keys ( ) ) ;
119+ for ( const filePath of filesToCheck ) {
120+ try {
121+ if ( ! fs . existsSync ( filePath ) ) {
122+ this . sessionFileCache . delete ( filePath ) ;
123+ }
124+ } catch ( error ) {
125+ // File access error, remove from cache
126+ this . sessionFileCache . delete ( filePath ) ;
127+ }
128+ }
129+ }
130+
84131
85132
86133 constructor ( ) {
@@ -234,7 +281,7 @@ class CopilotTokenTracker implements vscode.Disposable {
234281
235282 // Only process files modified in the current month
236283 if ( fileStats . mtime >= monthStart ) {
237- const tokens = await this . estimateTokensFromSession ( sessionFile ) ;
284+ const tokens = await this . estimateTokensFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
238285
239286 monthTokens += tokens ;
240287
@@ -266,21 +313,37 @@ class CopilotTokenTracker implements vscode.Disposable {
266313 const monthStats = { tokens : 0 , sessions : 0 , interactions : 0 , modelUsage : { } as ModelUsage } ;
267314
268315 try {
316+ // Clean expired cache entries
317+ this . clearExpiredCache ( ) ;
318+
269319 const sessionFiles = await this . getCopilotSessionFiles ( ) ;
270320 this . log ( `Processing ${ sessionFiles . length } session files for detailed stats` ) ;
271321
272322 if ( sessionFiles . length === 0 ) {
273323 this . warn ( 'No session files found - this might indicate an issue in GitHub Codespaces or different VS Code configuration' ) ;
274324 }
275325
326+ let cacheHits = 0 ;
327+ let cacheMisses = 0 ;
328+
276329 for ( const sessionFile of sessionFiles ) {
277330 try {
278331 const fileStats = fs . statSync ( sessionFile ) ;
279332
280333 if ( fileStats . mtime >= monthStart ) {
281- const tokens = await this . estimateTokensFromSession ( sessionFile ) ;
282- const interactions = await this . countInteractionsInSession ( sessionFile ) ;
283- const modelUsage = await this . getModelUsageFromSession ( sessionFile ) ;
334+ // Check if data is cached before making calls
335+ const wasCached = this . isCacheValid ( sessionFile , fileStats . mtime . getTime ( ) ) ;
336+
337+ const tokens = await this . estimateTokensFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
338+ const interactions = await this . countInteractionsInSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
339+ const modelUsage = await this . getModelUsageFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
340+
341+ // Update cache statistics
342+ if ( wasCached ) {
343+ cacheHits ++ ;
344+ } else {
345+ cacheMisses ++ ;
346+ }
284347
285348 this . log ( `Session ${ path . basename ( sessionFile ) } : ${ tokens } tokens, ${ interactions } interactions` ) ;
286349
@@ -308,6 +371,8 @@ class CopilotTokenTracker implements vscode.Disposable {
308371 this . warn ( `Error processing session file ${ sessionFile } : ${ fileError } ` ) ;
309372 }
310373 }
374+
375+ this . log ( `Cache performance - Hits: ${ cacheHits } , Misses: ${ cacheMisses } , Hit Rate: ${ sessionFiles . length > 0 ? ( ( cacheHits / sessionFiles . length ) * 100 ) . toFixed ( 1 ) : 0 } %` ) ;
311376 } catch ( error ) {
312377 this . error ( 'Error calculating detailed stats:' , error ) ;
313378 }
@@ -404,6 +469,45 @@ class CopilotTokenTracker implements vscode.Disposable {
404469 return modelUsage ;
405470 }
406471
472+ // Cached versions of session file reading methods
473+ private async getSessionFileDataCached ( sessionFilePath : string , mtime : number ) : Promise < SessionFileCache > {
474+ // Check if we have valid cached data
475+ const cached = this . getCachedSessionData ( sessionFilePath ) ;
476+ if ( cached && cached . mtime === mtime ) {
477+ return cached ;
478+ }
479+
480+ // Cache miss - read and process the file once to get all data
481+ const tokens = await this . estimateTokensFromSession ( sessionFilePath ) ;
482+ const interactions = await this . countInteractionsInSession ( sessionFilePath ) ;
483+ const modelUsage = await this . getModelUsageFromSession ( sessionFilePath ) ;
484+
485+ const sessionData : SessionFileCache = {
486+ tokens,
487+ interactions,
488+ modelUsage,
489+ mtime
490+ } ;
491+
492+ this . setCachedSessionData ( sessionFilePath , sessionData ) ;
493+ return sessionData ;
494+ }
495+
496+ private async estimateTokensFromSessionCached ( sessionFilePath : string , mtime : number ) : Promise < number > {
497+ const sessionData = await this . getSessionFileDataCached ( sessionFilePath , mtime ) ;
498+ return sessionData . tokens ;
499+ }
500+
501+ private async countInteractionsInSessionCached ( sessionFile : string , mtime : number ) : Promise < number > {
502+ const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime ) ;
503+ return sessionData . interactions ;
504+ }
505+
506+ private async getModelUsageFromSessionCached ( sessionFile : string , mtime : number ) : Promise < ModelUsage > {
507+ const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime ) ;
508+ return sessionData . modelUsage ;
509+ }
510+
407511 private checkCopilotExtension ( ) : void {
408512 this . log ( 'Checking GitHub Copilot extension status' ) ;
409513
@@ -1155,6 +1259,8 @@ class CopilotTokenTracker implements vscode.Disposable {
11551259 }
11561260 this . statusBarItem . dispose ( ) ;
11571261 this . outputChannel . dispose ( ) ;
1262+ // Clear cache on disposal
1263+ this . sessionFileCache . clear ( ) ;
11581264 }
11591265}
11601266
0 commit comments