Unity 协程运行时的监控和优化
我是快乐的搬运工: http://gulu-dev.com/post/perf_assist/2016-12-20-unity-coroutine-optimizing#toc_0
------------------------------------------------------------------------ 分割线 ----------------------------------------------------------------------------
目录:
Warm up: 从复用 Yield 对象说起
Coroutine 的工作原理
接管和监控 Coroutine 的行为 ◦ 问题描述
中间层 TrackedCoroutine
启动函数 InvokeStart()
监控 Plugins 内的协程
PerfAssist 组件 - CoroutineTracker (on GitHub) ◦ 功能介绍
常见问题调查
协程 (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 的行为
问题描述
由于以下几点问题的存在,协程的执行情况对开发者而言并不透明,很容易在开发过程中引入性能问题。
- 协程 (除了首次执行) 不是在用户的函数内触发,而是在单独的
SetupCoroutine()
内被激活并执行 - 协程的每次活跃执行,在代码上以单次 yield 为界限。对于具有复杂分支的业务逻辑,尤其是“本来在主流程内,后来被协程化”的代码,很难看出每一段 yield 的潜在执行量
- 实践中,如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧。这一类卡顿难以复现和调查。
中间层 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 ,用于帮助开发者监控和分析系统内协程的运行情况。
https://github.com/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 产生过深执行层次的
Unity 协程运行时的监控和优化的更多相关文章
- Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...
- Unity协程(Coroutine)原理深入剖析(转载)
记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...
- 深入浅出!从语义角度分析隐藏在Unity协程背后的原理
Unity的协程使用起来比较方便,但是由于其封装和隐藏了太多细节,使其看起来比较神秘.比如协程是否是真正的异步执行?协程与线程到底是什么关系?本文将从语义角度来分析隐藏在协程背后的原理,并使用C++来 ...
- Unity协程(Coroutine)原理深入剖析再续
Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...
- 【转】Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 记得去年6月份刚开始实习的时候,当时要我写网 ...
- 聊一聊Unity协程背后的实现原理
Unity开发不可避免的要用到协程(Coroutine),协程同步代码做异步任务的特性使程序员摆脱了曾经异步操作加回调的编码方式,使代码逻辑更加连贯易读.然而在惊讶于协程的好用与神奇的同时,因为不清楚 ...
- unity协程coroutine浅析
转载请标明出处:http://www.cnblogs.com/zblade/ 一.序言 在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下 ...
- Unity 协程使用指南
0x00 前言 在使用Unity的过程中,对协程仅仅知道怎样使用,但并不知道协程的内部机理,对于自己不清楚的部分就像一块大石压力心里.让自己感觉到担忧和不适. 这篇文章一探到底,彻底揭开协程的面纱,让 ...
- Unity协程使用经验
[Unity协程使用经验] 1.协程的好处是,异步操作发起的地方和结束的地方可以统一在一个方法,这样就不用引入额外的成员变量来进行状态同步. 2.在一个协程中,StartCoroutine()和 yi ...
随机推荐
- MQTT 协议学习: QoS等级 与 会话
背景 QoS 等级 与 通信的流程有关,直接影响了整个通信.而且篇幅比较长,所以我觉得应该单独拎出来讲一下. 概念 QoS 代表了 服务质量等级. 设置上,由2 位 的二进制控制,且值不允许为 3(0 ...
- day03-Python运维开发基础-(数据类型强转、运算符、逻辑短路、isinstance)
1. 强制转换成容器数据类型 # ### 强制类型转换 容器类型数据 (str list tuple set ) var1 = "你好世界" var2 = ["陈博文&q ...
- Linux-Power-management
1. 低级接口1.1 内核(swsusp)软件挂起1.1.1 睡眠状态的2个控制文件1.1.2 查看当前系统的睡眠控制文件内容1.1.3 状态表(表1)1.1.4 状态的使用1.2 uswsusp用户 ...
- java面试题汇总,不断更新中。。。
JVM,并发,锁相关: 1.请你谈谈对volatile的理解,volatile是否存在伪共享问题. 2.cas你知道吗? 3.原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗? 4 ...
- C# 控制台应用程序从外部传参运行和调试
参考:/*十有三博客*/ 新建一个用于演示的控制台应用程序项目,然后在Program.cs的入口Main方法里编写如下代码 foreach (var arg in args) { Console.Wr ...
- 七、Vue组件库:Element、Swiper(轮播专用组件)
一.vue的Element组件库 官网:https://element.eleme.cn/#/zh-CN 1.1安装 推荐安装方法: 首先要进入项目目录 cnpm i element-ui -S 或 ...
- Qt编写的项目作品2-控件属性设计器(组态)
一.功能特点 自动加载插件文件中的所有控件生成列表,默认自带的控件超过120个. 拖曳到画布自动生成对应的控件,所见即所得. 右侧中文属性栏,改变对应的属性立即应用到对应选中控件,直观简洁,非常适合小 ...
- python基础数据类型--元组(tuple)
python基础数据类型--元组(tuple) 一.元组的定义和特性 定义:与列表相似,只不过就是将[ ] 改成 ( ) 特性:1.可以存放多个值 2.不可变 3.按照从左到右的顺序定义元组元素,下标 ...
- php 和 文本编辑器火狐的配置
个人比较习惯的编辑器和浏览器配置 Sublime Ctrl+Shitf+P 输入 install 安装扩展: 点开菜单 -> view -> showConsole (或者按住 Ctrkl ...
- Linux系统发现新恶意软件
导读 安全研究人员发现了一种新的Linux恶意软件,它似乎是由中国黑客创建的,并被用作远程控制受感染系统的手段. 这个恶意软件命名为HiddenWasp,由用户模式rootkit,木马和初始部署脚本组 ...