🔧 Iris 网络复制系统技术分析 - 第十五部分:调试与性能分析

📖 本章导读:想象你是一位汽车修理工,面对一辆"跑不动"的汽车。你需要先诊断问题(是油没了?轮胎漏气?还是发动机故障?),然后才能修复它。Iris 的调试与性能分析系统就是你的"汽车诊断仪"——它能告诉你网络复制系统哪里出了问题、哪里可以优化。本章将教你如何成为一名优秀的"网络复制修理工"!🔧
🎯 15.1 为什么需要调试与性能分析?
💡 15.1.1 网络游戏的"隐形杀手"
PLAINTEXT
🎮 日常类比:餐厅服务质量
想象你经营一家餐厅,顾客抱怨"上菜太慢":
❓ 问题可能出在哪里?
┌────────────────────────────────────────────────────────────────────────┐
│ │
│ 🍳 厨房问题? 🏃 服务员问题? 📝 点单系统问题? │
│ ├── 厨师太少 ├── 人手不足 ├── 系统卡顿 │
│ ├── 食材准备慢 ├── 路线不合理 ├── 订单丢失 │
│ └── 设备故障 └── 托盘太小 └── 优先级混乱 │
│ │
│ 🎯 没有诊断工具,你只能瞎猜! │
│ ✅ 有了监控系统,问题一目了然! │
│ │
└────────────────────────────────────────────────────────────────────────┘
网络复制也是如此:
- 🍳 厨房 = 服务器(生成数据)- 🏃 服务员 = 网络(传输数据)- 📝 点单系统 = Iris(管理复制)- 👨🍳 顾客 = 客户端(接收数据)🔍 15.1.2 常见的网络复制问题
问题类型 | 现象 | 可能原因 |
|---|---|---|
😱 对象不同步 | 服务器和客户端位置不一致 | 未注册、被过滤、属性未标记 |
😱 延迟过高 | 动作要等很久才显示 | 优先级低、带宽不足 |
😱 带宽爆炸 | 网络流量突然暴增 | 过滤失效、增量压缩未生效 |
😱 CPU 过高 | 服务器帧率下降 | 对象过多、序列化效率低 |
📂 15.1.3 关键源文件索引(UE5.5 源码真实路径✅)
文件 | 位置(Engine/Source/Runtime/Experimental/Iris) | 你该去哪里下断点/找逻辑 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 每类/每对象的计时/bit 统计怎么“攒账” |
|
| Iris 的 LLM Tag(内存分科目) |
📝 15.2 日志系统:网络复制的"黑匣子"
💡 15.2.1 什么是日志系统?
PLAINTEXT
🎮 日常类比:飞机黑匣子
飞机上有两个黑匣子:
📦 飞行数据记录器(FDR):记录飞机的各种参数
🎙️ 驾驶舱语音记录器(CVR):记录飞行员的对话
当飞机出事时,黑匣子能告诉调查员发生了什么。
Iris 日志系统 = 网络复制的"黑匣子"
- 📝 记录复制事件- ⚠️ 记录错误警告- 🔍 事后分析排查
🎯 出问题时,日志是你最好的朋友!📊 15.2.2 Iris 日志类别(源码里到底有哪些?)
先说结论:Iris 的日志类别不是“想当然的一串”,而是分散在不同模块头文件里声明。你看到的某个 UE_LOG(LogXXX, ...),大概率能在对应模块的头文件/.cpp 顶部找到 DECLARE_... / DEFINE_...。
✅ 核心总开关:LogIris / LogIrisFiltering
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Core/IrisLog.hEngine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Core/IrisLog.cpp
CPP
// IrisLog.h(节选)IRISCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogIris, Log, All);
IRISCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogIrisFiltering, Log, All);
// IrisLog.cpp(节选)DEFINE_LOG_CATEGORY(LogIris);
DEFINE_LOG_CATEGORY(LogIrisFiltering);✅ 复制桥接层专用:LogIrisBridge
你什么时候会看到它:打印“桥接层做了什么”(注册对象、销毁、RPC flush、各种调试打印)。
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/ReplicationSystem/ReplicationBridge.hEngine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationBridge.cpp
CPP
// ReplicationBridge.h(节选)IRISCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogIrisBridge, Log, All);
// ReplicationBridge.cpp(节选)DEFINE_LOG_CATEGORY(LogIrisBridge)✅ 过滤配置专用:LogIrisFilterConfig
你什么时候会看到它:当你调整类到过滤器的映射、动态过滤器配置时(很适合排查“为什么对象突然不相关/不复制了”)。
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/ReplicationSystem/ObjectReplicationBridge.hEngine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ObjectReplicationBridge.cpp
CPP
// ObjectReplicationBridge.h(节选)DECLARE_LOG_CATEGORY_EXTERN(LogIrisFilterConfig, Log, All);
// ObjectReplicationBridge.cpp(节选)DEFINE_LOG_CATEGORY(LogIrisFilterConfig)🧠 小白友好理解:把
LogIris当成“总经理广播”,把LogIrisFiltering当成“安检口广播”,把LogIrisBridge当成“调度室广播”,把LogIrisFilterConfig当成“规则配置变更广播”。你要查哪类问题,就把哪路广播开大声一点。🔊
⚙️ 15.2.3 日志级别配置
PLAINTEXT
🎮 日常类比:消息通知设置
想象你手机的通知设置:
📵 静音:什么都不通知
🔔 重要:只通知重要消息
🔔🔔 全部:所有消息都通知级别 | 名称 | 说明 | 适用场景 |
|---|---|---|---|
| 💀 致命 | 直接崩溃/断言级别 | 必须修复 |
| ❌ 错误 | 功能异常但不崩溃 | 需要关注 |
| ⚠️ 警告 | 潜在问题 | 建议检查 |
| 📢 提示 | 比 | 看关键流程 |
| 📝 日志 | 一般信息 | 开发调试 |
| 📖 详细 | 详细信息 | 深度调试 |
| 🧾 超详细 | 很吵、很细 | 只在定位疑难杂症时开 |
配置方法:
INI
; 方式 1:在 DefaultEngine.ini 中配置[Core.Log]LogIris=Verbose ; Iris 主日志设为详细LogIrisFiltering=Warning ; 过滤系统只显示警告以上CPP
// 方式 2:通过控制台命令// Log LogIris Verbose// Log LogIrisFiltering Warning🔍 15.2.4 日志分析实战案例
PLAINTEXT
📋 问题:玩家报告"敌人突然消失"
🔍 第一步:开启过滤日志
控制台输入:Log LogIrisFiltering Verbose
🔍 第二步:复现问题,查看日志
[LogIrisFiltering] Object filtered out: BP_Enemy_C_0 by GridFilter
[LogIrisFiltering] GridFilter: Object outside view range
[LogIrisFiltering] ObjectPos=(5000, 3000, 0)
[LogIrisFiltering] ViewPos=(0, 0, 0)
[LogIrisFiltering] Distance=5831, MaxDistance=5000
🎯 第三步:定位问题
原因:敌人距离玩家 5831 单位,超过了 GridFilter 的 5000 单位限制
✅ 第四步:解决方案
• 增加 GridFilter 的裁剪距离
• 或者为重要敌人设置更高的优先级🔧 15.3 调试工具:网络复制的"X光机"
💡 15.3.1 IrisDebugging 到底是什么?(更像“调试器外挂”🧰)
很多同学以为 IrisDebugging 是一套“打印对象信息”的通用 API,但在 UE5.5 的 Iris 里:
Iris/Core/IrisDebugging.h更偏向“给调试器/断点用”的 Helper:比如“命中某对象名就断下”、以及提供extern "C"的函数方便你在 VS 的 Watch/Immediate Window 里直接调用。而你常见的“打印对象列表/相关性/裁剪距离”等 控制台命令,主要在
ObjectReplicationBridgeDebugging.cpp。
PLAINTEXT
🎮 日常类比:游戏里的“作弊码菜单”(仅开发环境)
- 你想“锁血/透视”不是为了正式玩,而是为了定位 bug 更快。
- `IrisDebugging` 也是这个定位:让你在关键时刻一脚刹车(断点),或者直接把内部状态吐出来。🔎 源码摘录:断点条件 + Watch/Immediate Window 可调用函数
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Core/IrisDebugging.h
CPP
// IrisDebugging.h(节选)namespace UE::Net::IrisDebugHelper
{
IRISCORE_API bool BreakOnObjectName(UObject* Object);
IRISCORE_API bool BreakOnNetRefHandle(FNetRefHandle NetRefHandle);
IRISCORE_API bool BreakOnRPCName(FName RPCName);
extern "C" IRISCORE_API void DebugOutputNetObjectState(uint64 NetRefHandleId, uint32 ReplicationSystemId);
extern "C" IRISCORE_API const TCHAR* DebugNetObjectStateToString(uint32 NetRefHandleId, uint32 ReplicationSystemId);
}🧨 源码摘录:控制台变量/命令如何把“断点条件”装进引擎?
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Core/IrisDebugging.cpp
CPP
// IrisDebugging.cpp(节选)FAutoConsoleVariableRef NetIrisDebugName(
TEXT("Net.Iris.DebugName"),
GIrisDebugName,
TEXT("Set a class name or object name to break on."),
ECVF_Default);
static FAutoConsoleCommand NetIrisDebugNetRefHandle(
TEXT("Net.Iris.DebugNetRefHandle"),
TEXT("Specify a NetRefHandle ID that we will break on (or none to turn off)."),
FConsoleCommandWithArgsDelegate::CreateLambda([](const TArray<FString>& Args){ /*...*/ }));✅ 小白可操作理解:你把
Net.Iris.DebugName设成MyBoss,然后当 Iris 处理到名字包含MyBoss的对象时,代码会UE_DEBUG_BREAK()—— 你就能在“案发现场”看调用栈,而不是事后猜。🕵️♂️
🖥️ 15.3.2 控制台调试命令大全(按源码校准版✅)
很多“命令速查表”会把 Iris 写得像一个全家桶,但在 UE5.5 的源码里:
大量真实存在的 Iris 调试命令前缀是
Net.Iris.(注意N大写)。其中最实用的一批,集中在
ObjectReplicationBridgeDebugging.cpp。
📌 命令在哪注册?
Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ObjectReplicationBridgeDebugging.cpp
🧾 命令清单(真实存在)
目标 | 命令 | 你拿它来解决什么? |
|---|---|---|
看所有注册对象 |
| “对象到底有没有进 Iris?” |
看对任意连接相关 |
| “为什么某些对象没人看得见?” |
看全员永远相关 |
| “AlwaysRelevant 配置是否生效?” |
看指定连接相关 |
| “这个玩家为啥看不到那个对象?”(可带过滤信息) |
看动态过滤配置 |
| “类到过滤器的映射到底是什么?” |
看裁剪距离 |
| “敌人消失/闪烁,是不是距离裁剪?” |
看 PushBased 状态 |
| “属性不同步,是不是没 push-based/没标脏?” |
🧩 通用参数(源码支持)
RepSystemId=X:PIE 多实例时指定某个复制系统ConnectionId=1或ConnectionId=1,5,7:只看指定连接(ObjectReplicationBridge.h注释里给了示例)WithSubObjects:把子对象也打印出来SortByClass/SortByNetRefHandle:排序方式
另外还有命令自己的参数,例如:
Net.Iris.PrintRelevantObjectsToConnection支持WithFilter(源码注释里写了)Net.Iris.PrintNetCullDistances支持NumClasses=X(限制输出前 N 个类)
🔎 源码摘录:命令注册长这样
CPP
// ObjectReplicationBridgeDebugging.cpp(节选)FAutoConsoleCommand ObjectBridgePrintReplicatedObjects(
TEXT("Net.Iris.PrintReplicatedObjects"),
TEXT("Prints the list of replicated objects registered for replication in Iris"),
FConsoleCommandWithArgsDelegate::CreateLambda([](const TArray<FString>& Args)
{
// ... FindReplicationSystemFromArg(Args) 支持 RepSystemId=
// ... FindPrintTraitsFromArgs(Args) 支持 WithSubObjects/SortBy...
// ... ObjectBridge->PrintReplicatedObjects(...)
}));✅ 小白实战建议:当你怀疑“对象没复制”,第一条就跑
Net.Iris.PrintReplicatedObjects;如果它根本不在列表里,后面所有优化都是白忙活。
📋 15.3.3 对象信息输出示例
PLAINTEXT
═══════════════════════════════════════════════════════════════════Object Replication Info: BP_PlayerCharacter_C_0═══════════════════════════════════════════════════════════════════
📌 Basic Info:├── NetRefHandle: 0x00010042├── Class: BP_PlayerCharacter_C├── Owner: PlayerController_0└── NetRole: ROLE_Authority
📊 Replication State:├── IsReplicating: Yes├── IsDirty: No├── LastReplicatedFrame: 12345└── PollPeriod: 0 (every frame)
🎯 Filter Status:├── GridFilter: Passed ✅├── ConnectionFilter: Passed ✅└── GroupFilter: Passed ✅
⭐ Priority Info:├── StaticPriority: 1.0├── Prioritizer: SphereNetObjectPrioritizer└── CurrentPriority: 0.85
🔗 Connections Relevancy:├── Connection 1: Relevant ✅ (Owner)├── Connection 2: Relevant ✅ (Distance: 500)└── Connection 3: Not Relevant ❌ (Distance: 8000)🎨 15.3.4 可视化调试工具
PLAINTEXT
🎮 日常类比:汽车仪表盘
开车时,仪表盘能让你一眼看到:
⛽ 油量、🌡️ 水温、🏎️ 速度、📍 导航
Iris 可视化调试 = 网络复制的"仪表盘"
> ⚠️ 源码事实:在 UE5.5 的 `Runtime/Experimental/Iris` 目录里,我没有找到 `Net.Iris.Draw...` 之类的“场景内画线/画圈”命令注册;**Iris 更偏向用 `Net.Iris.Print...` 系列打印 + Insights 时间线来“可视化”**。下面这张图是帮你快速建立直觉的示意图,真正排查还是以 15.3.2/15.4 为准。PLAINTEXT
┌────────────────────────────────────────────────────────────────────────┐
│ 🎨 可视化调试效果图解 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 游戏场景俯视图 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🟢 = 正常复制的对象 │ │
│ │ 🔴 = 脏对象(需要同步) │ │
│ │ ⚪ = 被过滤的对象 │ │
│ │ 🔵 = 玩家视图范围 │ │
│ │ │ │
│ │ ⚪ ⚪ │ │
│ │ ⚪ ⚪ │ │
│ │ ⚪ ┌────────┐ ⚪ │ │
│ │ │ 🔵 │ │ │
│ │ ⚪ │ 🟢 🔴 │ ⚪ │ │
│ │ │ 🟢 🟢 │ │ │
│ │ ⚪ │ 👤 │ ⚪ │ │
│ │ └────────┘ │ │
│ │ ⚪ ⚪ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 💡 通过颜色可以快速判断对象复制状态和过滤效果 │
│ │
└────────────────────────────────────────────────────────────────────────┘📈 15.4 性能分析:找出网络复制的"瓶颈"
💡 15.4.1 性能分析的重要性
PLAINTEXT
🎮 日常类比:体检报告
每年体检,医生会给你一份详细的报告:
📊 血压:120/80 ✅ 正常
📊 血糖:5.6 ✅ 正常
📊 胆固醇:6.2 ⚠️ 偏高
📊 尿酸:480 ❌ 超标
有了这份报告,你就知道该关注什么、改善什么。
性能分析 = 给网络复制系统做"体检"📊 15.4.2 关键性能指标 (KPIs)
PLAINTEXT
┌────────────────────────────────────────────────────────────────────────┐
│ 📊 Iris 关键性能指标 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ⏱️ 时间指标(越小越好) │
│ ├── NetUpdateTime │ < 2ms ✅ │ 2-5ms ⚠️ │ > 5ms ❌ │
│ ├── FilteringTime │ < 0.5ms │ 0.5-1ms │ > 1ms │
│ ├── PrioritizationTime │ < 0.3ms │ 0.3-0.8ms │ > 0.8ms │
│ └── SerializationTime │ < 1ms │ 1-3ms │ > 3ms │
│ │
│ 📦 数量指标 │
│ ├── ReplicatedObjects │ < 1000 建议 │ 每连接复制的对象数 │
│ ├── DirtyObjects │ < 100 建议 │ 每帧脏对象数 │
│ └── FilteredObjects │ 越多越好 │ 被过滤掉的对象数 │
│ │
│ 📡 带宽指标 │
│ ├── BandwidthUsage │ < 80% limit │ 带宽使用率 │
│ ├── AvgPacketSize │ 500-1200 │ 平均包大小(字节) │
│ └── PacketsPerSecond │ 30-60 │ 每秒数据包数 │
│ │
└────────────────────────────────────────────────────────────────────────┘📊 15.4.3 IrisProfiler:不是“一个类”,而是一套“打点开关”🎯(源码版)
在 UE5.5 的 Iris 里,“Profiler”更多是宏 + 编译开关,你在代码里会看到类似:IRIS_PROFILER_SCOPE(Xxx),它会在录制 Trace 时产生 CPU 事件。
🔎 源码摘录:IRIS_PROFILER_SCOPE 的真身
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Core/IrisProfiler.h
CPP
// IrisProfiler.h(节选)#ifndef IRIS_PROFILER_ENABLE# if (UE_BUILD_SHIPPING)# define IRIS_PROFILER_ENABLE 0# else# define IRIS_PROFILER_ENABLE 1# endif#endif
#if IRIS_PROFILER_ENABLE# include "ProfilingDebugging/CpuProfilerTrace.h"# define IRIS_PROFILER_SCOPE(x) TRACE_CPUPROFILER_EVENT_SCOPE(x)# define IRIS_PROFILER_SCOPE_TEXT(X) TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(X)#else# define IRIS_PROFILER_SCOPE(x)# define IRIS_PROFILER_SCOPE_TEXT(X)#endif🧠 小白理解:这就像你给快递员贴“行程记录仪”。平时不开就不记录;一旦你开始录制 Trace,它就把“每一步花了多少时间”记下来。
📊 Iris 的 CSV 统计:IRIS_CSV_PROFILER_SCOPE
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Core/IrisCsv.h
CPP
// IrisCsv.h(节选)CSV_DECLARE_CATEGORY_EXTERN(Iris);
#if UE_NET_IRIS_CSV_STATS# define IRIS_CSV_PROFILER_SCOPE(CsvCategory, x) \
CSV_SCOPED_TIMING_STAT(CsvCategory, x); \
IRIS_PROFILER_SCOPE(x)#else# define IRIS_CSV_PROFILER_SCOPE(CsvCategory, x) IRIS_PROFILER_SCOPE(x)#endif🧩 小白理解:
IRIS_PROFILER_SCOPE更像“录像”(Insights 时间线);CSV_SCOPED_TIMING_STAT更像“记账”(一列一列的统计表)。
🧑💻 客户端细粒度 CSV:用 net.Iris.EnableDetailedClientProfiler 开关
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Core/IrisProfiler.cpp
CPP
// IrisProfiler.cpp(节选)static FAutoConsoleVariableRef CVarEnableDetailedClientProfilerRef(
TEXT("net.Iris.EnableDetailedClientProfiler"),
bEnableDetailedClientProfiler,
TEXT("Generates detailed CSV Iris stats (client only)."),
ECVF_Default);🔍 15.4.4 使用 Unreal Insights 分析:Iris 的“时间线录像机”⏱️(源码对得上)
在 UE5.5 的 Iris 里,很多“时间线上的 Iris 事件”来自 IRIS_PROFILER_SCOPE(...) —— 而它在源码里最终会落到 TRACE_CPUPROFILER_EVENT_SCOPE(...)(见 15.4.3)。
✅ 你至少需要开 cpu(Iris 并没有单独的 iris TraceChannel)
PLAINTEXT
// 命令行参数(推荐:最稳)
-trace=cpu,net
// 或控制台命令(运行时开启)
Trace.Start cpu,net
...复现问题...
Trace.Stop🧠 小白理解:CPU Trace 就像“把每一帧的耗时都拍成录像”。Iris 在关键流程里打了
IRIS_PROFILER_SCOPE,所以你能在时间线上看到“它到底卡在哪一步”。
📌 你在时间线上会看到什么?(示意图)
PLAINTEXT
┌────────────────────────────────────────────────────────────────────────┐
│ 📊 Unreal Insights 时间线视图(示意) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Frame 1234 │
│ │ │
│ │ GameThread ═══════════════════════════════════════════════════ │
│ │ ├─ Tick ────────────────────────────────────────────────────── │
│ │ │ ├─ Physics ████ │
│ │ │ ├─ Animation ██████ │
│ │ │ └─ Iris_* ████████████████████ │
│ │ │ ├─ ...(Filtering / Poll / Quantize / Write 等) │
│ │ │ └─ ... │
│ │ │
│ │ 0ms 5ms 10ms 15ms 16.67ms (60fps) │
│ │
└────────────────────────────────────────────────────────────────────────┘🔎 实战小技巧:怎么把“时间线峰值”对回到 Iris 的“对象规模”?
时间线发现 Iris 突然变粗:先别急着猜“序列化慢”。
立刻跑两条命令:
Net.Iris.PrintRelevantObjects RepSystemId=0Net.Iris.PrintReplicatedObjects RepSystemId=0 WithSubObjects
✅ 思路:时间线告诉你“哪一段耗时高”,而这两条命令告诉你“是不是对象/子对象规模突然膨胀”。两者合起来,定位会快很多。
📊 15.4.5 CSV 统计输出:Iris 真的在记哪些“账”?(源码版)
先记住一句话:Iris 的很多“性能统计”不是靠 PrintStats 命令打印出来的,而是直接上报到 UE 的 CSV Profiler。
🔎 源码摘录:Iris 在 NetStats.cpp 里定义了大量 CSV 分类
定义位置:
Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Stats/NetStats.cpp
CPP
// NetStats.cpp(节选)static FAutoConsoleVariableRef CShouldIncludeSubObjectWithRoot(
TEXT("net.Iris.Stats.ShouldIncludeSubObjectWithRoot"),
bCVARShouldIncludeSubObjectWithRoot,
TEXT("If enabled SubObjects will reports stats with RootObject..."));
CSV_DEFINE_CATEGORY(IrisPreUpdateMS, WITH_SERVER_CODE);
CSV_DEFINE_CATEGORY(IrisPollMS, WITH_SERVER_CODE);
CSV_DEFINE_CATEGORY(IrisQuantizeMS, WITH_SERVER_CODE);
CSV_DEFINE_CATEGORY(IrisWriteMS, WITH_SERVER_CODE);
CSV_DEFINE_CATEGORY(IrisWriteKBytes, WITH_SERVER_CODE);🧠 小白理解:这相当于 Iris 把“体检项目”提前定好了:预处理、轮询、量化、写入、写入字节数……你录制 CSV 时就能看到这些列。
🔎 源码摘录:Iris 会把统计值写进 CSV(例如 CSV_CUSTOM_STAT)
CPP
// NetStats.cpp(节选)CSV_CUSTOM_STAT(Iris, ReplicatingConnectionCount, Stats.ReplicatingConnectionCount, ECsvCustomStatOp::Set);
CSV_CUSTOM_STAT(Iris, HugeObjectWaitingForAckTimeInSeconds, Stats.HugeObjectWaitingForAckTimeInSeconds, ECsvCustomStatOp::Set);🧾 一个“长得像 CSV”的示例(帮助你理解列是什么)
CSV
Frame,IrisPreUpdateMS,IrisPollMS,IrisQuantizeMS,IrisWriteMS,IrisWriteKBytes
1,0.18,0.42,0.31,0.55,32.4
2,0.20,0.47,0.28,0.62,35.1
3,0.19,0.40,0.29,0.50,29.8✅ 实战建议:如果你在 CSV 里看到
IrisPollMS暴涨,优先怀疑“轮询了太多对象/太频繁”;如果IrisWriteKBytes暴涨,优先怀疑“过滤/条件没挡住,写出去的东西太多”。
📊 15.4.6 NetStats / NetStatsContext:Iris 的“分账本”怎么记出来的?📒(源码版)
很多人看 CSV 只看“列名”,但真正关键是:这些数是谁在什么时机攒出来的。
① 全局发送统计:FNetSendStats(更像“总账”)
位置:
Core/Private/Iris/Stats/NetStats.h
CPP
// NetStats.h(节选)/**
* Send stats for Iris replication reported to the CSV profiler.
* ... intended use is to do thread local tracking ... and then use Accumulate ...
*/class FNetSendStats
{
public:
IRISCORE_API void Accumulate(const FNetSendStats& Stats);
IRISCORE_API void Reset();
IRISCORE_API void ReportCsvStats();
// ... AddNumberOfReplicatedObjects / AddHugeObjectWaitingTime 等
};🧠 小白理解:
FNetSendStats是“今天一共送了多少单、堵车等了多久”这种总账,通常更关心服务器侧。
② 每类/每对象统计:FNetStatsContext + 一组宏(更像“明细账”)
位置:
Core/Private/Iris/Stats/NetStatsContext.h
CPP
// NetStatsContext.h(节选)struct FNetTypeStatsData
{
enum EStatsIndex : unsigned { PreUpdate, Poll, PollWaste, Quantize, Write, WriteWaste, WriteCreationInfo, WriteExports, Count };
struct FStatsValue { uint64 Time = 0; uint32 Bits = 0; uint32 Count = 0; };
FStatsValue Values[EStatsIndex::Count];
};
#if UE_NET_IRIS_CSV_STATS# define UE_NET_IRIS_STATS_TIMER(TimerName, NetStatsContext) UE::Net::Private::FNetStatsTimer TimerName(NetStatsContext);# define UE_NET_IRIS_STATS_ADD_TIME_AND_COUNT_FOR_OBJECT(Timer, StatName, ObjectIndex) /*...*/# define UE_NET_IRIS_STATS_ADD_BITS_WRITTEN_AND_COUNT_FOR_OBJECT(NetStatsContext, BitCount, StatName, ObjectIndex) /*...*/#endif🧠 小白理解:这就像外卖平台的“明细账”:每个菜系(类)、每个订单(对象)分别记:备餐多久(Poll/Quantize)、打包多久(Write)、写了多少字节(Bits)。
③ 两个你会经常用到的“统计开关”(源码可查✅)
是否输出更“啰嗦”的 per-class CSV(非 Shipping 默认开):
net.Iris.UseVerboseIrisCsvStats位置:
Core/Private/Iris/ReplicationSystem/ObjectReplicationBridge.cpp
CPP
// ObjectReplicationBridge.cpp(节选)static FAutoConsoleVariableRef CVarUseVerboseCsvStats(
TEXT("net.Iris.UseVerboseIrisCsvStats"),
bUseVerboseIrisCsvStats,
TEXT("Whether to use verbose per-class csv stats. Default is false in Shipping, otherwise True.")
);SubObject 统计是否并到 Root 上:
net.Iris.Stats.ShouldIncludeSubObjectWithRoot位置:
Core/Private/Iris/Stats/NetStats.cpp
✅ 小白建议:当你看到“每类的 Write/Poll 统计怪怪的”,先确认 SubObject 是否被并账了;否则你会把锅甩错对象类型。
💾 15.5 内存追踪:找出内存"黑洞"
💡 15.5.1 为什么需要内存追踪?
PLAINTEXT
🎮 日常类比:家庭开支记账
不记账:💸 "钱都花哪去了?"
记账后:📊 一目了然!
内存追踪 = 告诉你内存都被谁占用了📊 15.5.2 内存使用分类:别猜“内存去哪了”,看 LLM 怎么分科目✅
在 UE5.5 Iris 里,IrisMemoryTracker 的核心工作是:注册一组 LLM Tag(见 15.5.4 的源码)。所以对新手来说,最靠谱的“分类”就是这些 Tag:
LLM Tag | 你可以把它想成 | 常见含义 |
|---|---|---|
| “Iris 总账” | Iris 相关内存总和 |
| “状态缓存” | 各类复制状态/状态数据的占用 |
| “初始化开销” | 初始化/构建协议等阶段的占用 |
| “连接分摊” | 每连接相关的缓存、结构等占用 |
🧠 小白理解:别纠结“ProtocolData/FilterData”这种你自己起的名字;先用引擎已经分好的科目,你才知道该去哪个模块继续深挖。
📋 15.5.3 你会在 LLM 里看到什么?(输出示意)
PLAINTEXT
LLM (示意)
- NetworkingSummary
- Iris
- State
- Initialization
- Connection✅ 实战建议:如果
IrisConnection随在线人数线性上涨,很可能是“每连接缓存过大/没释放”;如果IrisState随场景对象数量上涨,很可能是“状态缓存/对象数失控”。
🔍 15.5.4 源码对照:IrisMemoryTracker 其实是 LLM 标签体系
很多文章会把“内存追踪”说成一个独立系统,但在 Iris 里它更接近:给 LLM(Low Level Memory Tracker)注册一组 Tag,这样你就能在 LLM 视图里看到 Iris 的内存占用。
声明 Tag:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Core/IrisMemoryTracker.h定义 Tag:
Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Core/IrisMemoryTracker.cpp
CPP
// IrisMemoryTracker.h(节选)LLM_DECLARE_TAG_API(Iris, IRISCORE_API);
LLM_DECLARE_TAG_API(IrisState, IRISCORE_API);
LLM_DECLARE_TAG_API(IrisInitialization, IRISCORE_API);
LLM_DECLARE_TAG_API(IrisConnection, IRISCORE_API);
// IrisMemoryTracker.cpp(节选)LLM_DEFINE_TAG(Iris, NAME_None, NAME_None, GET_STATFNAME(STAT_IrisLLM), GET_STATFNAME(STAT_NetworkingSummaryLLM));
LLM_DEFINE_TAG(IrisState, "State", "Iris", GET_STATFNAME(STAT_IrisStateLLM), GET_STATFNAME(STAT_NetworkingSummaryLLM));
LLM_DEFINE_TAG(IrisInitialization, "Initialization", "Iris", GET_STATFNAME(STAT_IrisInitializationLLM), GET_STATFNAME(STAT_NetworkingSummaryLLM));
LLM_DEFINE_TAG(IrisConnection, "Connection", "Iris", GET_STATFNAME(STAT_IrisConnectionLLM), GET_STATFNAME(STAT_NetworkingSummaryLLM));✅ 小白操作思路:你把 LLM 打开,就能看到 “Iris / State / Initialization / Connection” 这些分项。它不是“猜内存去哪了”,而是“把账本分了科目”。📒
🚨 15.6 常见问题排查指南
💡 15.6.1 问题排查的"望闻问切"
PLAINTEXT
🎮 日常类比:中医看病的四诊法
👀 望:观察日志、统计数据
👂 闻:监听网络流量、事件
💬 问:了解问题发生的场景
✋ 切:使用调试工具深入分析🔍 15.6.2 问题 1:对象未复制(按源码能落地的排查路径)
排查步骤:
PLAINTEXT
Step 1: 先确认“对象到底有没有注册进 Iris”
Net.Iris.PrintReplicatedObjects RepSystemId=0
→ 在输出里搜对象名(或类名)
→ 如果根本找不到:问题通常在“注册/桥接层/生命周期”
Step 2: 再确认“对象是不是被过滤/不相关了”
Net.Iris.PrintRelevantObjectsToConnection RepSystemId=0 ConnectionId=1 WithFilter
→ 这个命令会在输出里附带过滤信息(WithFilter)
→ 如果对象在 ReplicatedObjects 里,但不在 RelevantObjects 里:优先查过滤/裁剪距离
Step 3: 快速验证“是不是距离裁剪导致消失”
Net.Iris.PrintNetCullDistances RepSystemId=0 NumClasses=20
→ 看你关心的类,MostCommon NetCullDistance 是多少
Step 4: 只要你怀疑“某个对象一处理就出错”,直接在案发点断下来
Net.Iris.DebugName=MyBoss (或用 Net.Iris.DebugNetRefHandle 指定句柄)
→ 命中就 UE_DEBUG_BREAK(),你能直接看调用栈
Step 5: 回到基础:属性是否真的可复制
✅ UPROPERTY(Replicated)
✅ GetLifetimeReplicatedProps 里注册
✅(如果是 Push Model)修改后记得“标脏”(告诉系统它变了)🔎 源码依据:
Net.Iris.PrintReplicatedObjects/Net.Iris.PrintRelevantObjectsToConnection/Net.Iris.PrintNetCullDistances/Net.Iris.DebugName都能在 UE5.5 源码中找到注册位置(见本章 15.3.2 与 15.3.1)。
🔍 15.6.3 问题 2:属性不同步(“对象在,但某个字段不动”)
排查步骤:
PLAINTEXT
Step 1: 先确认“对象这条链路是通的”
Net.Iris.PrintReplicatedObjects RepSystemId=0
Net.Iris.PrintRelevantObjectsToConnection RepSystemId=0 ConnectionId=1 WithFilter
→ 对象必须“已注册 + 对该连接相关”,否则你盯着属性看是白费劲
Step 2: 再看“是不是复制条件把你挡在门外了”
常见条件(在 Gameplay 代码里配):
- COND_OwnerOnly / COND_SkipOwner
- COND_InitialOnly
→ 现象:服务器改了,客户端永远收不到(其实是条件没满足)
Step 3: 用 Iris 自带命令确认 PushBased 状态(源码真实存在✅)
Net.Iris.PrintPushBasedStatuses
→ 它会打印哪些类是 fully push-based,并列出“不是 push-based 的属性路径”
→ 如果你改的属性属于 push-based:必须在 Gameplay 层做“标脏”(否则 Iris 认为“没变过”)
Step 4: 还不对?考虑“你看到的是量化后的值”
现象:服务器 100.123456,客户端 100.12
→ 这不是不同步,而是量化精度导致的“正常舍入”
→ 线索通常在具体序列化器的 Quantize/Dequantize(可回看第七部分)🔎 源码依据:
Net.Iris.PrintPushBasedStatuses注册于ObjectReplicationBridgeDebugging.cpp,用于快速判断“这类到底是不是 push-based”。
🔍 15.6.4 问题 3:性能瓶颈(“录像 + 记账”双管齐下)
排查步骤:
PLAINTEXT
Step 1: 先用 Unreal Insights 录“录像”(看时间线)
命令行:-trace=cpu,net
或控制台:Trace.Start cpu,net
复现后:Trace.Stop
Step 2: 如果想要“表格化记账”,用 CSV Profiler(Iris 会上报 Iris* 类别)
- Iris 在源码里用 CSV_DEFINE_CATEGORY/CSV_CUSTOM_STAT 上报(见 15.4.5)- 想要更细:检查 net.Iris.UseVerboseIrisCsvStats / net.Iris.EnableDetailedClientProfiler
Step 3: 用 Iris 自带的“对象维度”命令快速判断规模是否异常
Net.Iris.PrintRelevantObjects RepSystemId=0
Net.Iris.PrintReplicatedObjects RepSystemId=0 WithSubObjects
Step 4: 对症下药(把锅甩给正确的人😉)
- IrisPollMS 高:降低轮询压力(例如更合理的 PollPeriod / Dormancy / 过滤)- IrisWriteKBytes 高:过滤/条件/量化/增量压缩有没有真正生效- 大量 SubObject 计入 Root:尝试切换 net.Iris.Stats.ShouldIncludeSubObjectWithRoot📋 15.6.5 问题排查快速参考表(按 UE5.5 源码存在的命令)
现象 | 可能原因 | 最快验证手段 |
|---|---|---|
客户端看不到对象 | 没注册进 Iris |
|
对某玩家不可见 | 被过滤/距离裁剪 |
|
“敌人消失/闪烁” | NetCullDistance/过滤抖动 |
|
属性不更新 | 非 push-based / 未标脏 |
|
性能突然爆炸 | 轮询/写出量暴增 | Insights( |
内存上涨 | 状态缓存/连接/初始化占用 |
|
📋 15.7 总结与最佳实践
🎯 15.7.1 核心概念总结
PLAINTEXT
┌────────────────────────────────────────────────────────────────────────┐│ 📋 第十五部分知识点总结 │├────────────────────────────────────────────────────────────────────────┤│ ││ 📝 日志系统 ││ ├── LogIris 主日志类别及子类别 ││ ├── 7 个日志级别(Fatal → VeryVerbose) ││ └── 配置文件 / 代码 / 控制台 三种配置方式 ││ ││ 🔧 调试工具 ││ ├── IrisDebugHelper:断点条件/调试输出(Watch/Immediate Window 友好) ││ ├── 控制台命令:Net.Iris.PrintReplicatedObjects / PrintRelevant... ││ └── “可视化”主要靠:日志打印 + Unreal Insights 时间线 ││ ││ 📈 性能分析 ││ ├── IrisProfiler:时间统计、计数器 ││ ├── Unreal Insights:详细时间线分析 ││ └── IrisCsv:CSV 数据导出 ││ ││ 💾 内存追踪 ││ ├── IrisMemoryTracker:注册 LLM Tag(Iris/State/Initialization/Connection)││ └── 用 LLM Tag 追“哪一科目在涨” ││ ││ 🚨 问题排查 ││ ├── 对象未复制:注册、过滤、属性配置 ││ ├── 属性不同步:复制条件、Push Model、序列化器 ││ └── 性能瓶颈:分阶段分析、Insights 深入追踪 ││ │└────────────────────────────────────────────────────────────────────────┘✅ 15.7.2 最佳实践清单
PLAINTEXT
┌────────────────────────────────────────────────────────────────────────┐
│ ✅ 调试与性能分析最佳实践 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 🔧 开发阶段 │
│ □ 启用开发版日志级别 │
│ □ 配置 Unreal Insights 追踪 │
│ □ 启用可视化调试工具 │
│ □ 记录基准性能数据 │
│ │
│ 🧪 测试阶段 │
│ □ 测试对象创建和销毁 │
│ □ 测试属性同步 │
│ □ 测试过滤器效果 │
│ □ 进行性能回归测试 │
│ │
│ 🚀 发布阶段 │
│ □ 关闭详细日志(只保留 Warning 以上) │
│ □ 禁用可视化调试 │
│ □ 设置性能监控告警 │
│ │
│ 🔍 问题排查 │
│ □ 先看日志,再用工具 │
│ □ 从现象定位到具体模块 │
│ □ 使用控制台命令快速验证 │
│ □ 记录问题和解决方案 │
│ │
└────────────────────────────────────────────────────────────────────────┘📊 15.7.3 调试命令速查表(按 UE5.5 源码存在的命令)
场景 | 命令 | 说明 |
|---|---|---|
先确认“对象有没有进 Iris” |
| 输出里搜对象名/类名 |
看“对某连接是否相关” |
| 附带过滤信息(WithFilter) |
看“对任意连接相关” |
| 快速判断规模是否异常 |
看“永远相关列表” |
| 验证 AlwaysRelevant |
看“裁剪距离分布” |
| 排查“距离裁剪导致消失/闪烁” |
看 PushBased 状态 |
| 判断“是否需要标脏/哪些属性不是 push-based” |
看动态过滤映射 |
| 类 -> 动态过滤器映射 |
命中即断点 |
| 命中对象/类名就 |
录 Insights 时间线 |
| Iris 的 |
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)
