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
177 changes: 109 additions & 68 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,23 @@ interface EditorUsage {
};
}

interface PeriodStats {
tokens: number;
sessions: number;
avgInteractionsPerSession: number;
avgTokensPerSession: number;
modelUsage: ModelUsage;
editorUsage: EditorUsage;
co2: number;
treesEquivalent: number;
waterUsage: number;
estimatedCost: number;
}

interface DetailedStats {
today: {
tokens: number;
sessions: number;
avgInteractionsPerSession: number;
avgTokensPerSession: number;
modelUsage: ModelUsage;
editorUsage: EditorUsage;
co2: number;
treesEquivalent: number;
waterUsage: number;
estimatedCost: number;
};
month: {
tokens: number;
sessions: number;
avgInteractionsPerSession: number;
avgTokensPerSession: number;
modelUsage: ModelUsage;
editorUsage: EditorUsage;
co2: number;
treesEquivalent: number;
waterUsage: number;
estimatedCost: number;
};
today: PeriodStats;
month: PeriodStats;
lastMonth: PeriodStats;
lastUpdated: Date;
}

Expand Down Expand Up @@ -368,7 +360,9 @@ class CopilotTokenTracker implements vscode.Disposable {

public async clearCache(): Promise<void> {
try {
this.log('[DEBUG] clearCache() called');
// Show the output channel so users can see what's happening
this.outputChannel.show(true);
this.log('DEBUG clearCache() called');
this.log('Clearing session file cache...');

const cacheSize = this.sessionFileCache.size;
Expand All @@ -377,6 +371,8 @@ class CopilotTokenTracker implements vscode.Disposable {
// Reset diagnostics loaded flag so the diagnostics view will reload files
this.diagnosticsHasLoadedFiles = false;
this.diagnosticsCachedFiles = [];
// Clear cached computed stats so details panel doesn't show stale data
this.lastDetailedStats = undefined;

this.log(`Cache cleared successfully. Removed ${cacheSize} entries.`);
vscode.window.showInformationMessage('Cache cleared successfully. Reloading statistics...');
Expand Down Expand Up @@ -596,9 +592,13 @@ class CopilotTokenTracker implements vscode.Disposable {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
// Calculate last month boundaries
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); // Last day of previous month
const lastMonthStart = new Date(lastMonthEnd.getFullYear(), lastMonthEnd.getMonth(), 1);

const todayStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
const monthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
const lastMonthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };

try {
// Clean expired cache entries
Expand Down Expand Up @@ -626,9 +626,9 @@ class CopilotTokenTracker implements vscode.Disposable {
// Fast check: Get file stats first to avoid processing old files
const fileStats = fs.statSync(sessionFile);

// Skip files modified before the current month (quick filter)
// Skip files modified before last month (quick filter)
// This is the main performance optimization - filters out old sessions without reading file content
if (fileStats.mtime < monthStart) {
if (fileStats.mtime < lastMonthStart) {
skippedFiles++;
continue;
}
Expand Down Expand Up @@ -708,18 +708,46 @@ class CopilotTokenTracker implements vscode.Disposable {
}
}
}
else if (lastActivity >= lastMonthStart && lastActivity <= lastMonthEnd) {
// Session is from last month - only track lastMonth stats
if (wasCached) {
cacheHits++;
} else {
cacheMisses++;
}

lastMonthStats.tokens += tokens;
lastMonthStats.sessions += 1;
lastMonthStats.interactions += interactions;

// Add editor usage to last month stats
if (!lastMonthStats.editorUsage[editorType]) {
lastMonthStats.editorUsage[editorType] = { tokens: 0, sessions: 0 };
}
lastMonthStats.editorUsage[editorType].tokens += tokens;
lastMonthStats.editorUsage[editorType].sessions += 1;

// Add model usage to last month stats
for (const [model, usage] of Object.entries(modelUsage)) {
if (!lastMonthStats.modelUsage[model]) {
lastMonthStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
}
lastMonthStats.modelUsage[model].inputTokens += usage.inputTokens;
lastMonthStats.modelUsage[model].outputTokens += usage.outputTokens;
}
}
else {
// Session is too old (no activity in current month), skip it
// Session is too old (no activity in current or last month), skip it
skippedFiles++;
}
} catch (fileError) {
this.warn(`Error processing session file ${sessionFile}: ${fileError}`);
}
}

this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions`);
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last Month ${lastMonthStats.sessions} sessions`);
if (skippedFiles > 0) {
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in current month)`);
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in recent months)`);
}
const totalCacheAccesses = cacheHits + cacheMisses;
this.log(`💾 Cache performance: ${cacheHits} hits, ${cacheMisses} misses (${totalCacheAccesses > 0 ? ((cacheHits / totalCacheAccesses) * 100).toFixed(1) : 0}% hit rate)`);
Expand All @@ -729,12 +757,15 @@ class CopilotTokenTracker implements vscode.Disposable {

const todayCo2 = (todayStats.tokens / 1000) * this.co2Per1kTokens;
const monthCo2 = (monthStats.tokens / 1000) * this.co2Per1kTokens;
const lastMonthCo2 = (lastMonthStats.tokens / 1000) * this.co2Per1kTokens;

const todayWater = (todayStats.tokens / 1000) * this.waterUsagePer1kTokens;
const monthWater = (monthStats.tokens / 1000) * this.waterUsagePer1kTokens;
const lastMonthWater = (lastMonthStats.tokens / 1000) * this.waterUsagePer1kTokens;

const todayCost = this.calculateEstimatedCost(todayStats.modelUsage);
const monthCost = this.calculateEstimatedCost(monthStats.modelUsage);
const lastMonthCost = this.calculateEstimatedCost(lastMonthStats.modelUsage);

const result: DetailedStats = {
today: {
Expand All @@ -761,6 +792,18 @@ class CopilotTokenTracker implements vscode.Disposable {
waterUsage: monthWater,
estimatedCost: monthCost
},
lastMonth: {
tokens: lastMonthStats.tokens,
sessions: lastMonthStats.sessions,
avgInteractionsPerSession: lastMonthStats.sessions > 0 ? Math.round(lastMonthStats.interactions / lastMonthStats.sessions) : 0,
avgTokensPerSession: lastMonthStats.sessions > 0 ? Math.round(lastMonthStats.tokens / lastMonthStats.sessions) : 0,
modelUsage: lastMonthStats.modelUsage,
editorUsage: lastMonthStats.editorUsage,
co2: lastMonthCo2,
treesEquivalent: lastMonthCo2 / this.co2AbsorptionPerTreePerYear,
waterUsage: lastMonthWater,
estimatedCost: lastMonthCost
},
lastUpdated: now
};

Expand All @@ -773,7 +816,8 @@ class CopilotTokenTracker implements vscode.Disposable {

private async calculateDailyStats(): Promise<DailyTokenStats[]> {
const now = new Date();
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
// Use last 30 days instead of current month for better chart visibility
const thirtyDaysAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30);

// Map to store daily stats by date string (YYYY-MM-DD)
const dailyStatsMap = new Map<string, DailyTokenStats>();
Expand All @@ -786,8 +830,8 @@ class CopilotTokenTracker implements vscode.Disposable {
try {
const fileStats = fs.statSync(sessionFile);

// Only process files modified in the current month
if (fileStats.mtime >= monthStart) {
// 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());
Expand Down Expand Up @@ -840,42 +884,39 @@ class CopilotTokenTracker implements vscode.Disposable {
// Convert map to array and sort by date
let dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));

// Fill in missing dates between the first date and today
if (dailyStatsArray.length > 0) {
const firstDate = new Date(dailyStatsArray[0].date);
const today = new Date();

// Create a set of existing dates for quick lookup
const existingDates = new Set(dailyStatsArray.map(s => s.date));

// Generate all dates from first date to today
const allDates: string[] = [];
const currentDate = new Date(firstDate);

while (currentDate <= today) {
const dateKey = this.formatDateKey(currentDate);
allDates.push(dateKey);
currentDate.setDate(currentDate.getDate() + 1);
}

// Add missing dates with zero values
for (const dateKey of allDates) {
if (!existingDates.has(dateKey)) {
dailyStatsMap.set(dateKey, {
date: dateKey,
tokens: 0,
sessions: 0,
interactions: 0,
modelUsage: {},
editorUsage: {}
});
}
// Always fill in all 30 days to show complete chart
const today = new Date();

// Create a set of existing dates for quick lookup
const existingDates = new Set(dailyStatsArray.map(s => s.date));

// Generate all dates from 30 days ago to today
const allDates: string[] = [];
const currentDate = new Date(thirtyDaysAgo);

while (currentDate <= today) {
const dateKey = this.formatDateKey(currentDate);
allDates.push(dateKey);
currentDate.setDate(currentDate.getDate() + 1);
}

// Add missing dates with zero values
for (const dateKey of allDates) {
if (!existingDates.has(dateKey)) {
dailyStatsMap.set(dateKey, {
date: dateKey,
tokens: 0,
sessions: 0,
interactions: 0,
modelUsage: {},
editorUsage: {}
});
}

// Re-convert map to array and sort by date
dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
}

// Re-convert map to array and sort by date
dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));

return dailyStatsArray;
}

Expand Down Expand Up @@ -3192,7 +3233,7 @@ class CopilotTokenTracker implements vscode.Disposable {

// Handle messages from the webview
this.diagnosticsPanel.webview.onDidReceiveMessage(async (message) => {
this.log(`Diagnostics webview message: ${JSON.stringify(message)}`);
this.log(`DEBUG Diagnostics webview message: ${JSON.stringify(message)}`);
switch (message.command) {
case 'copyReport':
await vscode.env.clipboard.writeText(report);
Expand Down Expand Up @@ -3252,7 +3293,7 @@ class CopilotTokenTracker implements vscode.Disposable {
await this.showUsageAnalysis();
break;
case 'clearCache':
this.log('[DEBUG] clearCache message received from diagnostics webview');
this.log('DEBUG clearCache message received from diagnostics webview');
await this.clearCache();
// After clearing cache, refresh the diagnostic report if it's open
if (this.diagnosticsPanel) {
Expand Down
4 changes: 2 additions & 2 deletions src/webview/chart/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function renderLayout(data: InitialChartData): void {
const header = el('div', 'header');
const headerLeft = el('div', 'header-left');
const icon = el('span', 'header-icon', '📈');
const title = el('span', 'header-title', 'Token Usage Over Time');
const title = el('span', 'header-title', 'Token Usage - Last 30 Days');
headerLeft.append(icon, title);
const buttons = el('div', 'button-row');
buttons.append(
Expand Down Expand Up @@ -140,7 +140,7 @@ function renderLayout(data: InitialChartData): void {
chartShell.append(toggles, canvasWrap);
chartSection.append(chartShell);

const footer = el('div', 'footer', `Day-by-day token usage for the current month\nLast updated: ${new Date(data.lastUpdated).toLocaleString()}\nUpdates automatically every 5 minutes.`);
const footer = el('div', 'footer', `Day-by-day token usage for the last 30 days\nLast updated: ${new Date(data.lastUpdated).toLocaleString()}\nUpdates automatically every 5 minutes.`);

container.append(header, summarySection, chartSection, footer);
root.append(style, container);
Expand Down
Loading
Loading