2016年10月17日 星期一

詳解Unity WebGL記憶體

作者:MARCO TRIVELLATO 原文
翻譯/潤稿:Kelvin Lo

自Unity支援WebGL以來,我們開發團隊就一直致力於優化WebGL的記憶體消耗。在Unity文件上已有對於WebGL記憶體管理的詳盡分析,在Unite Europe 2015與Unite Boston 2015兩屆大會上,也有專題針對WebGL進行深入的講解。然而開發者仍然對這方面的內容依舊討論熱烈,所以我們意識到應該分享更多關於這方面的內容。

Unity WebGL和其它發佈平台有何不同?


如果開發者從一些本來就要控制記憶體的平台像是PC或是WebPlayer轉過來的話或許已經有個基礎,應該都不會造成問題。

如果目標是遊戲機(Console)的話,記憶體管理相對比其他平台容易,因為可以準確知道記憶體是如何運用的。你可以很好的管理記憶體確保遊戲完美運作。手機平台記憶體管理就有些複雜,因為設備種類繁多,但至少可以選擇最低標準的設備,並根據市場情況略過那些標準更低的設備。

網頁就沒那麼輕鬆了,理想狀況下所有玩家(Client)都有64位元瀏覽器和大量記憶體,但事實總是殘酷的。首先你無法知道它們電腦的硬體規格。然後除了他們的作業系統和瀏覽器之外,並無法取得其它資訊。最後玩家可能像執行其它網頁一樣的方法開啟你的WebGL內容。因此這是一個非常複雜的問題。

總覽


這張圖描述的是當Unity WebGL在瀏覽器執行的記憶體配置

這張圖顯示在Unity Heap區域上,Unity WebGL的內容會需要分配額外的記憶體,這裡需要理解清楚才能進一步優化來降低玩家流失率。


從圖裡能看到有幾個記憶體分配,DOM, Unity Heap, Asset Data以及程式碼,程式碼一旦載入網頁就會永遠存在記憶體裡,其他像是Asset Bundles, WebAudio, Memory File System將會依據內容不同來分配(Asset Bundle下載大小不同,音樂的播放不同等等)

在載入的時候,asm.js解析和編譯期間也有幾個瀏覽器記憶體臨時分配,這裡有時候會導致32位元瀏覽器出現記憶體不足。


Unity Heap



一般來說,Unity Heap是包含所有Unity特定的物件(Game Objects),元件(Components),材質(Textures)和著色器(Shaders)等等。
在WebGL,Unity Heap的大小需要先算好讓瀏覽器分配記憶體空間,一旦分配好之後Buffer就不能調整大小了。

負責分配Unity Heap的程式如下:

buffer = new ArrayBuffer(TOTAL_MEMORY);

這段程式可以在產生的build.js裡找到,並會交由瀏覽器的JS VM執行。
TOTAL_MEMORY可以從Player Settings裡的WebGL Memory Size來指定,預設情況是256MB,實際上一個空專案只有16MB。

然而,真的遊戲專案可能需要更多,在大多數情況下會需要用到256MB或386MB,記住,設定越多記憶體表示越多玩家能正常執行。
原始碼/編譯碼 的記憶體

程式可以執行之前需要:


1.被下載到Client

2.複製到一個Text blob區塊
3.編譯

顧慮到上述每一個步驟都需要一塊記憶體


  • 下載暫存區是暫時的,但原始碼和編譯碼在記憶體裡會留著直到關閉頁面。
  • 下載暫存區和原始碼區分配的記憶體大小都是由Unity所產生的無壓縮js空間。估計會需要多少空間:
    1.製作一個可發佈版本
    2.將jsgz及datagz改名為*.gz,然後用解壓縮的工具解開它們。
    3.解開後的大小會是需要的瀏覽器記憶體大小
  • 編譯碼的大小取決於不同瀏覽器
一個簡單的優化方法是啟用引擎剝離功能(Strip Engine Code),那樣的話發佈的包就不會包含不需要的原生的引擎碼,(例如:不需要2d物理模組將會被剝除)。

