资产诊断:总加载时间不能解释内容为什么没显示

自动关联目录:资产诊断:总加载时间不能解释内容为什么没显示

核心判断:资产与渲染诊断不能只记录“加载耗时”和“当前 FPS”。每个指标必须绑定逻辑资产、目标变体、请求代际、World、运行时对象和生命周期阶段;否则只能证明结果变差,不能解释成本发生在哪里,更无法判断内容为何没有显示。

一、业务冲突:同一个空白画面,可能是七种完全不同的故障

玩家看到“内容迟迟没出现”,开发侧却可能面对不同事实:引用解析到了错误变体、文件已下载但校验失败、缓存里有字节却没有原子安装、UObject 已加载但没有强引用、Actor 已提交生成但组件尚未注册、材质或纹理仍在异步应用、对象已经 Ready 却被可见性策略裁掉。

“卡顿”同样不能由一个 FPS 解释。主线程可能在批量 Spawn、Decode、创建 DMI 或广播属性;Render Thread 可能在重建 SceneProxy;GPU 可能受透明 Overdraw、阴影或 Shader 首次使用影响;UI 还可能独立增长 Slate Batch。只看最终帧时间,会把原因相反的问题归为同一类。

早期最自然的做法是记录一次总加载耗时,再采样 FPS、Game Thread、Render Thread、内存、Draw Call 和面数。这能发现“有问题”,但无法回答三个关键问题:时间花在哪一段、这段工作属于哪个资产请求、显示完成的判定究竟是什么。

诊断系统真正要交付的不是更多数字,而是一条可以复盘的因果链:哪个需求,在什么目标和代际下,从哪一个状态进入下一状态,耗费了什么资源,最后成功、回退、取消还是被旧结果淘汰。

二、实际实现:从平均分级演进到每 World 的结构化快照

早期创作期工具已经做了两件有价值的事。静态检查按全局与区域返回移动对象、透明重叠、局部密度、三角形、灯光、组件 Tick 和 UI 控件等具体原因,区域问题还能直接定位相机;实际游玩测试按固定周期记录 FPS、GT、RT、进程内存、Draw Call 和面数,最后取平均并以最差维度给出风险等级。

运行时代码也逐步加入 UE Trace Scope、Stat Counter 和 CSV 指标。网格完成、材质应用、碰撞重建、属性批处理、拓扑维护和资源统计都有独立 CPU 区间,至少可以在 Unreal Insights 中看到尖峰属于哪个执行阶段,而不是只看到一段匿名 Game Thread 时间。

后续流式系统进一步把观测收敛成每个 UWorld 的性能快照,不再由调试 UI 每帧扫描场景。快照同时记录:

  • 帧时间滑窗、P95、峰值、连续超预算帧,以及流式活跃与稳定区间的帧率差。
  • 候选对象、驻留 Actor、流入流出数量与字节吞吐、等待中的预加载和首次可玩时间。
  • 进程内存趋势、内存压力后的回收延迟,以及按 Mesh、Material、Texture 分类的估算驻留量。
  • 远景代理数量、等待与失败请求、切档次数、代理三角形、Section 和估算 GPU 字节。
  • Draw Call、Primitive 与 GPU 帧时间。

资源统计不是把每个 Actor 的引用简单相加,而是维护 对象 -> 资源 与 资源 -> 引用计数 两层关系;共享资源只在第一个对象进入时计费,最后一个对象离开时扣除。这比“对象数乘平均大小”更接近 UE 中共享 UObject 的驻留语义。

重建路径还保存机器可读的原因位、重建类型、触发来源、缓存失效范围、脏根数量、资源查询与缓存命中、各阶段耗时、是否回退和失败原因。同一份快照能进入屏幕 Overlay、UE Stat、CSV 和 JSON Dump,并有自动化测试覆盖 P95、资源去重、首次记录、预加载成功率、内存增长和序列化字段。

三、值得保留:让指标靠近状态所有者

第一,状态所有者负责产出快照,展示层只消费。流式管理器最清楚候选、需求、驻留和淘汰;LOD 管理器最清楚代理请求、当前档位和失败资产。把统计放在它们内部,既避免调试 UI 重扫全场,也减少“展示口径”和“真实状态”分叉。

第二,平均值、尾延迟和连续性同时保留。平均 FPS 适合看总体趋势,P95 与峰值才能看到尖峰,连续超预算帧才能区分偶发抖动和持续失速。早期只取平均的报告容易把规律性卡顿稀释掉,后续滑窗是实质改进。

