diff --git a/Lite/Analysis/AnomalyDetector.cs b/Lite/Analysis/AnomalyDetector.cs index 73c728ef..ab520fd9 100644 --- a/Lite/Analysis/AnomalyDetector.cs +++ b/Lite/Analysis/AnomalyDetector.cs @@ -260,7 +260,12 @@ ORDER BY c.total_ms DESC var currentMs = Convert.ToInt64(reader.GetValue(1)); var baselineMs = Convert.ToInt64(reader.GetValue(2)); - // New wait (absent in baseline) or 5x+ increase + // Normalize to per-hour rates before comparing (windows are different lengths) + var baselineHours = (baselineEnd - baselineStart).TotalHours; + var currentHours = (context.TimeRangeEnd - context.TimeRangeStart).TotalHours; + if (baselineHours <= 0) baselineHours = 1; + if (currentHours <= 0) currentHours = 1; + double ratio; string anomalyType; @@ -271,7 +276,9 @@ ORDER BY c.total_ms DESC } else { - ratio = (double)currentMs / baselineMs; + var baselineRate = baselineMs / baselineHours; + var currentRate = currentMs / currentHours; + ratio = baselineRate > 0 ? currentRate / baselineRate : 100.0; anomalyType = "spike"; } @@ -353,8 +360,22 @@ private async Task DetectBlockingAnomalies(AnalysisContext context, var baselineDeadlocks = Convert.ToInt64(reader.GetValue(2)); var currentDeadlocks = Convert.ToInt64(reader.GetValue(3)); - // Blocking spike: at least 5 events AND 3x baseline (or new) - if (currentBlocking >= 5 && (baselineBlocking == 0 || (double)currentBlocking / baselineBlocking >= 3)) + // Normalize to per-hour rates (windows are different lengths) + var baselineHours = (baselineEnd - baselineStart).TotalHours; + var currentHours = (context.TimeRangeEnd - context.TimeRangeStart).TotalHours; + if (baselineHours <= 0) baselineHours = 1; + if (currentHours <= 0) currentHours = 1; + + var baselineBlockingRate = baselineBlocking / baselineHours; + var currentBlockingRate = currentBlocking / currentHours; + var blockingRatio = baselineBlocking > 0 ? currentBlockingRate / baselineBlockingRate : 100.0; + + var baselineDeadlockRate = baselineDeadlocks / baselineHours; + var currentDeadlockRate = currentDeadlocks / currentHours; + var deadlockRatio = baselineDeadlocks > 0 ? currentDeadlockRate / baselineDeadlockRate : 100.0; + + // Blocking spike: at least 5 events AND 3x baseline rate (or new) + if (currentBlocking >= 5 && (baselineBlocking == 0 || blockingRatio >= 3)) { anomalies.Add(new Fact { @@ -366,13 +387,13 @@ private async Task DetectBlockingAnomalies(AnalysisContext context, { ["current_count"] = currentBlocking, ["baseline_count"] = baselineBlocking, - ["ratio"] = baselineBlocking > 0 ? (double)currentBlocking / baselineBlocking : 100 + ["ratio"] = blockingRatio } }); } - // Deadlock spike: at least 3 events AND 3x baseline (or new) - if (currentDeadlocks >= 3 && (baselineDeadlocks == 0 || (double)currentDeadlocks / baselineDeadlocks >= 3)) + // Deadlock spike: at least 3 events AND 3x baseline rate (or new) + if (currentDeadlocks >= 3 && (baselineDeadlocks == 0 || deadlockRatio >= 3)) { anomalies.Add(new Fact { @@ -384,7 +405,7 @@ private async Task DetectBlockingAnomalies(AnalysisContext context, { ["current_count"] = currentDeadlocks, ["baseline_count"] = baselineDeadlocks, - ["ratio"] = baselineDeadlocks > 0 ? (double)currentDeadlocks / baselineDeadlocks : 100 + ["ratio"] = deadlockRatio } }); } diff --git a/Lite/Controls/FinOpsTab.xaml.cs b/Lite/Controls/FinOpsTab.xaml.cs index fc2ba10b..93862f8c 100644 --- a/Lite/Controls/FinOpsTab.xaml.cs +++ b/Lite/Controls/FinOpsTab.xaml.cs @@ -578,7 +578,7 @@ private async System.Threading.Tasks.Task LoadHighImpactQueriesAsync(int serverI { var hoursBack = GetHighImpactHoursBack(); var data = await _dataService.GetHighImpactQueriesAsync(serverId, hoursBack); - HighImpactDataGrid.ItemsSource = data; + _highImpactFilterMgr!.UpdateData(data); HighImpactNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; HighImpactCountIndicator.Text = data.Count > 0 ? $"{data.Count} high-impact query(s)" : ""; } diff --git a/Lite/Controls/PlanViewerControl.xaml.cs b/Lite/Controls/PlanViewerControl.xaml.cs index b4f0875b..b740cde6 100644 --- a/Lite/Controls/PlanViewerControl.xaml.cs +++ b/Lite/Controls/PlanViewerControl.xaml.cs @@ -353,16 +353,17 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1) HorizontalAlignment = HorizontalAlignment.Center }); - // Actual rows of Estimated rows (accuracy %) — red if off by 10x+ + // Actual rows per execution vs Estimated rows (accuracy %) — red if off by 10x+ var estRows = node.EstimateRows; - var accuracyRatio = estRows > 0 ? node.ActualRows / estRows : (node.ActualRows > 0 ? double.MaxValue : 1.0); + var actualRowsPerExec = node.ActualExecutions > 0 ? node.ActualRows / (double)node.ActualExecutions : node.ActualRows; + var accuracyRatio = estRows > 0 ? actualRowsPerExec / estRows : (actualRowsPerExec > 0 ? double.MaxValue : 1.0); var rowBrush = (accuracyRatio < 0.1 || accuracyRatio > 10.0) ? Brushes.OrangeRed : fgBrush; var accuracy = estRows > 0 ? $" ({accuracyRatio * 100:F0}%)" : ""; stack.Children.Add(new TextBlock { - Text = $"{node.ActualRows:N0} of {estRows:N0}{accuracy}", + Text = $"{actualRowsPerExec:N0} of {estRows:N0}{accuracy}", FontSize = 9, Foreground = rowBrush, TextAlignment = TextAlignment.Center, diff --git a/Lite/Services/LocalDataService.DailySummary.cs b/Lite/Services/LocalDataService.DailySummary.cs index eb2acce5..3664c699 100644 --- a/Lite/Services/LocalDataService.DailySummary.cs +++ b/Lite/Services/LocalDataService.DailySummary.cs @@ -42,7 +42,8 @@ FROM v_wait_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time < $3 AND delta_wait_time_ms > 0 - ORDER BY delta_wait_time_ms DESC + GROUP BY wait_type + ORDER BY SUM(delta_wait_time_ms) DESC LIMIT 1 ) AS top_wait_type, COALESCE( diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index 5a744c91..7ebca428 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -171,11 +171,12 @@ FROM v_wait_stats WHERE server_id = $1 AND wait_type IN ('THREADPOOL', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE') AND delta_waiting_tasks > 0 -AND collection_time >= NOW() - INTERVAL '10 minutes' +AND collection_time >= $2 ORDER BY collection_time DESC LIMIT 3"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); + command.Parameters.Add(new DuckDBParameter { Value = DateTime.UtcNow.AddMinutes(-10) }); var items = new List(); using var reader = await command.ExecuteReaderAsync(); diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs index 8366eb3c..f96fcc81 100644 --- a/Lite/Windows/SettingsWindow.xaml.cs +++ b/Lite/Windows/SettingsWindow.xaml.cs @@ -861,25 +861,28 @@ private void ValidateSmtpButton_Click(object sender, RoutedEventArgs e) private async void TestEmailButton_Click(object sender, RoutedEventArgs e) { - /* Temporarily apply current UI values for the test */ - App.SmtpServer = SmtpServerBox.Text?.Trim() ?? ""; - if (int.TryParse(SmtpPortBox.Text, out var port)) - App.SmtpPort = port; - App.SmtpUseSsl = SmtpSslCheckBox.IsChecked == true; - App.SmtpUsername = SmtpUsernameBox.Text?.Trim() ?? ""; - App.SmtpFromAddress = SmtpFromBox.Text?.Trim() ?? ""; - App.SmtpRecipients = SmtpRecipientsBox.Text?.Trim() ?? ""; - - if (!string.IsNullOrEmpty(SmtpPasswordBox.Password)) - { - App.SaveSmtpPassword(SmtpPasswordBox.Password); - } + /* Save current App values so we can restore after the test */ + var origServer = App.SmtpServer; + var origPort = App.SmtpPort; + var origSsl = App.SmtpUseSsl; + var origUsername = App.SmtpUsername; + var origFrom = App.SmtpFromAddress; + var origRecipients = App.SmtpRecipients; TestEmailButton.IsEnabled = false; TestEmailButton.Content = "Sending..."; try { + /* Temporarily apply current UI values for the test */ + App.SmtpServer = SmtpServerBox.Text?.Trim() ?? ""; + if (int.TryParse(SmtpPortBox.Text, out var port)) + App.SmtpPort = port; + App.SmtpUseSsl = SmtpSslCheckBox.IsChecked == true; + App.SmtpUsername = SmtpUsernameBox.Text?.Trim() ?? ""; + App.SmtpFromAddress = SmtpFromBox.Text?.Trim() ?? ""; + App.SmtpRecipients = SmtpRecipientsBox.Text?.Trim() ?? ""; + var error = await Services.EmailAlertService.SendTestEmailAsync(); if (error == null) { @@ -892,6 +895,14 @@ private async void TestEmailButton_Click(object sender, RoutedEventArgs e) } finally { + /* Restore original values — only Save should persist changes */ + App.SmtpServer = origServer; + App.SmtpPort = origPort; + App.SmtpUseSsl = origSsl; + App.SmtpUsername = origUsername; + App.SmtpFromAddress = origFrom; + App.SmtpRecipients = origRecipients; + TestEmailButton.Content = "Send Test Email"; TestEmailButton.IsEnabled = true; }