請記住,例外支持(Exceptions support)和第三方的套件會影響你的程式大小,話雖如此,我們知道開發者想要在發佈時做空值檢查(null checks)和陣列檢查(Array bounds checks)時記憶體不要銷超出了例外支持的範圍,為此,你可以送-emit-null-checks-enable-array-bounds-check給il2cpp,像這樣:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");

最後請記住,發佈development build會產生更多程式碼的包因為它不是minified,這應該對你來說不是問題,因為最後你會發佈給玩家的是最終版本(release build),對吧? ;-)。


資源(Asset Data)


在其他平台上,應用程式可以永久的存取硬碟上的內容,但在網頁是沒有實際的檔案系統所以是不可能的,因此,一旦Unity WebGL的資料(.data檔案)被下載完成後,它就會存在記憶體裡。缺點是和其他平台相比它需要額外的記憶體(從5.3開始.data的檔案會壓縮成lz4放在記憶體)。例如這個Profiler說明,256mb的Unity heap會產生約40mb的.data檔案。


甚麼是.data檔案?他是Unity產生的文件組合:data.unity3d(全部的場景,有依賴關係的資源和Resources目錄底下的所有東西),unity_default_resources和引擎所需要的一些小檔案。

要了解資源的確切大小,可以在WebGL打包完後看一下 Temp\StagingArea\Data裡的data.unity3d(記住,當你關閉Unity時,temp資料夾會被刪除)。你也可查看傳給UnityLoader.js裡DataRequest的偏移植。

new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');

(這段程式碼可能會依照不同的Unity版本而不同 - 這段從Unity 5.4節錄)

記憶體檔案系統(Memory File System)

上面提到Unity WebGL雖然沒有真正的檔案系統,但還是可以存取資料,和其他平台相比主要的差異是在所有的I/O行為都在記憶體裡面完成。重要的是這個檔案系統並不在Unity heap裡面,因此會需要額外的記憶體開銷,比如當我們寫一個陣列到檔案時:


var buffer = new byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);

這個檔案會寫到記憶體裡,你也可以在瀏覽器的profiler找到:



這段Unity heap的記憶體是256mb


同理,由於Unity的快取系統(caching system)依賴著檔案系統,整個快取也是放在這樣的檔案系統,這代表像PlayerPrefs和快取的Asset Bundle會同樣永遠放在記憶體裡直到關閉,而且是在Unity heap區的規畫之外。

Asset Bundles


降低WebGL記憶體消耗最好的方法之一就是採用Asset Bundle(如果你對這個不熟可以參考文件)。然而,不同的Asset Bundle使用方式可能會對記憶體(Unity heap內或外)產生重大的影響,有可能會造成無法再32位元瀏覽器無法正常執行。

現在你需要用到Asset Bundle,然後你該怎麼做?將所有的資源打包成一包Asset Bundle?

千萬不要!就算這樣能降低網頁的載入時間,你還是需要下載(可能超大)這個Asset Bundle,導致記憶體飆高。來看看下載AB之前的記憶體用量:


如你看到的,256mb被Unity heap定義了。這是下載了AB之後還沒有放入暫存。




你現在看到的是一個額外的緩衝區,大約是同等於硬碟上的65mb,由XHR分配。這只是一個臨時的緩衝區但是他可能造成記憶體幾禎的尖峰,直到GC(garbage collect)回收它。


那麼如何做可以減低這些記憶體尖峰?需要幫每個資源建立一個Asset Bundle?很有趣的想法但不太實用。

打包Asset Bundle是沒有一個規則,需要根據你專案的需求來讓包裝下載有意義。(例如:單機遊戲可選男女主角,且遊戲週期只會用到一種,那選完角色之後可以只載入該性別的包)

最後,記住完成之後要用AssetBundle.Unload來卸載。

Asset Bundle Caching



WebGL的Asset Bundle外取和在其他平台一樣用WWW.LoadFromCacheOrDownload,雖然有一個明顯的不同是這是放在記憶體的。在Unity WebGL裡AB的快取依賴IndexedDB,被放在記憶體檔案系統裡的emscripten編譯器支援。

