perf: pre-compile regexes in redaction helpers and UI pages

Replace 11 on-demand Regex.Replace/Match calls (in CommandCenterTextHelper,
WizardPage, SchemaConfigEditor, and QuickSendDialog) with static readonly
pre-compiled Regex fields using RegexOptions.Compiled.

Each field is compiled once at startup and reused on every invocation,
eliminating the overhead of per-call regex compilation or .NET cache
lookups.

Affected call sites:
- CommandCenterTextHelper.RedactSupportPath: 2 patterns
- CommandCenterTextHelper.RedactSupportValue: 6 patterns
- WizardPage.Render: 2 patterns (URL detection + device code)
- SchemaConfigEditor.GetLabel: 1 pattern (camelCase → label)
- QuickSendDialog.TryExtractMissingScope: 1 pattern

No behaviour change; all 393 Tray tests and 1219 Shared tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
github-actions[bot] 2026-05-06 13:11:34 +00:00 committed by GitHub
parent 584a19fadd
commit ab227a7c1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 70 additions and 54 deletions

View File

@ -17,6 +17,10 @@ public sealed partial class SchemaConfigEditor : UserControl
private JsonElement _config;
private readonly Dictionary<string, object?> _changes = new();
private static readonly Regex CamelCaseSplitPattern = new(
"([a-z])([A-Z])",
RegexOptions.Compiled);
private static readonly SolidColorBrush SecondaryBrush =
new(ColorHelper.FromArgb(255, 140, 150, 170));
@ -378,7 +382,7 @@ public sealed partial class SchemaConfigEditor : UserControl
private static string GetLabel(string path, string name)
{
var result = Regex.Replace(name, "([a-z])([A-Z])", "$1 $2");
var result = CamelCaseSplitPattern.Replace(name, "$1 $2");
result = result.Replace("_", " ").Replace(".", " \u203A ");
// Title-case the first character
if (result.Length > 0)

View File

@ -42,6 +42,9 @@ public sealed class QuickSendDialog : WindowEx
uint uFlags);
private static readonly IntPtr HWND_TOPMOST = new(-1);
private static readonly Regex MissingScopePattern = new(
@"missing\s+scope\s*:\s*([A-Za-z0-9._-]+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private const int SW_SHOWNORMAL = 1;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
@ -280,7 +283,7 @@ public sealed class QuickSendDialog : WindowEx
return false;
}
var match = Regex.Match(message, @"missing\s+scope\s*:\s*([A-Za-z0-9._-]+)", RegexOptions.IgnoreCase);
var match = MissingScopePattern.Match(message);
if (!match.Success)
{
return false;

View File

@ -11,6 +11,48 @@ namespace OpenClawTray.Helpers;
internal static class CommandCenterTextHelper
{
// Pre-compiled patterns used in RedactSupportPath / RedactSupportValue.
// Compiled once at startup; reused on every diagnostic / support-text build.
private static readonly Regex PathWindowsUserPattern = new(
@"\b[A-Za-z]:\\Users\\[^\\]+",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex PathUnixUserPattern = new(
@"/Users/[^/]+",
RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueUrlHostPattern = new(
@"\b[a-z][a-z0-9+.-]*://(?:[^@\s/]+@)?([^:/\s]+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueIpPattern = new(
@"\b(?:\d{1,3}\.){3}\d{1,3}\b",
RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueEmailPattern = new(
@"\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueUserAtHostPattern = new(
@"\b(?<user>[A-Za-z0-9._-]+)@(?<host>[A-Za-z0-9._-]+)(?=[:\s]|$)",
RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueHostAfterToPattern = new(
@"(?<=\bto\s)[A-Za-z0-9._-]+(?=:\d{1,5}\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ValueLeadingHostPattern = new(
@"^\s*[A-Za-z0-9._-]+(?=:\d{1,5}\b)",
RegexOptions.Compiled,
TimeSpan.FromMilliseconds(100));
internal static string BuildSupportContext(GatewayCommandCenterState state)
{
var builder = new StringBuilder();
@ -346,19 +388,9 @@ internal static class CommandCenterTextHelper
}
}
redacted = Regex.Replace(
redacted,
@"\b[A-Za-z]:\\Users\\[^\\]+",
"%USERPROFILE%",
RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
redacted = PathWindowsUserPattern.Replace(redacted, "%USERPROFILE%");
redacted = Regex.Replace(
redacted,
@"/Users/[^/]+",
"$HOME",
RegexOptions.None,
TimeSpan.FromMilliseconds(100));
redacted = PathUnixUserPattern.Replace(redacted, "$HOME");
return redacted;
}
@ -368,47 +400,19 @@ internal static class CommandCenterTextHelper
if (string.IsNullOrWhiteSpace(value))
return "unknown";
var redacted = Regex.Replace(
var redacted = ValueUrlHostPattern.Replace(
value,
@"\b[a-z][a-z0-9+.-]*://(?:[^@\s/]+@)?([^:/\s]+)",
match => match.Value.Replace(match.Groups[1].Value, "<host>"),
RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
match => match.Value.Replace(match.Groups[1].Value, "<host>"));
redacted = Regex.Replace(
redacted,
@"\b(?:\d{1,3}\.){3}\d{1,3}\b",
"<ip>",
RegexOptions.None,
TimeSpan.FromMilliseconds(100));
redacted = ValueIpPattern.Replace(redacted, "<ip>");
redacted = Regex.Replace(
redacted,
@"\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b",
"<email>",
RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
redacted = ValueEmailPattern.Replace(redacted, "<email>");
redacted = Regex.Replace(
redacted,
@"\b(?<user>[A-Za-z0-9._-]+)@(?<host>[A-Za-z0-9._-]+)(?=[:\s]|$)",
"<user>@<host>",
RegexOptions.None,
TimeSpan.FromMilliseconds(100));
redacted = ValueUserAtHostPattern.Replace(redacted, "<user>@<host>");
redacted = Regex.Replace(
redacted,
@"(?<=\bto\s)[A-Za-z0-9._-]+(?=:\d{1,5}\b)",
"<host>",
RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
redacted = ValueHostAfterToPattern.Replace(redacted, "<host>");
redacted = Regex.Replace(
redacted,
@"^\s*[A-Za-z0-9._-]+(?=:\d{1,5}\b)",
"<host>",
RegexOptions.None,
TimeSpan.FromMilliseconds(100));
redacted = ValueLeadingHostPattern.Replace(redacted, "<host>");
return redacted;
}

View File

@ -18,6 +18,13 @@ namespace OpenClawTray.Onboarding.Pages;
/// </summary>
public sealed class WizardPage : Component<OnboardingState>
{
private static readonly Regex UrlInMessagePattern = new(
@"(https?://[^\s\)\"",]+)",
RegexOptions.Compiled);
private static readonly Regex DeviceCodePattern = new(
@"(?:^|\s)(?:[Cc]ode|user_code|USER_CODE)\s*[:=]\s*([A-Z0-9]{2,8}(?:-[A-Z0-9]{2,8})+|[A-Z0-9]{4,12})\b",
RegexOptions.Compiled);
public override Element Render()
{
// Read persisted wizard state from shared OnboardingState
@ -523,7 +530,7 @@ public sealed class WizardPage : Component<OnboardingState>
if (!string.IsNullOrEmpty(displayMessage))
{
// URL detection — find https:// URLs in the message
var urlMatch = Regex.Match(displayMessage, @"(https?://[^\s\)\"",]+)");
var urlMatch = UrlInMessagePattern.Match(displayMessage);
if (urlMatch.Success)
{
var detectedUrl = urlMatch.Value;
@ -545,9 +552,7 @@ public sealed class WizardPage : Component<OnboardingState>
// Capture must contain a digit or hyphen (or be all uppercase) to avoid
// matching common English words like "below" that follow "code".
// Case-sensitive on the value to require the GitHub-style uppercase code.
var codeMatch = Regex.Match(
displayMessage,
@"(?:^|\s)(?:[Cc]ode|user_code|USER_CODE)\s*[:=]\s*([A-Z0-9]{2,8}(?:-[A-Z0-9]{2,8})+|[A-Z0-9]{4,12})\b");
var codeMatch = DeviceCodePattern.Match(displayMessage);
if (codeMatch.Success)
{
var code = codeMatch.Groups[1].Value;
@ -581,7 +586,7 @@ public sealed class WizardPage : Component<OnboardingState>
{
if (!string.IsNullOrEmpty(displayMessage))
{
var urlMatch = Regex.Match(displayMessage, @"(https?://[^\s\)\"",]+)");
var urlMatch = UrlInMessagePattern.Match(displayMessage);
if (urlMatch.Success)
{
try