414 lines
16 KiB
C#
414 lines
16 KiB
C#
/*
|
|
* @Author: Codex
|
|
* @Description: Runtime MapData compression measurement tool.
|
|
* @Date: 2026年05月23日 星期六
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Text;
|
|
using MemoryPack;
|
|
using RuntimeData;
|
|
using TH1_Logic.Core;
|
|
using TH1_Logic.Net;
|
|
using TH1_Logic.Steam;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Logic.Editor
|
|
{
|
|
public class MapDataCompressionEditorWindow : EditorWindow
|
|
{
|
|
private enum PayloadKind
|
|
{
|
|
MainMapData,
|
|
GameStartMessage,
|
|
ForceUpdateMessage,
|
|
}
|
|
|
|
private enum CompressionKind
|
|
{
|
|
RuntimeDeflateCodec,
|
|
GZipFastest,
|
|
GZipOptimal,
|
|
DeflateFastest,
|
|
DeflateOptimal,
|
|
}
|
|
|
|
private class CompressionResult
|
|
{
|
|
public string Name;
|
|
public int RawBytes;
|
|
public int CompressedBytes;
|
|
public int DecompressedBytes;
|
|
public double CompressMs;
|
|
public double DecompressMs;
|
|
public double DeserializeMs;
|
|
public bool BytesEqual;
|
|
public bool DeserializeOk;
|
|
public string Error;
|
|
}
|
|
|
|
private PayloadKind _payloadKind = PayloadKind.MainMapData;
|
|
private int _targetReceiverCount = 3;
|
|
private bool _verifyDeserialize = true;
|
|
private Vector2 _reportScrollPosition;
|
|
private GUIStyle _reportStyle;
|
|
private readonly List<CompressionResult> _results = new List<CompressionResult>();
|
|
private string _lastReport = "尚未测试";
|
|
|
|
[MenuItem("Tools/Steam/MapData 压缩率测试")]
|
|
private static void ShowWindow()
|
|
{
|
|
var window = GetWindow<MapDataCompressionEditorWindow>();
|
|
window.titleContent = new GUIContent("MapData 压缩率");
|
|
window.minSize = new Vector2(620, 500);
|
|
window.Show();
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
DrawOptions();
|
|
EditorGUILayout.Space(8);
|
|
DrawRunButtons();
|
|
EditorGUILayout.Space(8);
|
|
DrawResults();
|
|
}
|
|
|
|
private void DrawOptions()
|
|
{
|
|
EditorGUILayout.LabelField("MapData 压缩率测试", EditorStyles.boldLabel);
|
|
EditorGUILayout.HelpBox("运行时默认测试 Main.MapData。Runtime Deflate Codec 是正式联机发送链路当前使用的压缩格式。", MessageType.Info);
|
|
|
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
|
{
|
|
_payloadKind = (PayloadKind)EditorGUILayout.EnumPopup("测试内容", _payloadKind);
|
|
_targetReceiverCount = Mathf.Max(1, EditorGUILayout.IntField("接收端人数", _targetReceiverCount));
|
|
_verifyDeserialize = EditorGUILayout.Toggle("解压后反序列化验证", _verifyDeserialize);
|
|
|
|
var mapData = Main.MapData;
|
|
EditorGUILayout.LabelField($"Main.MapData: {(mapData == null ? "null" : "可用")}");
|
|
if (mapData?.Net != null)
|
|
{
|
|
EditorGUILayout.LabelField($"NetMode: {mapData.Net.Mode}");
|
|
EditorGUILayout.LabelField($"ActionCount: {mapData.Net.Actions?.Count ?? 0}");
|
|
}
|
|
|
|
var lobby = LobbyManager.Instance?.Lobby;
|
|
if (lobby != null && lobby.IsInLobby())
|
|
{
|
|
var liveTargets = Mathf.Max(1, lobby.GetMemberCount() - 1);
|
|
if (GUILayout.Button($"使用当前房间接收人数: {liveTargets}"))
|
|
{
|
|
_targetReceiverCount = liveTargets;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawRunButtons()
|
|
{
|
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
|
{
|
|
GUI.enabled = Main.MapData != null;
|
|
if (GUILayout.Button("测试 Main.MapData 压缩率", GUILayout.Height(42)))
|
|
{
|
|
RunAllCases();
|
|
}
|
|
GUI.enabled = true;
|
|
|
|
if (GUILayout.Button("复制上次报告"))
|
|
{
|
|
EditorGUIUtility.systemCopyBuffer = _lastReport;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawResults()
|
|
{
|
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
|
{
|
|
EditorGUILayout.LabelField("结果", EditorStyles.boldLabel);
|
|
var style = GetReportStyle();
|
|
_reportScrollPosition = EditorGUILayout.BeginScrollView(_reportScrollPosition, GUILayout.MinHeight(220), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.SelectableLabel(_lastReport, style, GUILayout.Height(GetReportHeight(style)));
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
}
|
|
|
|
private void RunAllCases()
|
|
{
|
|
_results.Clear();
|
|
try
|
|
{
|
|
var rawBytes = BuildPayloadBytes();
|
|
if (rawBytes == null || rawBytes.Length == 0)
|
|
{
|
|
_lastReport = "测试失败: 原始数据为空";
|
|
return;
|
|
}
|
|
|
|
RunCase(rawBytes, CompressionKind.RuntimeDeflateCodec);
|
|
RunCase(rawBytes, CompressionKind.GZipFastest);
|
|
RunCase(rawBytes, CompressionKind.GZipOptimal);
|
|
RunCase(rawBytes, CompressionKind.DeflateFastest);
|
|
RunCase(rawBytes, CompressionKind.DeflateOptimal);
|
|
_lastReport = BuildReport();
|
|
UnityEngine.Debug.Log(_lastReport);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_lastReport = $"测试失败: {e}";
|
|
UnityEngine.Debug.LogError(_lastReport);
|
|
}
|
|
}
|
|
|
|
private byte[] BuildPayloadBytes()
|
|
{
|
|
var mapData = Main.MapData;
|
|
if (mapData == null) return null;
|
|
|
|
switch (_payloadKind)
|
|
{
|
|
case PayloadKind.GameStartMessage:
|
|
return MemoryPackSerializer.Serialize<BaseMessage>(new GameStartMessage { MapData = mapData });
|
|
case PayloadKind.ForceUpdateMessage:
|
|
return MemoryPackSerializer.Serialize<BaseMessage>(new ForceUpdateMessage { MapData = mapData });
|
|
default:
|
|
return MemoryPackSerializer.Serialize(mapData);
|
|
}
|
|
}
|
|
|
|
private void RunCase(byte[] rawBytes, CompressionKind kind)
|
|
{
|
|
var result = new CompressionResult
|
|
{
|
|
Name = GetCaseName(kind),
|
|
RawBytes = rawBytes.Length,
|
|
};
|
|
|
|
try
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var compressed = Compress(rawBytes, kind);
|
|
stopwatch.Stop();
|
|
result.CompressMs = stopwatch.Elapsed.TotalMilliseconds;
|
|
result.CompressedBytes = compressed.Length;
|
|
|
|
stopwatch.Restart();
|
|
var decompressed = Decompress(compressed, kind);
|
|
stopwatch.Stop();
|
|
result.DecompressMs = stopwatch.Elapsed.TotalMilliseconds;
|
|
result.DecompressedBytes = decompressed.Length;
|
|
result.BytesEqual = BytesEqual(rawBytes, decompressed);
|
|
|
|
if (_verifyDeserialize && result.BytesEqual)
|
|
{
|
|
stopwatch.Restart();
|
|
result.DeserializeOk = VerifyDeserialize(decompressed);
|
|
stopwatch.Stop();
|
|
result.DeserializeMs = stopwatch.Elapsed.TotalMilliseconds;
|
|
}
|
|
else
|
|
{
|
|
result.DeserializeOk = !_verifyDeserialize;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
result.Error = e.Message;
|
|
}
|
|
|
|
_results.Add(result);
|
|
}
|
|
|
|
private static byte[] Compress(byte[] rawBytes, CompressionKind kind)
|
|
{
|
|
if (kind == CompressionKind.RuntimeDeflateCodec)
|
|
{
|
|
return NetworkPayloadCodec.EncodeForDiagnostics(rawBytes, true);
|
|
}
|
|
|
|
using (var output = new MemoryStream())
|
|
{
|
|
using (var stream = CreateCompressionStream(output, kind))
|
|
{
|
|
stream.Write(rawBytes, 0, rawBytes.Length);
|
|
}
|
|
|
|
return output.ToArray();
|
|
}
|
|
}
|
|
|
|
private static byte[] Decompress(byte[] compressedBytes, CompressionKind kind)
|
|
{
|
|
if (kind == CompressionKind.RuntimeDeflateCodec)
|
|
{
|
|
return NetworkPayloadCodec.DecodeIfNeeded(compressedBytes);
|
|
}
|
|
|
|
using (var input = new MemoryStream(compressedBytes))
|
|
using (var stream = CreateDecompressionStream(input, kind))
|
|
using (var output = new MemoryStream())
|
|
{
|
|
stream.CopyTo(output);
|
|
return output.ToArray();
|
|
}
|
|
}
|
|
|
|
private bool VerifyDeserialize(byte[] bytes)
|
|
{
|
|
switch (_payloadKind)
|
|
{
|
|
case PayloadKind.GameStartMessage:
|
|
case PayloadKind.ForceUpdateMessage:
|
|
return MemoryPackSerializer.Deserialize<BaseMessage>(bytes) != null;
|
|
default:
|
|
return MemoryPackSerializer.Deserialize<MapData>(bytes) != null;
|
|
}
|
|
}
|
|
|
|
private static Stream CreateCompressionStream(Stream output, CompressionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case CompressionKind.RuntimeDeflateCodec:
|
|
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
|
case CompressionKind.GZipFastest:
|
|
return new GZipStream(output, System.IO.Compression.CompressionLevel.Fastest);
|
|
case CompressionKind.GZipOptimal:
|
|
return new GZipStream(output, System.IO.Compression.CompressionLevel.Optimal);
|
|
case CompressionKind.DeflateFastest:
|
|
return new DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest);
|
|
case CompressionKind.DeflateOptimal:
|
|
return new DeflateStream(output, System.IO.Compression.CompressionLevel.Optimal);
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
|
}
|
|
}
|
|
|
|
private static Stream CreateDecompressionStream(Stream input, CompressionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case CompressionKind.GZipFastest:
|
|
case CompressionKind.GZipOptimal:
|
|
return new GZipStream(input, CompressionMode.Decompress);
|
|
case CompressionKind.RuntimeDeflateCodec:
|
|
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
|
case CompressionKind.DeflateFastest:
|
|
case CompressionKind.DeflateOptimal:
|
|
return new DeflateStream(input, CompressionMode.Decompress);
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
|
}
|
|
}
|
|
|
|
private string BuildReport()
|
|
{
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine($"MapData 压缩率测试 - {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
|
builder.AppendLine($"测试内容: {_payloadKind}");
|
|
builder.AppendLine($"接收端人数: {_targetReceiverCount}");
|
|
builder.AppendLine();
|
|
|
|
foreach (var result in _results)
|
|
{
|
|
builder.AppendLine($"[{result.Name}]");
|
|
if (!string.IsNullOrEmpty(result.Error))
|
|
{
|
|
builder.AppendLine($"失败: {result.Error}");
|
|
builder.AppendLine();
|
|
continue;
|
|
}
|
|
|
|
var savedBytes = result.RawBytes - result.CompressedBytes;
|
|
var savedPercent = result.RawBytes > 0 ? savedBytes * 100.0 / result.RawBytes : 0;
|
|
var ratio = result.RawBytes > 0 ? result.CompressedBytes * 100.0 / result.RawBytes : 0;
|
|
var rawTotal = (long)result.RawBytes * _targetReceiverCount;
|
|
var compressedTotal = (long)result.CompressedBytes * _targetReceiverCount;
|
|
var totalSaved = rawTotal - compressedTotal;
|
|
|
|
builder.AppendLine($"原始: {FormatBytes(result.RawBytes)} ({result.RawBytes:N0} bytes)");
|
|
builder.AppendLine($"压缩后: {FormatBytes(result.CompressedBytes)} ({result.CompressedBytes:N0} bytes)");
|
|
builder.AppendLine($"单份降低: {FormatBytes(savedBytes)} / {savedPercent:F2}%");
|
|
builder.AppendLine($"压缩后占原始: {ratio:F2}%");
|
|
builder.AppendLine($"房主发送 {_targetReceiverCount} 份: {FormatBytes(rawTotal)} -> {FormatBytes(compressedTotal)}, 少发 {FormatBytes(totalSaved)}");
|
|
builder.AppendLine($"耗时: 压缩 {result.CompressMs:F2} ms, 解压 {result.DecompressMs:F2} ms, 反序列化验证 {result.DeserializeMs:F2} ms");
|
|
builder.AppendLine($"校验: 解压字节一致={result.BytesEqual}, 反序列化={result.DeserializeOk}");
|
|
builder.AppendLine();
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static string GetCaseName(CompressionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case CompressionKind.RuntimeDeflateCodec:
|
|
return "Runtime Deflate Codec";
|
|
case CompressionKind.GZipFastest:
|
|
return "GZip Fastest";
|
|
case CompressionKind.GZipOptimal:
|
|
return "GZip Optimal";
|
|
case CompressionKind.DeflateFastest:
|
|
return "Deflate Fastest";
|
|
case CompressionKind.DeflateOptimal:
|
|
return "Deflate Optimal";
|
|
default:
|
|
return kind.ToString();
|
|
}
|
|
}
|
|
|
|
private static bool BytesEqual(byte[] left, byte[] right)
|
|
{
|
|
if (left == null || right == null || left.Length != right.Length) return false;
|
|
for (var i = 0; i < left.Length; i++)
|
|
{
|
|
if (left[i] != right[i]) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private GUIStyle GetReportStyle()
|
|
{
|
|
if (_reportStyle != null) return _reportStyle;
|
|
|
|
_reportStyle = new GUIStyle(EditorStyles.textArea)
|
|
{
|
|
alignment = TextAnchor.UpperLeft,
|
|
richText = false,
|
|
stretchHeight = false,
|
|
wordWrap = true,
|
|
};
|
|
return _reportStyle;
|
|
}
|
|
|
|
private float GetReportHeight(GUIStyle style)
|
|
{
|
|
var report = _lastReport ?? string.Empty;
|
|
var width = Mathf.Max(100f, position.width - 52f);
|
|
return Mathf.Max(220f, style.CalcHeight(new GUIContent(report), width) + 12f);
|
|
}
|
|
|
|
private static string FormatBytes(long bytes)
|
|
{
|
|
string[] units = { "B", "KB", "MB", "GB" };
|
|
var value = (double)bytes;
|
|
var unitIndex = 0;
|
|
while (value >= 1024 && unitIndex < units.Length - 1)
|
|
{
|
|
value /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
return $"{value:F2} {units[unitIndex]}";
|
|
}
|
|
}
|
|
}
|