using System.Text.Json; namespace WxAgent.Core; public sealed record WechatStabilitySample( DateTimeOffset Timestamp, int ProcessCount, long WorkingSetBytes, int HandleCount, int ThreadCount, bool WindowFound, IReadOnlyList 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 Samples, long MessageEvents, long ReconnectEvents, long WorkingSetGrowthBytes, int HandleGrowth, int ThreadGrowth, bool Healthy, IReadOnlyList Findings, double RequestedSeconds = 0, double ObservedSeconds = 0, bool CompletedDuration = false); public static class WechatStabilityAnalyzer { public static WechatStabilityReport Analyze( DateTimeOffset startedAt, DateTimeOffset finishedAt, IReadOnlyList 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(); 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); } } }