Files

107 lines
4.6 KiB
C#

using System.Text.Json;
namespace WxAgent.Core;
public sealed record WechatStabilitySample(
DateTimeOffset Timestamp,
int ProcessCount,
long WorkingSetBytes,
int HandleCount,
int ThreadCount,
bool WindowFound,
IReadOnlyList<WxAgentErrorCode> Errors,
long MessageEvents,
long ReconnectEvents,
long AgentWorkingSetBytes = 0,
int AgentHandleCount = 0,
int AgentThreadCount = 0,
DateTimeOffset? LastSnapshotAt = null);
public sealed record WechatStabilityReport(
DateTimeOffset StartedAt,
DateTimeOffset FinishedAt,
IReadOnlyList<WechatStabilitySample> Samples,
long MessageEvents,
long ReconnectEvents,
long WorkingSetGrowthBytes,
int HandleGrowth,
int ThreadGrowth,
bool Healthy,
IReadOnlyList<string> Findings,
double RequestedSeconds = 0,
double ObservedSeconds = 0,
bool CompletedDuration = false);
public static class WechatStabilityAnalyzer
{
public static WechatStabilityReport Analyze(
DateTimeOffset startedAt,
DateTimeOffset finishedAt,
IReadOnlyList<WechatStabilitySample> samples,
long messageEvents,
long reconnectEvents)
{
ArgumentNullException.ThrowIfNull(samples);
if (samples.Count == 0)
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "At least one stability sample is required.");
var first = samples[0];
var last = samples[^1];
var memoryGrowth = last.WorkingSetBytes - first.WorkingSetBytes;
var handleGrowth = last.HandleCount - first.HandleCount;
var threadGrowth = last.ThreadCount - first.ThreadCount;
var findings = new List<string>();
var recoveredWindowLoss = last.WindowFound && reconnectEvents > 0;
if (samples.Any(sample => !sample.WindowFound) && !recoveredWindowLoss) findings.Add("wechat-window-unavailable");
foreach (var error in last.Errors) findings.Add("final-error:" + error);
if (last.AgentWorkingSetBytes - first.AgentWorkingSetBytes > 128L * 1024 * 1024) findings.Add("agent-working-set-growth-over-128mb");
if (last.AgentHandleCount - first.AgentHandleCount > 100) findings.Add("agent-handle-growth-over-100");
if (last.AgentThreadCount - first.AgentThreadCount > 20) findings.Add("agent-thread-growth-over-20");
if (memoryGrowth > 256L * 1024 * 1024) findings.Add("working-set-growth-over-256mb");
if (handleGrowth > 500) findings.Add("handle-growth-over-500");
if (threadGrowth > 100) findings.Add("thread-growth-over-100");
if (samples.Any(sample => sample.Timestamp - (sample.LastSnapshotAt ?? startedAt) > TimeSpan.FromMinutes(5)))
findings.Add("listener-snapshot-gap-over-5min");
return new WechatStabilityReport(
startedAt,
finishedAt,
samples,
messageEvents,
reconnectEvents,
memoryGrowth,
handleGrowth,
threadGrowth,
findings.Count == 0,
findings);
}
public static WechatStabilityReport CheckRun(
WechatStabilityReport report, TimeSpan requested, TimeSpan observed, bool cancelled,
string? listenerFailure = null, string? samplingFailure = null)
{
var findings = report.Findings.ToList();
var completed = observed + TimeSpan.FromSeconds(1) >= requested && !cancelled;
if (!completed) findings.Add("duration-incomplete");
if (listenerFailure is not null) findings.Add("listener-failed:" + listenerFailure);
if (samplingFailure is not null) findings.Add("sampling-failed:" + samplingFailure);
if (report.Samples[^1].LastSnapshotAt is not { } snapshot
|| report.FinishedAt - snapshot > TimeSpan.FromSeconds(10)) findings.Add("listener-snapshot-stale");
return report with { Healthy = findings.Count == 0, Findings = findings, RequestedSeconds = requested.TotalSeconds,
ObservedSeconds = observed.TotalSeconds, CompletedDuration = completed };
}
public static async Task WriteJsonAsync(WechatStabilityReport report, string path, CancellationToken cancellationToken)
{
var fullPath = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
var temporary = fullPath + ".tmp";
try
{
await File.WriteAllTextAsync(temporary, JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
File.Move(temporary, fullPath, overwrite: true);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
}