來看看用LoadFromCacheOrDownload下載Asset Bundle之前的記憶體抓圖:


如你所見,512mb被Unity heap用掉了,4mb左右其他分配。包被載入之後的圖:



額外的記憶體需求上升至167mb左右,這是這個Asset Bundle額外需要的記憶體(原本是64mb壓縮包)。然後下圖是js vm做完GC之後的圖:



結果好多了,但仍需要85mb左右,大多數的記憶體用來存放Asset Bundle,這些記憶體就算你用unload去釋放到結束你都無法取回記憶體。還有,當玩家第二次用瀏覽器打開你的內容時,不管之前是否有分配過區塊,這些記憶體又會再次被分配。

這是來自Chrome的記憶體快照參考:



在Unity heap之外還有一個Asset bundle系統所需要
快取相關的臨時分配。壞消息是最近我們發現它比預期的大很多,我們預期會在Unity 5.5 beta 4, 5.3.6p6和5.4.1p2修復這個問題。


如果你用更舊的Unity版本不想升級或是你的WebGL內容已經接近發佈了,可以透過編輯器腳本來設定一些屬性:

PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);

長遠來看要最小化Asset Bundle快取記憶體用量最好的解決方案是用WWW構造函數而不是用LoadFromCacheOrDownload()或是沒有hash/version參數的UnityWebRequest.GetAssetBundle()
如果你使用新的UnityWebRequest API的話。


然後在XMLHttpRequest等級使用備用的快取機制,將下載的檔案直接存到indexedDB裡避開記憶體檔案系統。這是我們最近開發放在Asset Store的方案,需要的話可隨意取用修改。

Asset Bundle 壓縮


在Unity 5.3和5.4,都是支援LZMA和LZ4壓縮的,雖然使用LZMA(預設值)結果會比未壓縮的LZ4來的小,但用在WebGL上有幾個缺點:有明顯的效能問題、需要更多記憶體分配。所以我們比較推薦用LZ4不要壓縮(實際上,LZMA壓縮法在Unity5.5會被移除),為了彌補這個缺口,你可能會希望能用gzip/brotli來壓縮你的資源。

可以查看更多關於打包壓縮的相關資料

WebAudio


Unity WebGL的音效是有別於其他平台的,這和記憶體會有什麼關聯?

Unity將在JavaScript上支援建立特定的AudioBuffer物件,用來更方便播放網頁音效。
由於WebAudio緩衝區存在Unity heap外面,因此無法用Unity profiler追蹤,你需要用特定的瀏覽器工具來檢查記憶體查看有多少用在音效上。這個例子用Firefox來查詢音效記憶體用量:


考慮到這些音效緩衝存放未壓縮的資料,不太適合放超大型的音效檔案(例如:很長的背景音樂)。所以現在你可能需要寫些自己的js套件來使用<audio>標籤,這樣音效檔案壓縮用較少的記憶體。

FAQ


減少記憶體使用的最佳方法是什麼?簡單概括如下:

1.減少Unity Heap的大小

  • 盡可能保持“WebGL Memory Size”夠小
2.減少包裡程式碼量
  • 啟用Strip Engine Code
  • 關閉異常檢測(Disable Exceptions)
  • 避免使用第三方外掛程式
3.減少資料大小
  • 使用Asset Bundle
  • 壓縮材質

是否有能夠決定最小WebGL Memory Size的方法?

有,最佳方案是使用記憶體分析器(memory profiler),分析內容實際所需的記憶體大小,然後依據結果改變WebGL Memory Size。

以空專案為例,記憶體分析器告訴我們總使用量僅為16MB(這個值可能在不同Unity版本上有所不同),這代表只須要設定WebGL Memory Size大於16MB即可。當然,記憶體的總使用量將會依據內容而有所不同。

然而,如果因為某些原因無法使用分析器,可以簡單地通過不斷減少WebGL Memory Size 值,直到發現你的內容真正所需要的最小記憶體使用量為止。

另外值得注意的是任何不是16的倍數的值都將被自動四捨五入(執行期間)為下一個16的倍數,這是Emscripten編譯器所要求的。

