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
7 changes: 5 additions & 2 deletions Dashboard/Converters/QueryTextCleanupConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace PerformanceMonitorDashboard.Converters
{
public class QueryTextCleanupConverter : IValueConverter
public partial class QueryTextCleanupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Expand All @@ -28,7 +28,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
text = text.Replace("\t", " ", StringComparison.Ordinal);

// Replace multiple spaces with single space
text = Regex.Replace(text, @"\s+", " ");
text = MultipleSpacesRegExp().Replace(text, " ");

// Trim leading/trailing whitespace
text = text.Trim();
Expand All @@ -40,5 +40,8 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
{
throw new NotImplementedException();
}

[GeneratedRegex(@"\s+")]
private static partial Regex MultipleSpacesRegExp();
}
}
7 changes: 5 additions & 2 deletions Dashboard/Helpers/DateFilterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace PerformanceMonitorDashboard.Helpers
{
public static class DateFilterHelper
public static partial class DateFilterHelper
{
public static bool MatchesFilter(object? value, string? filterText)
{
Expand Down Expand Up @@ -148,7 +148,7 @@ private static bool TryConvertToDateTime(object value, out DateTime result)
}

// "last N hours/days/weeks" expressions
var lastMatch = Regex.Match(expressionLower, @"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)");
var lastMatch = LastNHoursDaysWeeksMonthsRegExp().Match(expressionLower);
if (lastMatch.Success)
{
int count = int.Parse(lastMatch.Groups[1].Value, CultureInfo.InvariantCulture);
Expand Down Expand Up @@ -231,5 +231,8 @@ private static bool IsRelativeExpression(string expression)
expression == "tomorrow" ||
Regex.IsMatch(expression, @"last\s+\d+\s+(hour|hours|day|days|week|weeks|month|months)");
}

[GeneratedRegex(@"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)")]
private static partial Regex LastNHoursDaysWeeksMonthsRegExp();
}
}
35 changes: 19 additions & 16 deletions Dashboard/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,16 @@
/// Post-parse analysis pass that walks a parsed plan tree and adds warnings
/// for common performance anti-patterns. Called after ShowPlanParser.Parse().
/// </summary>
public static class PlanAnalyzer
public static partial class PlanAnalyzer
{
private static readonly Regex FunctionInPredicateRegex = new(
@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex FunctionInPredicateRegex = FunctionInPredicateRegExp();

private static readonly Regex LeadingWildcardLikeRegex = new(
@"\blike\b[^'""]*?N?'%",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex LeadingWildcardLikeRegex = LeadingWildcardLikeRegExp();

private static readonly Regex CaseInPredicateRegex = new(
@"\bCASE\s+(WHEN\b|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CaseInPredicateRegex = CaseInPredicateRegExp();

// Matches CTE definitions: WITH name AS ( or , name AS (
private static readonly Regex CteDefinitionRegex = new(
@"(?:\bWITH\s+|\,\s*)(\w+)\s+AS\s*\(",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CteDefinitionRegex = CteDefinitionRegExp();

public static void Analyze(ParsedPlan plan)
{
Expand Down Expand Up @@ -504,7 +496,7 @@
"Leading wildcard LIKE prevents an index seek — SQL Server must scan every row. If substring search performance is critical, consider a full-text index or a trigram-based approach.",
"CASE expression in predicate" =>
"CASE expression in a predicate prevents an index seek. Rewrite using separate WHERE clauses combined with OR, or split into multiple queries.",
_ when nonSargableReason.StartsWith("Function call") =>

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 499 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorDashboard.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorDashboard.Models.PlanNode, PerformanceMonitorDashboard.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)
$"{nonSargableReason} prevents an index seek. Remove the function from the column side — apply it to the parameter instead, or create a computed column with the expression and index that.",
_ =>
$"{nonSargableReason} prevents an index seek, forcing a scan."
Expand Down Expand Up @@ -686,7 +678,7 @@

// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
node.ObjectName.StartsWith("@"))
node.ObjectName.StartsWith('@'))
{
node.Warnings.Add(new PlanWarning
{
Expand Down Expand Up @@ -890,7 +882,7 @@
return "Implicit conversion (CONVERT_IMPLICIT)";

// ISNULL / COALESCE wrapping column
if (Regex.IsMatch(predicate, @"\b(isnull|coalesce)\s*\(", RegexOptions.IgnoreCase))
if (IsNullCoalesceRegExp().IsMatch(predicate))
return "ISNULL/COALESCE wrapping column";

// Common function calls on columns
Expand Down Expand Up @@ -930,7 +922,7 @@
var refPattern = new Regex(
$@"\b(FROM|JOIN)\s+{Regex.Escape(cteName)}\b",
RegexOptions.IgnoreCase);
var refCount = refPattern.Matches(text).Count;
var refCount = refPattern.Count(text);

if (refCount > 1)
{
Expand Down Expand Up @@ -1243,4 +1235,15 @@
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
}

[GeneratedRegex(@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex FunctionInPredicateRegExp();
[GeneratedRegex(@"\blike\b[^'""]*?N?'%", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex LeadingWildcardLikeRegExp();
[GeneratedRegex(@"\bCASE\s+(WHEN\b|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex CaseInPredicateRegExp();
[GeneratedRegex(@"(?:\bWITH\s+|\,\s*)(\w+)\s+AS\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex CteDefinitionRegExp();
[GeneratedRegex(@"\b(isnull|coalesce)\s*\(", RegexOptions.IgnoreCase)]
private static partial Regex IsNullCoalesceRegExp();
}
7 changes: 5 additions & 2 deletions Dashboard/Services/ReproScriptBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace PerformanceMonitorDashboard.Services;
/// Builds paste-ready T-SQL reproduction scripts from query text and plan XML.
/// Extracts parameters from plan XML ParameterList (same approach as sp_QueryReproBuilder).
/// </summary>
public static class ReproScriptBuilder
public static partial class ReproScriptBuilder
{
/// <summary>
/// Builds a complete reproduction script from available query data.
Expand Down Expand Up @@ -399,7 +399,7 @@ private static List<string> FindUnresolvedVariables(string queryText, List<Query
var extractedNames = new HashSet<string>(parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);

/* Find all @variable references in the query text */
var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
var matches = AtVariableRegExp().Matches(queryText);
var seenVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (Match match in matches)
Expand Down Expand Up @@ -429,6 +429,9 @@ private static List<string> FindUnresolvedVariables(string queryText, List<Query

return unresolved;
}

[GeneratedRegex(@"@\w+", RegexOptions.IgnoreCase)]
private static partial Regex AtVariableRegExp();
}

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion Dashboard/TracePatternHistoryWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public TracePatternHistoryWindow(
_toDate = toDate;

// Collapse newlines/tabs to spaces and truncate for a clean single-line header
var displayPattern = System.Text.RegularExpressions.Regex.Replace(queryPattern, @"\s+", " ").Trim();
var displayPattern = MultipleSpacesRegExp().Replace(queryPattern, " ").Trim();
if (displayPattern.Length > 120)
displayPattern = displayPattern.Substring(0, 120) + "...";
QueryIdentifierText.Text = $"Trace Pattern History: [{databaseName}] — {displayPattern}";
Expand Down Expand Up @@ -406,6 +406,9 @@ private void ExportToCsv_Click(object sender, RoutedEventArgs e)
}
}

[System.Text.RegularExpressions.GeneratedRegex(@"\s+")]
private static partial System.Text.RegularExpressions.Regex MultipleSpacesRegExp();

#endregion
}
}
9 changes: 5 additions & 4 deletions Installer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@

namespace PerformanceMonitorInstaller
{
class Program
partial class Program
{
/*
Pre-compiled regex patterns for performance
*/
private static readonly Regex GoBatchPattern = new Regex(
@"^\s*GO\s*(?:--[^\r\n]*)?\s*$",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);
private static readonly Regex GoBatchPattern = GoBatchRegExp();

private static readonly Regex SqlFileNamePattern = new Regex(
@"^\d{2}[a-z]?_.*\.sql$",
Expand Down Expand Up @@ -1800,5 +1798,8 @@ Write file

return reportPath;
}

[GeneratedRegex(@"^\s*GO\s*(?:--[^\r\n]*)?\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)]
private static partial Regex GoBatchRegExp();
}
}
9 changes: 5 additions & 4 deletions InstallerGui/Services/InstallationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,15 @@ public class InstallationResult
/// <summary>
/// Service for installing the Performance Monitor database
/// </summary>
public class InstallationService : IDisposable
public partial class InstallationService : IDisposable
{
private readonly HttpClient _httpClient;
private bool _disposed;

/*
Compiled regex patterns for better performance
*/
private static readonly Regex SqlFilePattern = new(
@"^\d{2}[a-z]?_.*\.sql$",
RegexOptions.Compiled);
private static readonly Regex SqlFilePattern = SqlFileRegExp();

private static readonly Regex SqlCmdDirectivePattern = new(
@"^:r\s+.*$",
Expand Down Expand Up @@ -1429,5 +1427,8 @@ INSERT INTO PerformanceMonitor.config.installation_history
/*Don't let history logging failure break the installation*/
}
}

[GeneratedRegex(@"^\d{2}[a-z]?_.*\.sql$", RegexOptions.Compiled)]
private static partial Regex SqlFileRegExp();
}
}
2 changes: 1 addition & 1 deletion Lite.Tests/Lite.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
13 changes: 7 additions & 6 deletions Lite/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@
/// Post-parse analysis pass that walks a parsed plan tree and adds warnings
/// for common performance anti-patterns. Called after ShowPlanParser.Parse().
/// </summary>
public static class PlanAnalyzer
public static partial class PlanAnalyzer
{
private static readonly Regex FunctionInPredicateRegex = new(
@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex FunctionInPredicateRegex = FunctionInPredicateRegExp();

private static readonly Regex LeadingWildcardLikeRegex = new(
@"\blike\b[^'""]*?N?'%",
Expand Down Expand Up @@ -504,7 +502,7 @@
"Leading wildcard LIKE prevents an index seek — SQL Server must scan every row. If substring search performance is critical, consider a full-text index or a trigram-based approach.",
"CASE expression in predicate" =>
"CASE expression in a predicate prevents an index seek. Rewrite using separate WHERE clauses combined with OR, or split into multiple queries.",
_ when nonSargableReason.StartsWith("Function call") =>

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 505 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'PerformanceMonitorLite.Services.PlanAnalyzer.AnalyzeNode(PerformanceMonitorLite.Models.PlanNode, PerformanceMonitorLite.Models.PlanStatement)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)
$"{nonSargableReason} prevents an index seek. Remove the function from the column side — apply it to the parameter instead, or create a computed column with the expression and index that.",
_ =>
$"{nonSargableReason} prevents an index seek, forcing a scan."
Expand Down Expand Up @@ -686,7 +684,7 @@

// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
node.ObjectName.StartsWith("@"))
node.ObjectName.StartsWith('@'))
{
node.Warnings.Add(new PlanWarning
{
Expand Down Expand Up @@ -930,7 +928,7 @@
var refPattern = new Regex(
$@"\b(FROM|JOIN)\s+{Regex.Escape(cteName)}\b",
RegexOptions.IgnoreCase);
var refCount = refPattern.Matches(text).Count;
var refCount = refPattern.Count(text);

if (refCount > 1)
{
Expand Down Expand Up @@ -1243,4 +1241,7 @@
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
}

[GeneratedRegex(@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex FunctionInPredicateRegExp();
}
Loading