第三,总内存和领域驻留量分开。进程物理内存能反映最终风险,按唯一资源去重后的 Mesh、Material、Texture 估算则帮助定位流式系统自身。两个口径都需要,但必须明确前者含有 UI、脚本、物理和引擎分配,后者是 GetResourceSize 一类估算,并非驱动层精确显存。

第四,原因码与结果指标并存。仅记录一次重建耗时无法解释为什么发生全量回退;仅记录“拓扑变化”又无法判断代价。原因、来源、回退、输入规模、分阶段耗时和结果放在同一快照中,才具备回归比较价值。

四、结构性缺陷:快照能描述局部,却串不起完整生命周期

现有快照主要覆盖运行时流式与渲染,构建、下载、缓存安装、Decode、材质准备和最终显示之间没有共同关联身份。一个阶段打印逻辑资产,另一个阶段打印文件或对象,异步回调再只剩请求序号;即使所有日志都在,也很难证明它们属于同一次需求。最新快照还会覆盖上一次状态,无法回答“这个对象此前为何回退过”。

“首次可玩”的语义也偏乐观。当前判断主要检查近区与常驻对象是否进入逻辑驻留集合,而 Spawn 请求提交后对象就可能先被加入该集合;这不能推出 Actor 已真实存在,更不能推出 Mesh、Material、Texture、组件注册和 Render State 已完成。这个值适合作为“逻辑区块满足时间”,不应直接命名为“画面可玩完成时间”。

若测量窗口没有语义,公式也会误导。固定样本数的 P95 在不同帧率下代表不同时间长度;内存斜率依赖固定采样周期,掉采样后用样本序号代替时间会失真;全局 Draw Call 和 GPU 时间无法说明某个流式对象的增量;进程内存上升也可能与资产系统无关。每个指标必须携带时间窗、采样频率、可用性和归因范围。

故障输出仍混有临时日志与自由文本。一次特定的卸载异常可以靠打印焦点坐标定位,但这种日志没有稳定 Schema,无法与后续重建、资源请求和对象代际自动关联。失败字符串适合人读,自动回归还需要有限、版本化的 Reason Code;无法获取的 GPU、物理或渲染指标也必须标记 Unavailable,不能用零混淆“没有成本”和“没有采到”。

五、目标设计:用 Trace Contract 连接状态,而不是连接日志文本

目标模型把资产生命周期定义为一组有明确准入条件的状态。每次 Demand 创建关联上下文,跨线程和异步回调只传递这份上下文的不可变副本;状态转移既更新运行事实,也写结构化事件。

stateDiagram-v2
    [*] --> SourceValidated
    SourceValidated --> ArtifactBuilt
    ArtifactBuilt --> ArtifactVerified
    ArtifactVerified --> Acquired
    Acquired --> Installed
    Installed --> Resident
    Resident --> Materialized
    Materialized --> PresentationReady
    PresentationReady --> FirstPresented

    SourceValidated --> Failed
    ArtifactBuilt --> Failed
    ArtifactVerified --> Failed
    Acquired --> Failed
    Installed --> Failed
    Resident --> Cancelled
    Materialized --> Stale
    Failed --> Fallback
    Stale --> Fallback
    Fallback --> PresentationReady

PresentationReady 表示当前 Generation 的必要依赖已驻留,Actor 与组件有效,材质和纹理已提交,旧表示可以安全替换;它仍不等于像素已经出现在屏幕。FirstPresented 应由渲染侧可验证信号或固定相机测试定义。DS 没有视觉阶段,应在 MaterializedLogic 结束并把渲染指标标记为不可用,而不是伪造一组零值。

struct AssetTraceContext
{
    LogicalAssetId asset;
    Revision revision;
    Target target;
    DemandId demand;
    Generation generation;
    WorldId world;
    optional<RuntimeEntityId> entity;
};

struct StageEvent
{
    AssetTraceContext context;
    AssetStage from;
    AssetStage to;
    TimePoint begin;
    TimePoint end;
    Outcome outcome;       // Success / Fallback / Cancelled / Stale / Failed
    ReasonCode reason;
    MetricDelta delta;     // bytes、队列等待、GT/RT 时间、驻留变化
};

ReadyResult TryCommitPresentation(const AssetTraceContext& c, RuntimeEntity& entity)
{
    if (c.generation != entity.CurrentGeneration())
        return Emit(c, Stale, ReasonCode::GenerationMismatch);
    if (!AllRequiredArtifactsVerified(c.target) || !AllDependenciesResident())
        return Pending;
    if (!entity.ActorLive() || !RequiredComponentsRegistered() || !MaterialStateCommitted())
        return Pending;

    SwapOldAndNewRepresentationAtomically();
    return Emit(c, PresentationReady, ReasonCode::None);
}

