107 lines
4.6 KiB
C#
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); }
|
|
}
|
|
}
|