WebGL Memory Size(mb)設定將決定產生html中TOTAL_MEMORY(bytes)的值。


在不重新打包專案的前提下要測試記憶體堆疊的值,建議使用編輯html的方式。一旦找到適合的值,只需在Unity專案設定中更改WebGL Memory Size即可。

最後,記住Unity分析器將佔用一些來自Unity heap的記憶體,所以在使用分析器時可能需要加一些WebGL記憶體大小。



執行時發生記憶體溢位,如何修復?
這要看是Unity還是瀏覽器的記憶體溢位。錯誤資訊會指出問題所在和解決辦法,“如果你是開發者,可以透過在WebGL設定中為專案分配更多(或更少)的記憶體來解決。”可以依據此來調整WebGL記憶體大小。然而還有很多可以解決記憶體溢位的方法。如果出現以下錯誤資訊:


除了訊息所提之外,你還可以嘗試減少程式和資料的大小。因為當瀏覽器載入網頁時,它會嘗試為一些內容尋找空餘的記憶體,其中最重要的是:代碼,資料,Unity heap和被編譯的asm.js。它們可能相當大,尤其是資料和Unity heap記憶體,這對32位元瀏覽器來說可能是問題。


在一些例子中,儘管有足夠的記憶體,瀏覽器仍載入失敗,因為記憶體是碎片化的。這就是為什麼有時候你的內容可能在重新啟動瀏覽器之後,可以成功載入的原因。

另一種情況是,當Unity 記憶體溢位時提示以下訊息:


這種情況下就需要優化你的Unity專案。


如何衡量記憶體消耗?

為了分析內容所使用的瀏覽器記憶體,可以使用Firefox瀏覽器的記憶體工具或Chrome的Heap snapshot。但它們無法顯示WebAudio記憶體使用情況,因此還可以透過about:memory裡的方法在Firefox裡拍張快照然後搜尋“webaudio”找到。如果需要透過JavaScript分析記憶體,請嘗試使用window.performance.memory(只支援Chrome)。

使用Unity Profiler測量Unity heap記憶體使用。需注意您可能需要增加WebGL的記憶體大小,以便能夠使用Profiler。

此外,我們一直致力於開發新的工具,以便分析發佈版本:使用時先包成WebGL版本,訪問http://files.unity3d.com/build-report/就能使用該工具。雖然這個工具在Unity5.4中可用,但請注意這還是開發中的功能,可能隨時會更改或刪除。但至少現在可以使用它達到測試的目的。

WebGL Memory Size的最小值與最大值是多少?

16MB是最小的,最大是2032MB,然而我們通常建議保持在512MB以下。

是否可能因為開發目的需要分配超過2032MB的記憶體?

這是一個技術上的限制:2048MB(或更多)將會超出TypeArray所用的32位元整數型態的最大值,而TypeArray被用於在JavaScript中實現Unity heap。

為何Unity Heap大小不可改變?

我們一直在考慮使用Emscripten編譯器標誌ALLOW_MEMORY_GROWTH,來允許調整Heap的大小,但目前是沒這麼設定,因為它會禁用一些Chrome中的優化。我們還未對這個影響做一些真正的基準測試。預計使用這個flag會導致記憶體問題更嚴重。如果您遇到Unity heap太小,以至於無法滿足所需記憶體的情況,這時就需要更多記憶體,那麼瀏覽器就必須分配一個更大的Heap,從舊的裡面中複製一切,然後再釋放舊的Heap。這麼做需要同時維持新Heap和舊Heap兩份記憶體(直到完成複製),這樣需要更多的總記憶體。因此反而會比使用預定固定記憶體的方式佔用更大。

為什麼32位元瀏覽器在64位元作業系統上會記憶體溢位?

32位元瀏覽器執行時的記憶體限制是一樣的,無論作業系統是64或32位元。



結論


最後建議使用瀏覽器專用的分析工具,來分析你的Unity WebGL內容,因為Unity profiler無法追蹤超出Unity heap之外的記憶體分配。

希望這些資訊對你有用。如有任何疑問請到論壇討論。

3 則留言:

著作人