Trace Event 是低基数、可长期比较的事实;UE Trace Scope 负责定位函数级 CPU 细节,Stat/CSV 负责时序曲线,屏幕 Overlay 负责现场观察,JSON Snapshot 负责复现附件。它们共享字段口径,但不应由一种工具替代所有层级。

六、决策规则:先找最后一个可信状态,再看性能数字

诊断“内容没显示”时,第一步不是开 GPU Profile,而是找到该 Demand 的最后一个成功状态:未到 ArtifactVerified,查构建、目标兼容和依赖;已验证但未 Installed,查队列、下载、校验、临时文件与原子切换;已安装但未 Resident,查内存预算、引用与淘汰;已驻留但未 PresentationReady,查旧 Generation、Actor、组件、材质和主线程应用队列;已 Ready 但未 FirstPresented,再查 Bounds、裁剪、隐藏状态、材质 Pass、SceneProxy 与相机。

诊断卡顿时,用 Stage Event 与帧时间尖峰做相关,再按 UE 线程拆分:GT 尖峰优先看 Decode、Spawn、DMI、通知和 UI 失效;RT 尖峰看 Render State 与 SceneProxy 重建;GPU 尖峰看阴影、透明 Overdraw、材质、Primitive 和分辨率。诊断内存时,先区分进程总量与去重后的领域驻留,再检查 Demand 数、引用计数、淘汰后回收延迟和短期重复流入。

统一度量口径如下,阈值由目标平台和场景基线决定,不使用历史真实值:

阶段指标与公式必须携带的维度
构建BuildLatency = T_verified - T_buildStart;制品字节;增量复用率 ReusedValidInputs / ValidInputsRevision、Target、处理器版本、冷/热构建
依赖直接与传递依赖数、最大深度、循环与缺失数;FanOut = UniqueTransitiveDeps / RootAssets资产类型、Target、可选/必要依赖
获取与安装排队、传输、校验、安装分段 P50/P95;VerifiedThroughput = VerifiedBytes / TransferTime缓存/网络来源、重试、取消、错误码
缓存VerifiedHitRate = VerifiedHits / Lookups;损坏、修复、驱逐和版本共存量Revision、Target、命名空间、命中是否通过完整校验
准备与应用Decode、Prepare、Game Thread Apply、队列年龄 P95;每帧提交量与失败回退资产类型、批次、Generation、同步/异步入口
可见就绪TTVR = T_presentationReady - T_demand,并拆为获取、驻留、物化、提交World、Entity、占位/回退、首次/重复请求
驻留与内存唯一资源估算量、进程峰值、回收延迟;ThrashRate = ReenterSoonAfterEvict / EvictionsMesh/Texture/Material/UI、预算档、压力状态
材质与 ShaderVariant 数、Shader Code、PSO 首次使用、DMI 创建与参数应用耗时父材质族、VariantKey、平台、画质档
渲染GT/RT/GPU P50/P95/P99、Draw Call、Primitive、三角形、Overdraw、Slate Batch固定相机、分辨率、画质、冷/热缓存、流式活跃状态
正确性旧代际拒绝、取消后完成、显式回退、空洞帧和状态回退次数Outcome、ReasonCode、前后状态、关联上下文

按实际需求门槛复核,本章纳入的每一段都有对应工程触发:多目标制品与增量重建要求构建指标;共享请求、缓存校验和安装恢复要求获取指标;异步 Actor 与依赖到达要求可见就绪状态;批量材质、碰撞和组件通知要求 GT 分阶段耗时;远景代理和流式淘汰要求驻留、吞吐与抖动指标;创作期热点、运行时滑窗和 UI Batch 增长要求分别观察内容、3D 与 UI。没有发现已经接通的热状态自适应闭环,也没有完整的跨构建到首帧 Trace,因此它们只作为目标设计,不写成既有能力;运营监控和用户行为分析仍不在范围内。

我的反思是:过去容易把“增加一个 Stat”当成可观测性建设,但没有生命周期身份和完成语义,Stat 只是更精确的孤岛。后续实现把平均分级推进到 P95、资源去重、原因归因和可导出快照,方向是正确的;下一步真正有价值的升级,是让一次 Demand 的状态与代价可以跨阶段追踪。先证明对象停在哪个状态,再分析这个状态为什么慢,远比从最终 FPS 倒推原因可靠。