Unity 协程运行时的监控和优化

来源:互联网 发布:cacti for windows 编辑:程序博客网 时间:2024/05/29 07:36


原文地址:https://zhuanlan.zhihu.com/p/24519241?refer=gu-lu


Unity 协程运行时的监控和优化

协程 (Coroutine) 是大部分现代编程环境都提供的一个非常有用的机制。它允许我们把不同时刻发生的行为,在代码中以线性的方式聚合起来。与基于事件与回调的系统相比,以协程方式组织的业务逻辑,可读性相对好一些。

Unity 内的协程实现是传统协程的简化——在主线程内每一帧给定的时间点上,引擎通过一定的调度机制来唤醒和执行满足条件的协程,以实际上的分时串行化执行回避了协程之间的通信问题。但由于种种因素,协程的执行情况对程序员而言相对不那么透明,可以通过一些简单的机制来对其进行监控和优化。

Warm up: 从复用 Yield 对象说起

先从一个最简单而直接的改进开始吧。下面一个在每帧结束时执行的协程的例子:

void Start(){    StartCoroutine(OnEndOfFrame());}IEnumerator OnEndOfFrame(){    yield return null;    while (true)    {        //Debug.LogFormat("Called on EndOfFrame.");        yield return new WaitForEndOfFrame();    }}

在 Profiler 内可以看到,上面的代码会导致 WaitForEndOfFrame 对象的每帧分配,给 GC 增加负担。假设游戏内有 10 个活跃协程,运行在 60 fps,那么每秒钟的 GC 增量负担是 10 * 60 * 16 = 9.6 KB/s。

我们可以简单地通过复用一个全局的 WaitForEndOfFrame 对象来优化掉这个开销:

static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();

在合适的地方创建一个全局共享的 _endOfFrame 之后,只需要把上面的代码改为

    ...    yield return _endOfFrame;    ...

上面的 9.6 KB/s 的 GC 开销就被完全避免了,而逻辑上与优化前完全没有任何区别。

实际上,所有继承自 YieldInstruction 的用于挂起协程的指令类型,都可以使用全局缓存来避免不必要的 GC 负担。常见的有:

  • WaitForSeconds
  • WaitForFixedUpdate
  • WaitForEndOfFrame

在 Yielders.cs 这个文件里,集中地创建了上面这些类型的静态对象,使用时可以直接这样:

    ...    yield return Yielders.GetWaitForSeconds(1.0f);  // wait for one second    ...

Coroutine 的工作原理

观察调用链可知,Unity Coroutine 的调用约定靠返回的 IEnumerator 对象来维系。我们知道 IEnumerator 的核心功能函数是:

    bool MoveNext();

这个函数在每次被 Unity 协程调度函数 (通常是协程所在类的 SetupCoroutine()) 唤醒时调用,用于驱动对应的协程由上一次 yield 语句开始执行下面的代码段,直到下一条 yield 语句 (对应返回 true) 或函数退出 (对应返回 false)。

下图是一次典型的协程调用:

图中的绿色实心方块是协程实际的活跃执行时间。可以看出,一个协程的完整生命周期是“在整个生命周期内对其内部所有代码段的一个遍历并依次执行”的过程。

接管和监控 Coroutine 的行为

问题描述

由于以下几点问题的存在,协程的执行情况对开发者而言并不透明,很容易在开发过程中引入性能问题。

  1. 协程 (除了首次执行) 不是在用户的函数内触发,而是在单独的 SetupCoroutine() 内被激活并执行
  2. 协程的每次活跃执行,在代码上以单次 yield 为界限。对于具有复杂分支的业务逻辑,尤其是“本来在主流程内,后来被协程化”的代码,很难看出每一段 yield 的潜在执行量
  3. 实践中,如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧。这一类卡顿难以复现和调查。

中间层 TrackedCoroutine

针对这些情况,我们可以在主流程和协程之间添加一层 Wrapper,来接管和监控实际协程的执行情况。具体地说,可以实现一个纯转发的 IEnumerator,如下的缩减版所示:

public class TrackedCoroutine : IEnumerator{    IEnumerator _routine;    public TrackedCoroutine(IEnumerator routine)    {        _routine = routine;        // 在这里标记协程的创建    }    object IEnumerator.Current    {        get        {            return _routine.Current;        }    }    public bool MoveNext()    {        // 在这里可以:        //     1. 标记协程的执行        //     2. 记录协程本次执行的时间        bool next = _routine.MoveNext();        if (next)        {            // 一次普通的执行        }        else        {            // 协程运行到末尾,已结束        }        return next;    }    public void Reset()    {        _routine.Reset();    }}

完整版的代码见 TrackedCoroutine 类的实现。

有了这样一个 TrackedCoroutine 之后,我们就可以把正常的

abc.StartCoroutine(xxx());

替换为

abc.StartCoroutine(new TrackedCoroutine(xxx()));

启动函数 InvokeStart()

在 RuntimeCoroutineTracker 类中,可以看到以下两个接口,针对以 IEnumerator,string,及可选的单参形式等三种形式的协程启动的封装。

public class RuntimeCoroutineTracker{    public static Coroutine InvokeStart(MonoBehaviour initiator, IEnumerator routine);    public static Coroutine InvokeStart(MonoBehaviour initiator, string methodName, object arg = null);}

上面的外部调用就可以替换为:

RuntimeCoroutineTracker.InvokeStart(abc, xxx());

至此,藉由一个中间层 TrackedCoroutine,我们得以接管和监控所有协程的单次运行过程。

监控 Plugins 内的协程

由于 Plugins 目录单独编译,无法直接调用外部的功能,这里我们为所有的插件提供一个转发机制,用于把插件内启动协程的请求转发到上面的启动函数。

首先定义两个委托:

public delegate Coroutine CoroutineStartHandler_IEnumerator(MonoBehaviour initiator, IEnumerator routine);public delegate Coroutine CoroutineStartHandler_String(MonoBehaviour initiator, string methodName, object arg = null);

然后把实际的协程请求转发给这两个委托:

public class CoroutinePluginForwarder{    ...    public static Coroutine InvokeStart(MonoBehaviour initiator, IEnumerator routine)    {        return InvokeStart_IEnumerator(initiator, routine);    }    public static Coroutine InvokeStart(MonoBehaviour initiator, string methodName, object arg = null)    {        return InvokeStart_String(initiator, methodName, arg);    }    ...}

最后在运行时注册两个委托即可:

CoroutinePluginForwarder.InvokeStart_IEnumerator = RuntimeCoroutineTracker.InvokeStart;CoroutinePluginForwarder.InvokeStart_String = RuntimeCoroutineTracker.InvokeStart;

完整的代码实现见 CoroutinePluginForwarder 类。

PerfAssist 组件 - CoroutineTracker (on GitHub)

在上面这些实现的基础上,前段时间我实现了一个编辑器内的工具面板 CoroutineTracker ,用于帮助开发者监控和分析系统内协程的运行情况。

  • PerfAssist/PA_CoroutineTracker

功能介绍

左边的四列是程序运行时所有被追踪协程的实时的启动次数,结束次数,执行次数和执行时间。

当点击图形上任何一个位置时,选中该时间点(秒为单位),在图形上是绿色竖条。

此时右边的数据报表刷新为在这一秒中活动的所有协程的列表,如下图所示:

注意,该表中的数据依次为:

  • 协程的完整修饰名 (mangled name)
  • 在选定时间段内的执行次数 (selected execution count)
  • 在选定时间段内的执行时间 (selected execution time)
  • 到该选中时间为止时总的执行次数 (summed execution count)
  • 到该选中时间为止时总的执行时间 (summed execution time)

可以通过表头对每一列的数据进行排序。

当选中列表中某一个协程时,面板的右下角会显示该协程的详细信息,如下图所示:

这里有下面的信息:

  • 该协程的序列 ID (sequence ID)
  • 启动时间 (creation time)
  • 结束时间 (termination time)
  • 启动时堆栈 (creation stacktrace)

向下滚动,可看到该协程的完整执行流程信息,如下图所示:

常见问题调查

使用这个工具,我们可以更方便地调查下面的问题:

  • yield 过于频繁的
  • 单次运行时间太久的
  • 总时间开销太高的
  • 进入死循环,始终未能正确结束掉的
  • 递归 yield 产生过深执行层次的

[完]

Gu Lu
[2016-12-20]

[注]

  • 本文同时发在我的 blog 和知乎专栏
  • 本文已授权侑虎科技的公众号转载
  • 本文遵循 Creative Commons BY-NC-ND 4.0 许可协议。
  • CoroutineTracker 工具是 PerfAssist 套件的一部分,后续的改进和更新都会出现在那里。
  • 如果在使用时遇到问题,欢迎直接在 GitHub 上发 Issues 或 Pull Requests 给我,往往能比评论得到更快速的回复。

[补]

  • [2017-01-06] 多谢评论中的 @CM 君,此问题已修复。

    • @CM 君提到,“hi, 我看到Yielders注释写道Dictionary以值类型作Key会产生GC,不是是否有进行过实际的测试。我在Profiler中看过Dictionary的ContainsKey、ContainsValue、[Key]等操作均无GC产生。”
    • 这段代码的原出处在这里。我在 Unity 5.5.0 下已验证,以 float 作为 Dictionary 的 Key 时,确实不会像注释中描述的那样,产生 GC。最新的 Yielders.cs 中已修复此情况。
  • [2017-01-06] 评论中提到的找不到文件的情况,是因为所缺的问价在子库 PA_Common中,见此页面上对该子库的引用。使用 "git submodule ..." 更新到本地即可。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 打架斗殴被关在看守所了怎么办 在看守所羁押期间患癌症怎么办 无法偿还借款拘留15天后怎么办 离婚起诉被告人被羁押怎么办 事实不清的案件怎么办 交通事故没时间去做笔录怎么办 偷东西被拘留家长该怎么办 在香港被拘留了怎么办 发票认证机卡了怎么办 交罚款的单子丢了怎么办 父亲行政拘留考警察政审不过怎么办 非法经营罪立案后不批刑拘怎么办 12分扣完了怎么办2018 驾驶证c1扣14分怎么办 车辆被扣12分怎么办 两个违章扣12分怎么办 车辆违章扣12分怎么办 一下扣了20分怎么办 违章扣了100多分怎么办 车子累计扣12分怎么办 起诉了对方不来怎么办 在监狱里被打伤了怎么办 初三要体检没去怎么办 羁押人在看守所没判刑怎么办? 在看守所关两年了还没有判刑怎么办 开麻将馆被拘留怎么办 拘留31天了我该怎么办 收到一封拘留信怎么办 存钱的收据掉了怎么办 行政拘留法制没有批的怎么办 别人起诉我我该怎么办 去钟落潭看守所送衣服要怎么办 长城宽带账号密码忘了怎么办 预约考试密码忘了怎么办 健康证预约号忘记怎么办啊 人在看守所七个月还没结果怎么办 起诉书和判决书丢了怎么办 进了看守所信用卡逾期怎么办 公安局审讯室监控影相被删除怎么办 关进看守所以前的工作怎么办 上海初中借读生学籍怎么办