压测工具优化

This commit is contained in:
wuwenbo 2026-05-15 17:43:59 +08:00
parent 94b3b21924
commit e2d4231bd3
2 changed files with 133 additions and 31 deletions

View File

@ -21,14 +21,18 @@ namespace Logic.Editor
{ {
public class NetworkStressEditorWindow : EditorWindow public class NetworkStressEditorWindow : EditorWindow
{ {
private const float TestSeconds = 60f; private const float TotalSeconds = 60f;
private const float MessagesPerSecond = 30f; private const float SendSeconds = 50f;
private const float ReportWaitSeconds = TotalSeconds - SendSeconds;
private const float ClientReportRetrySeconds = 10f;
private const float ClientReportRetryInterval = 1f;
private const float MessagesPerSecond = 10f;
private const int SmallPayloadBytes = 512; private const int SmallPayloadBytes = 512;
private const int LargePayloadBytes = 256 * 1024; private const int LargePayloadBytes = 128 * 1024;
private const int LargeEveryMessages = 60; private const int LargeEveryMessages = 120;
private const int JitterMaxMs = 80; private const int JitterMaxMs = 80;
private const float DropPercent = 2f; private const float DropPercent = 1f;
private const float UnreliablePercent = 5f; private const float UnreliablePercent = 2f;
private const int RandomSeed = 1214; private const int RandomSeed = 1214;
private const string ScenarioName = "OneClick-60s"; private const string ScenarioName = "OneClick-60s";
@ -36,6 +40,7 @@ namespace Logic.Editor
{ {
Idle, Idle,
Running, Running,
WaitingForReports,
} }
private class PeerStats private class PeerStats
@ -96,6 +101,9 @@ namespace Logic.Editor
private string _lastExportPath = string.Empty; private string _lastExportPath = string.Empty;
private string _eventLog = string.Empty; private string _eventLog = string.Empty;
private double _startTime; private double _startTime;
private double _reportWaitUntil;
private double _reportRetryUntil;
private double _nextReportRetryTime;
private double _lastUpdateTime; private double _lastUpdateTime;
private double _nextRepaintTime; private double _nextRepaintTime;
private double _sendAccumulator; private double _sendAccumulator;
@ -164,7 +172,7 @@ namespace Logic.Editor
private void DrawStatus(ILobby lobby) private void DrawStatus(ILobby lobby)
{ {
EditorGUILayout.LabelField("Steam P2P 一键压测", EditorStyles.boldLabel); EditorGUILayout.LabelField("Steam P2P 一键压测", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("房主点一次开始。测试固定 60 秒覆盖小包、周期性大包、ACK、轻量抖动和轻量丢包到时自动停止并导出报告。", MessageType.Info); EditorGUILayout.HelpBox("房主点一次开始。总流程固定 60 秒:前 50 秒发包,后 10 秒等待从端报告;房主收齐报告会立刻自动导出。", MessageType.Info);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{ {
@ -194,11 +202,11 @@ namespace Logic.Editor
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{ {
GUI.enabled = lobby != null && lobby.IsInitialized() && lobby.IsInLobby(); GUI.enabled = lobby != null && lobby.IsInitialized() && lobby.IsInLobby();
var buttonText = _state == RunState.Running ? "停止并导出报告" : "开始 60 秒一键压测"; var buttonText = _state == RunState.Idle ? "开始 60 秒一键压测" : "停止并导出报告";
if (GUILayout.Button(buttonText, GUILayout.Height(52))) if (GUILayout.Button(buttonText, GUILayout.Height(52)))
{ {
if (_state == RunState.Running) if (_state != RunState.Idle)
FinishAndExport(true, true); FinishAndExportNow(true, true);
else else
StartOneClickTest(); StartOneClickTest();
} }
@ -216,8 +224,9 @@ namespace Logic.Editor
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{ {
EditorGUILayout.LabelField("实时结果", EditorStyles.boldLabel); EditorGUILayout.LabelField("实时结果", EditorStyles.boldLabel);
var elapsed = _state == RunState.Running ? EditorApplication.timeSinceStartup - _startTime : 0d; var elapsed = _state == RunState.Idle ? 0d : EditorApplication.timeSinceStartup - _startTime;
EditorGUILayout.LabelField($"进度: {Math.Min(elapsed, TestSeconds):0.0}/{TestSeconds:0}s Pending ACK: {_pendingAcks.Count} 延迟队列: {_scheduledProbes.Count}"); var waitLeft = _state == RunState.WaitingForReports ? Math.Max(0d, _reportWaitUntil - EditorApplication.timeSinceStartup) : 0d;
EditorGUILayout.LabelField($"进度: {Math.Min(elapsed, TotalSeconds):0.0}/{TotalSeconds:0}s 等待报告: {waitLeft:0.0}s Pending ACK: {_pendingAcks.Count} 延迟队列: {_scheduledProbes.Count}");
DrawStatsLine(GetSelfStats(), "本机"); DrawStatsLine(GetSelfStats(), "本机");
if (lobby != null && lobby.IsInLobby()) if (lobby != null && lobby.IsInLobby())
@ -268,9 +277,18 @@ namespace Logic.Editor
GenerateProbes(now, delta); GenerateProbes(now, delta);
DrainScheduledProbes(now); DrainScheduledProbes(now);
if (now - _startTime >= TestSeconds) if (now - _startTime >= SendSeconds)
FinishAndExport(true, true); FinishSendingAndWaitForReports(true);
} }
else if (_state == RunState.WaitingForReports)
{
if (LobbyManager.Instance.Lobby?.IsLobbyOwner() == true && HasAllReports())
ExportHostReportAndStop("房主已收齐报告,自动导出");
else if (now >= _reportWaitUntil)
ExportHostReportAndStop("等待从端报告超时,导出现有报告");
}
RetryReportToHost(now);
if (now >= _nextRepaintTime) if (now >= _nextRepaintTime)
{ {
@ -307,6 +325,9 @@ namespace Logic.Editor
_state = RunState.Running; _state = RunState.Running;
_statusLine = "运行中"; _statusLine = "运行中";
_startTime = EditorApplication.timeSinceStartup; _startTime = EditorApplication.timeSinceStartup;
_reportWaitUntil = 0d;
_reportRetryUntil = 0d;
_nextReportRetryTime = 0d;
_lastUpdateTime = _startTime; _lastUpdateTime = _startTime;
_sendAccumulator = 0d; _sendAccumulator = 0d;
_nextSequence = 0; _nextSequence = 0;
@ -319,7 +340,36 @@ namespace Logic.Editor
AppendEvent($"开始 60 秒压测: session={_sessionId}, run={_runId}"); AppendEvent($"开始 60 秒压测: session={_sessionId}, run={_runId}");
} }
private void FinishAndExport(bool broadcastStop, bool sendReportToHost) private void FinishSendingAndWaitForReports(bool broadcastStop)
{
if (_state != RunState.Running) return;
foreach (var pending in _pendingAcks.Values.ToList())
GetStats(pending.TargetMemberId).AckTimeouts++;
GetSelfStats().AckTimeouts += _pendingAcks.Count;
_scheduledProbes.Clear();
_pendingAcks.Clear();
_state = RunState.WaitingForReports;
_statusLine = "等待从端报告";
_reportWaitUntil = EditorApplication.timeSinceStartup + ReportWaitSeconds;
_lastReport = BuildReport();
_reports[$"{ScenarioName}/{_runId}/{GetSelfMemberId()}"] = _lastReport;
if (broadcastStop && LobbyManager.Instance.Lobby != null && LobbyManager.Instance.Lobby.IsLobbyOwner())
BroadcastControl(NetworkStressMessageKind.ControlStop);
if (LobbyManager.Instance.Lobby == null || !LobbyManager.Instance.Lobby.IsLobbyOwner())
StartReportRetryToHost();
if (LobbyManager.Instance.Lobby?.IsLobbyOwner() != true)
ExportHostReportAndStop("从端压测结束,本地报告已导出并发送给房主");
else if (HasAllReports())
ExportHostReportAndStop("房主已收齐报告,自动导出");
else
AppendEvent("房主等待从端报告");
}
private void FinishAndExportNow(bool broadcastStop, bool sendReportToHost)
{ {
if (_state == RunState.Running) if (_state == RunState.Running)
{ {
@ -328,8 +378,6 @@ namespace Logic.Editor
GetSelfStats().AckTimeouts += _pendingAcks.Count; GetSelfStats().AckTimeouts += _pendingAcks.Count;
} }
_state = RunState.Idle;
_statusLine = "已完成";
_scheduledProbes.Clear(); _scheduledProbes.Clear();
_pendingAcks.Clear(); _pendingAcks.Clear();
_lastReport = BuildReport(); _lastReport = BuildReport();
@ -339,10 +387,17 @@ namespace Logic.Editor
BroadcastControl(NetworkStressMessageKind.ControlStop); BroadcastControl(NetworkStressMessageKind.ControlStop);
if (sendReportToHost) if (sendReportToHost)
SendReportToHost(); StartReportRetryToHost();
ExportHostReportAndStop("手动停止,报告已导出");
}
private void ExportHostReportAndStop(string reason)
{
_state = RunState.Idle;
_statusLine = "已完成";
ExportReport(); ExportReport();
AppendEvent("压测结束,报告已自动导出"); AppendEvent(reason);
} }
private void GenerateProbes(double now, double delta) private void GenerateProbes(double now, double delta)
@ -386,7 +441,7 @@ namespace Logic.Editor
PayloadHash = ComputeHash(payload), PayloadHash = ComputeHash(payload),
Reliable = reliable, Reliable = reliable,
RequiresAck = true, RequiresAck = true,
DurationSeconds = TestSeconds, DurationSeconds = SendSeconds,
MessagesPerSecond = MessagesPerSecond, MessagesPerSecond = MessagesPerSecond,
SmallPayloadBytes = SmallPayloadBytes, SmallPayloadBytes = SmallPayloadBytes,
LargePayloadBytes = LargePayloadBytes, LargePayloadBytes = LargePayloadBytes,
@ -518,13 +573,14 @@ namespace Logic.Editor
private void HandleControlStop(NetworkStressMessage message) private void HandleControlStop(NetworkStressMessage message)
{ {
if (message.RunId != _runId) return; if (message.RunId != _runId) return;
FinishAndExport(false, true); FinishAndExportNow(false, true);
AppendEvent("收到房主停止"); AppendEvent("收到房主停止");
} }
private void HandleProbe(NetworkStressMessage message) private void HandleProbe(NetworkStressMessage message)
{ {
if (message.RunId != _runId) return; if (message.RunId != _runId) return;
if (_state != RunState.Running) return;
var stats = GetStats(message.SenderMemberId); var stats = GetStats(message.SenderMemberId);
stats.ReceivedProbes++; stats.ReceivedProbes++;
@ -582,6 +638,7 @@ namespace Logic.Editor
private void HandleAck(NetworkStressMessage message) private void HandleAck(NetworkStressMessage message)
{ {
if (message.RunId != _runId) return; if (message.RunId != _runId) return;
if (_state != RunState.Running) return;
var key = (message.SenderMemberId, message.AckSequence); var key = (message.SenderMemberId, message.AckSequence);
if (!_pendingAcks.TryGetValue(key, out var sentProbe)) if (!_pendingAcks.TryGetValue(key, out var sentProbe))
{ {
@ -600,9 +657,15 @@ namespace Logic.Editor
private void HandleReport(NetworkStressMessage message) private void HandleReport(NetworkStressMessage message)
{ {
_reports[$"{message.RunId}/{message.SenderMemberId}"] = message.Text ?? string.Empty; _reports[$"{ScenarioName}/{message.RunId}/{message.SenderMemberId}"] = message.Text ?? string.Empty;
if (_state == RunState.Idle && !string.IsNullOrEmpty(_lastExportPath)) AppendEvent($"收到从端报告: {message.SenderMemberId}");
if (LobbyManager.Instance.Lobby?.IsLobbyOwner() == true && _state == RunState.WaitingForReports && HasAllReports())
ExportHostReportAndStop("房主已收齐报告,自动导出");
else if (LobbyManager.Instance.Lobby?.IsLobbyOwner() == true && _state == RunState.Idle && !string.IsNullOrEmpty(_lastExportPath))
{
ExportReport(); ExportReport();
AppendEvent("收到迟到从端报告,已更新导出文件");
}
} }
private void BroadcastControl(NetworkStressMessageKind kind) private void BroadcastControl(NetworkStressMessageKind kind)
@ -614,7 +677,7 @@ namespace Logic.Editor
RunId = _runId, RunId = _runId,
ScenarioName = ScenarioName, ScenarioName = ScenarioName,
SenderMemberId = GetSelfMemberId(), SenderMemberId = GetSelfMemberId(),
DurationSeconds = TestSeconds, DurationSeconds = SendSeconds,
MessagesPerSecond = MessagesPerSecond, MessagesPerSecond = MessagesPerSecond,
SmallPayloadBytes = SmallPayloadBytes, SmallPayloadBytes = SmallPayloadBytes,
LargePayloadBytes = LargePayloadBytes, LargePayloadBytes = LargePayloadBytes,
@ -629,12 +692,28 @@ namespace Logic.Editor
AppendEvent($"{kind} 广播失败"); AppendEvent($"{kind} 广播失败");
} }
private void SendReportToHost() private void StartReportRetryToHost()
{
if (LobbyManager.Instance.Lobby?.IsLobbyOwner() == true) return;
_reportRetryUntil = EditorApplication.timeSinceStartup + ClientReportRetrySeconds;
_nextReportRetryTime = 0d;
RetryReportToHost(EditorApplication.timeSinceStartup);
}
private void RetryReportToHost(double now)
{
if (string.IsNullOrEmpty(_lastReport)) return;
if (now > _reportRetryUntil || now < _nextReportRetryTime) return;
_nextReportRetryTime = now + ClientReportRetryInterval;
SendReportToHost();
}
private bool SendReportToHost()
{ {
var lobby = LobbyManager.Instance.Lobby; var lobby = LobbyManager.Instance.Lobby;
if (lobby == null || !lobby.IsInLobby() || lobby.IsLobbyOwner()) return; if (lobby == null || !lobby.IsInLobby() || lobby.IsLobbyOwner()) return false;
var host = lobby.GetLobbyOwnerId(); var host = lobby.GetLobbyOwnerId();
if (host == 0) return; if (host == 0) return false;
var report = new NetworkStressMessage var report = new NetworkStressMessage
{ {
@ -646,7 +725,7 @@ namespace Logic.Editor
TargetMemberId = host, TargetMemberId = host,
Text = _lastReport, Text = _lastReport,
}; };
TrySendMessage(report, false, host, true, out _); return TrySendMessage(report, false, host, true, out _);
} }
private bool TrySendMessage(NetworkStressMessage message, bool broadcast, ulong target, bool reliable, private bool TrySendMessage(NetworkStressMessage message, bool broadcast, ulong target, bool reliable,
@ -740,6 +819,20 @@ namespace Logic.Editor
return memberId.ToString(); return memberId.ToString();
} }
private bool HasAllReports()
{
var lobby = LobbyManager.Instance.Lobby;
if (lobby == null || !lobby.IsInLobby()) return false;
foreach (var memberId in lobby.GetAllMemberIds())
{
if (!_reports.ContainsKey($"{ScenarioName}/{_runId}/{memberId}"))
return false;
}
return true;
}
private string BuildReport() private string BuildReport()
{ {
var sb = new StringBuilder(4096); var sb = new StringBuilder(4096);
@ -750,7 +843,8 @@ namespace Logic.Editor
AppendJson(sb, "memberId", GetSelfMemberId().ToString(), true, false); AppendJson(sb, "memberId", GetSelfMemberId().ToString(), true, false);
AppendJson(sb, "memberName", GetMemberName(GetSelfMemberId()), true); AppendJson(sb, "memberName", GetMemberName(GetSelfMemberId()), true);
AppendJson(sb, "state", _statusLine, true); AppendJson(sb, "state", _statusLine, true);
AppendJson(sb, "durationSeconds", TestSeconds.ToString("0"), true, false); AppendJson(sb, "sendSeconds", SendSeconds.ToString("0"), true, false);
AppendJson(sb, "totalSeconds", TotalSeconds.ToString("0"), true, false);
AppendJson(sb, "messagesPerSecond", MessagesPerSecond.ToString("0"), true, false); AppendJson(sb, "messagesPerSecond", MessagesPerSecond.ToString("0"), true, false);
AppendJson(sb, "smallPayloadBytes", SmallPayloadBytes.ToString(), true, false); AppendJson(sb, "smallPayloadBytes", SmallPayloadBytes.ToString(), true, false);
AppendJson(sb, "largePayloadBytes", LargePayloadBytes.ToString(), true, false); AppendJson(sb, "largePayloadBytes", LargePayloadBytes.ToString(), true, false);

View File

@ -23,8 +23,10 @@ namespace TH1_Logic.Steam
private const int MaxLargeMessageBytes = 64 * 1024 * 1024; private const int MaxLargeMessageBytes = 64 * 1024 * 1024;
private const int MaxLargeWireMessageBytes = MaxLargeMessageBytes + OrderedMessageHeaderSize; private const int MaxLargeWireMessageBytes = MaxLargeMessageBytes + OrderedMessageHeaderSize;
private const float LargeMessageTimeout = 30f; private const float LargeMessageTimeout = 30f;
private const float OutgoingMessageSendInterval = 0.05f; private const float OutgoingMessageSendInterval = 0.01f;
private const float OutgoingMessageLimitRetryDelay = 0.25f; private const float OutgoingMessageLimitRetryDelay = 0.25f;
private const int MaxOutgoingMessagesPerUpdate = 8;
private const int MaxOutgoingBytesPerUpdate = 512 * 1024;
private const int OrderedMessageMagic = 0x31514f54; // TOQ1 private const int OrderedMessageMagic = 0x31514f54; // TOQ1
private const int OrderedMessageVersion = 1; private const int OrderedMessageVersion = 1;
private const int OrderedMessageHeaderSize = 20; private const int OrderedMessageHeaderSize = 20;
@ -1168,9 +1170,12 @@ namespace TH1_Logic.Steam
if (_outgoingPeerOrder.Count == 0) return; if (_outgoingPeerOrder.Count == 0) return;
var peerCount = _outgoingPeerOrder.Count; var peerCount = _outgoingPeerOrder.Count;
var sentCount = 0;
var sentBytes = 0;
for (int attempt = 0; attempt < peerCount; attempt++) for (int attempt = 0; attempt < peerCount; attempt++)
{ {
if (_outgoingPeerOrder.Count == 0) return; if (_outgoingPeerOrder.Count == 0) return;
if (sentCount >= MaxOutgoingMessagesPerUpdate || sentBytes >= MaxOutgoingBytesPerUpdate) return;
if (_outgoingPeerRoundRobinIndex >= _outgoingPeerOrder.Count) _outgoingPeerRoundRobinIndex = 0; if (_outgoingPeerRoundRobinIndex >= _outgoingPeerOrder.Count) _outgoingPeerRoundRobinIndex = 0;
var target = _outgoingPeerOrder[_outgoingPeerRoundRobinIndex]; var target = _outgoingPeerOrder[_outgoingPeerRoundRobinIndex];
_outgoingPeerRoundRobinIndex = (_outgoingPeerRoundRobinIndex + 1) % _outgoingPeerOrder.Count; _outgoingPeerRoundRobinIndex = (_outgoingPeerRoundRobinIndex + 1) % _outgoingPeerOrder.Count;
@ -1231,11 +1236,14 @@ namespace TH1_Logic.Steam
var result = SendRawToResult(pending.Target, conn, pending.Data, pending.Reliable, pending.UseNoNagle, false); var result = SendRawToResult(pending.Target, conn, pending.Data, pending.Reliable, pending.UseNoNagle, false);
if (result == EResult.k_EResultOK) if (result == EResult.k_EResultOK)
{ {
var sentLength = pending.Data.Length;
DequeueOutgoingMessage(pending.Target, queue); DequeueOutgoingMessage(pending.Target, queue);
if (pending.IsLastLargeChunk) if (pending.IsLastLargeChunk)
LogSystem.LogInfo($"Queued large P2P message sent to {pending.Target}, messageId: {pending.MessageId}, chunks: {pending.ChunkCount}"); LogSystem.LogInfo($"Queued large P2P message sent to {pending.Target}, messageId: {pending.MessageId}, chunks: {pending.ChunkCount}");
sentCount++;
sentBytes += sentLength;
queue.NextSendTime = Time.time + OutgoingMessageSendInterval; queue.NextSendTime = Time.time + OutgoingMessageSendInterval;
return; continue;
} }
pending.Attempts++; pending.Attempts++;