perf: replace HashSet/ToLowerInvariant with FrozenSet/FrozenDictionary in ChannelHealth; eliminate List<string> in GatewayUsageInfo.DisplayText (#175)
ChannelHealth: - s_healthyStatuses/s_intermediateStatuses: HashSet<string> → FrozenSet<string> (O(1) lookup, no per-call allocation; consistent with existing FrozenDictionary pattern in NotificationCategorizer and OpenClawGatewayClient) - DisplayText: Status.ToLowerInvariant() switch → FrozenDictionary<string,string> s_statusLabels lookup (eliminates the lowercase string allocation on every channel health update) GatewayUsageInfo.DisplayText: - List<string>/string.Join → nullable string?[4] accumulator + string.Join overload with offset+count (single array allocation; no List wrapper) Tests (+8): - ChannelHealth.DisplayText_CaseInsensitiveLabelLookup (6 cases: RUNNING, Connected, READY, NOT CONFIGURED, Connecting, STOPPED) - GatewayUsageInfo.DisplayText_PreservesPartOrder_TokensBeforeCostBeforeRequests - GatewayUsageInfo.DisplayText_ModelOnlyWithTokens_SeparatedBySeparator Result: 594 Shared passed, 20 skipped; 122 Tray passed (was 586+122) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
c9255f9d94
commit
eeac8d5c7c
@ -1,3 +1,5 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace OpenClaw.Shared;
|
||||
@ -108,11 +110,37 @@ public class ChannelHealth
|
||||
public string? AuthAge { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
private static readonly HashSet<string> s_healthyStatuses =
|
||||
new(StringComparer.OrdinalIgnoreCase) { "ok", "connected", "running", "active", "ready" };
|
||||
// FrozenSet gives O(1) case-insensitive lookup with no per-call allocation;
|
||||
// these sets are never mutated after startup so FrozenSet is the correct choice.
|
||||
private static readonly FrozenSet<string> s_healthyStatuses =
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{ "ok", "connected", "running", "active", "ready" }
|
||||
.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly HashSet<string> s_intermediateStatuses =
|
||||
new(StringComparer.OrdinalIgnoreCase) { "stopped", "idle", "paused", "configured", "pending", "connecting", "reconnecting" };
|
||||
private static readonly FrozenSet<string> s_intermediateStatuses =
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{ "stopped", "idle", "paused", "configured", "pending", "connecting", "reconnecting" }
|
||||
.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Maps each status string (case-insensitive) to its tray label; never mutated after startup.
|
||||
private static readonly FrozenDictionary<string, string> s_statusLabels =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ok"] = "[ON]",
|
||||
["connected"] = "[ON]",
|
||||
["running"] = "[ON]",
|
||||
["active"] = "[ON]",
|
||||
["linked"] = "[LINKED]",
|
||||
["ready"] = "[READY]",
|
||||
["connecting"] = "[...]",
|
||||
["reconnecting"] = "[...]",
|
||||
["error"] = "[ERR]",
|
||||
["disconnected"] = "[ERR]",
|
||||
["stale"] = "[STALE]",
|
||||
["configured"] = "[OFF]",
|
||||
["stopped"] = "[OFF]",
|
||||
["not configured"] = "[N/A]",
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given status string represents a healthy/running channel.
|
||||
@ -131,18 +159,8 @@ public class ChannelHealth
|
||||
{
|
||||
get
|
||||
{
|
||||
var label = Status.ToLowerInvariant() switch
|
||||
{
|
||||
"ok" or "connected" or "running" or "active" => "[ON]",
|
||||
"linked" => "[LINKED]",
|
||||
"ready" => "[READY]",
|
||||
"connecting" or "reconnecting" => "[...]",
|
||||
"error" or "disconnected" => "[ERR]",
|
||||
"stale" => "[STALE]",
|
||||
"configured" or "stopped" => "[OFF]",
|
||||
"not configured" => "[N/A]",
|
||||
_ => "[OFF]"
|
||||
};
|
||||
// FrozenDictionary lookup avoids allocating a lowercased copy of Status.
|
||||
var label = s_statusLabels.GetValueOrDefault(Status, "[OFF]");
|
||||
var detail = IsLinked && AuthAge != null ? $"linked · {AuthAge}" : Status;
|
||||
if (Error != null) detail += $" ({Error})";
|
||||
return $"{label} {Capitalize(Name)}: {detail}";
|
||||
@ -285,20 +303,31 @@ public class GatewayUsageInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (TotalTokens > 0)
|
||||
parts.Add($"Tokens: {ModelFormatting.FormatLargeNumber(TotalTokens)}");
|
||||
if (CostUsd > 0)
|
||||
parts.Add("$" + CostUsd.ToString("F2", CultureInfo.InvariantCulture));
|
||||
if (RequestCount > 0)
|
||||
parts.Add($"{RequestCount} requests");
|
||||
if (!string.IsNullOrEmpty(Model))
|
||||
parts.Add(Model);
|
||||
if (parts.Count == 0 && !string.IsNullOrEmpty(ProviderSummary))
|
||||
parts.Add(ProviderSummary);
|
||||
return parts.Count > 0
|
||||
? string.Join(" · ", parts)
|
||||
: "No usage data";
|
||||
// Avoid allocating a List<string> + string.Join: accumulate up to 4 nullable
|
||||
// string slots and build the result with a single switch expression.
|
||||
string? p0 = TotalTokens > 0
|
||||
? $"Tokens: {ModelFormatting.FormatLargeNumber(TotalTokens)}"
|
||||
: null;
|
||||
string? p1 = CostUsd > 0
|
||||
? "$" + CostUsd.ToString("F2", CultureInfo.InvariantCulture)
|
||||
: null;
|
||||
string? p2 = RequestCount > 0
|
||||
? $"{RequestCount} requests"
|
||||
: null;
|
||||
string? p3 = !string.IsNullOrEmpty(Model) ? Model : null;
|
||||
|
||||
// If all four are null, fall back to ProviderSummary or "No usage data".
|
||||
if (p0 is null && p1 is null && p2 is null && p3 is null)
|
||||
return string.IsNullOrEmpty(ProviderSummary) ? "No usage data" : ProviderSummary!;
|
||||
|
||||
// Pack non-null slots into a fixed-size array and join — one allocation.
|
||||
var parts = new string?[4];
|
||||
int n = 0;
|
||||
if (p0 is not null) parts[n++] = p0;
|
||||
if (p1 is not null) parts[n++] = p1;
|
||||
if (p2 is not null) parts[n++] = p2;
|
||||
if (p3 is not null) parts[n++] = p3;
|
||||
return string.Join(" · ", parts, 0, n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -235,6 +235,19 @@ public class ChannelHealthTests
|
||||
Assert.Contains(": ok", health.DisplayText);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("RUNNING", "[ON]")]
|
||||
[InlineData("Connected", "[ON]")]
|
||||
[InlineData("READY", "[READY]")]
|
||||
[InlineData("NOT CONFIGURED", "[N/A]")]
|
||||
[InlineData("Connecting", "[...]")]
|
||||
[InlineData("STOPPED", "[OFF]")]
|
||||
public void DisplayText_CaseInsensitiveLabelLookup(string status, string expectedLabel)
|
||||
{
|
||||
var health = new ChannelHealth { Name = "ch", Status = status };
|
||||
Assert.StartsWith(expectedLabel, health.DisplayText);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ok", true)]
|
||||
[InlineData("connected", true)]
|
||||
@ -573,6 +586,28 @@ public class GatewayUsageInfoTests
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_PreservesPartOrder_TokensBeforeCostBeforeRequests()
|
||||
{
|
||||
var usage = new GatewayUsageInfo { TotalTokens = 1000, CostUsd = 0.50, RequestCount = 10 };
|
||||
var display = usage.DisplayText;
|
||||
var tokensIdx = display.IndexOf("Tokens:", StringComparison.Ordinal);
|
||||
var costIdx = display.IndexOf("$", StringComparison.Ordinal);
|
||||
var reqIdx = display.IndexOf("requests", StringComparison.Ordinal);
|
||||
Assert.True(tokensIdx < costIdx && costIdx < reqIdx,
|
||||
$"Parts should appear in order tokens·cost·requests but got: {display}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_ModelOnlyWithTokens_SeparatedBySeparator()
|
||||
{
|
||||
var usage = new GatewayUsageInfo { TotalTokens = 5000, Model = "gpt-4" };
|
||||
var display = usage.DisplayText;
|
||||
Assert.Contains(" · ", display);
|
||||
Assert.Contains("Tokens:", display);
|
||||
Assert.Contains("gpt-4", display);
|
||||
}
|
||||
}
|
||||
|
||||
public class GatewayNodeInfoTests
|
||||
|
||||
Loading…
Reference in New Issue
Block a user