From ec883454e6bf8485539998b2a589b8b0c94593f4 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Singh <122798317+ankit3890@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:15:26 +0530 Subject: [PATCH] fix(cost-optimization): improve monthly cost estimation and prevent division by zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes an incorrect monthly cost projection in `CostOptimizationAnalyzer` and adds a defensive guard against a potential divide-by-zero. ## Problem `estimateMonthlyCost()` previously calculated: ```ts (totalCost / executionCount) * 30 ``` This assumes executions occur at a steady rate (~1/day), which isn't true for most workflows—especially idle ones. Since `detectIdleResources()` uses this value to populate `IdleResource.estimatedMonthlyCost`, it could significantly overestimate the savings reported by `terminate_idle` recommendations. ### Example - **2 executions** at **$1 each** over the course of a year - **Previous estimate:** `$30/month` - **Actual recent spend:** `$2/month` (or `$0/month` if no executions occurred in the last 30 days) As a result, long-idle or low-volume workflows could report inflated monthly savings. ## Fix - Updated `estimateMonthlyCost()` to sum the actual cost of executions within the trailing **30-day window** instead of extrapolating from lifetime averages. - Added an explicit guard when calculating `avgCostPerExecution` to safely handle potential divide-by-zero scenarios during future refactors. ## Impact - More accurate `IdleResource.estimatedMonthlyCost`. - More realistic `terminate_idle` recommendation savings. - `estimatedTotalMonthlySavings` now better reflects actual recent usage. - No API, type, or method signature changes. ## Testing - [ ] Verify sparse historical executions no longer produce inflated monthly costs. - [ ] Verify workflows with recent executions report actual 30-day spend. - [ ] Confirm empty execution datasets still return an efficiency score of **100** (regression check). ## Notes for Reviewers `estimateMonthlyCost()` filters using `created_at`, while idle detection uses `updated_at`. This is intentional, as cost should reflect **when an execution occurred** rather than its latest update time. Happy to align these semantics if a different interpretation is preferred. --- apps/pulse-metrics/src/cost-optimization.ts | 26 +++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/pulse-metrics/src/cost-optimization.ts b/apps/pulse-metrics/src/cost-optimization.ts index 512a9d0..9d7bdae 100644 --- a/apps/pulse-metrics/src/cost-optimization.ts +++ b/apps/pulse-metrics/src/cost-optimization.ts @@ -413,7 +413,10 @@ export class CostOptimizationAnalyzer { const successRate = succeeded / total; const totalCost = this.computeTotalCost(executions); - const avgCostPerExecution = totalCost / total; + // Guarded explicitly even though `total === 0` is handled above — protects + // against future refactors (e.g. this logic being extracted/reused + // elsewhere) that might drop that earlier guard. + const avgCostPerExecution = total ? totalCost / total : 0; const totalWorkflows = new Set(executions.map((e) => e.workflow_id)).size; const idleResourceRatio = totalWorkflows > 0 ? idleResources.length / totalWorkflows : 0; @@ -537,15 +540,30 @@ export class CostOptimizationAnalyzer { }, 0); } + /** + * Estimates the current monthly cost of a workflow using actual spend from + * the trailing 30-day window, rather than extrapolating from the + * all-time average cost per execution. Averaging over the full history and + * multiplying by 30 produces a number that has no relationship to real + * monthly spend once executions span more than ~a month (e.g. two + * executions total, both a year old, would still report a nonzero + * "monthly" cost under the old logic). + */ private estimateMonthlyCost( executions: ExecutionRecord[], usageData: Map, ): number { - const totalCost = executions.reduce((sum, exec) => { + const now = new Date(); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const recentExecutions = executions.filter( + (exec) => new Date(exec.created_at) >= thirtyDaysAgo, + ); + + return recentExecutions.reduce((sum, exec) => { const usage = usageData.get(exec.id); return sum + (usage?.totalCost ?? 0); }, 0); - return executions.length > 0 ? (totalCost / executions.length) * 30 : 0; } private analyzeModelUsage( @@ -577,4 +595,4 @@ export class CostOptimizationAnalyzer { return modelHierarchy[model]?.[0] ?? null; } -} \ No newline at end of file +}