Unity 協程運行時的監控和最佳化

來源:互聯網
上載者:User

標籤:init   使用者   get   wrap   選中   assets   註冊   針對   邏輯   

我是快樂的搬運工: 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 的行為

問題描述

由於以下幾點問題的存在,協程的執行情況對開發人員而言並不透明,很容易在開發過程中引入效能問題。

  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 類中,可以看到以下兩個介面,針對以 IEnumeratorstring,及可選的單參形式等三種形式的協程啟動的封裝。

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 協程運行時的監控和最佳化

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.