Unity UI Profiling Tools – 번역
Unity UI Profiling Tools
– 원문 링크 –
이전글 – Fundamentals of Unity UI
Unity UI Profiling Tools
확인 완료한 버전: 5.3 – 난이도: 고급
유니티 UI 성능을 측정, 분석할 수 있는 유용한 프로파일링 도구가 있습니다. 주요 도구들은 다음과 같습니다:
- Unity Profiler
- Unity Frame Debugger
- XCode’s Instruments 또는 Intel VTune
- XCode’s Frame Debugger 또는 Intel GPA
유니티 외의 도구들은 밀리세컨드(milisecond) 단위의 Method-level CPU 프로파일링과 드로우 콜(Draw Call)의 세부정보, 쉐이더 프로파일링을 제공합니다. 위에서 나열한 도구에 대한 사용법 및 설정방법 등은 이 글의 범위를 벗어납니다. XCode Frame Debugger 와 Instruments는 Apple 플랫폼용 IL2CPP 빌드에서만 사용할 수 있기때문에, 현재는 iOS 빌드를 프로파일링하는 경우에만 사용할 수 있습니다.
유니티 프로파일러 (Unity Profiler)
유니티 프로파일러는 주로 비교 프로파일링을 수행할 때 사용합니다: UI 요소를 활성화/비활성화 하면서, UI 계층을 좁혀가면서 성능 문제의 원인이 되는 UI 요소를 찾을 수 있습니다.
이를 분석하기 위해서는 프로파일러 출력 항목에서 “Canvas.BuildBatch” 와 “Canvas.SendWillRenderCanvases” 라인을 살펴봐야 합니다.
Canvas.BuildBatch는 이전에 설명했던 대로, 캔버스 배치(Batch) 생성 프로세스를 수행하는 네이티브-코드 연산 로직입니다.
Canvas.SendWillRenderCanvases는 Canvas 컴포넌트의 willRenderCanvases 이벤트에 등록된 C# 스크립트를 호출시키는 기능을 포함합니다. 유니티 UI의 CanvasUpdateRegistry 클래스는 이 이벤트를 받아서 리빌드(Rebuild) 프로세스를 실행하기 위해서 사용합니다. Dirty 상태로 표시된 UI 컴포넌트는 모두 이 시점에 해당 Canvas Renderer를 업데이트 합니다.
참고: UI 성능의 차이점을 좀 더 쉽게 확인하려면, 일반적으로 “렌더링” 및 “스크립트”를 제외한 나머지 모든 추적 카테고리(trace category)를 비활성화시키는 것이 좋습니다. CPU Usage 프로파일러의 왼쪽에 있는 추적 카테고리 이름 옆에 색칠된 박스를 클릭하면 나머지 카테고리를 비활성화시킬 수 있습니다.
또한 CPU 프로파일러에서 카테고리를 재정렬할 수 있습니다. 카테고리의 이름을 클릭하고 위아래로 드래그하면 재정렬이 가능합니다.
유니티 프레임 디버거 (Unity Frame Debugger)
유니티 프레임 디버거(Unity Frame Debugger)는 유니티 UI에 의해서 생성된 드로우 콜(Draw Call)을 줄이는 데 유용한 도구입니다. 유니티에 내장된 이 도구는 유니티 에디터의 Window 메뉴를 통해서 사용 가능합니다. 프레임 디버거가 활성화되면, 유니티 UI를 포함해서, 유니티에 의해서 생성된 드로우 콜을 모두 확인할 수 있습니다.
특히, 프레임 디버거는 게임뷰를 화면에 보여주기 위해서 생성된 드로우 콜에 대한 정보를 자체적으로 업데이트하기 때문에, Play 모드로 전환하지않고 다른 UI 구성을 테스트해볼 수 있습니다.
유니티 UI 드로우 콜의 위치는, 화면에 그려지는 Canvas 컴포넌트에서 선택된 Render Mode에 따라서 달라집니다.
- Screen Space – Canvas.RenderOverlays 그룹 내에 오버레이(Overlay)형태로 나타납니다.
- Screen Space – 카메라는, 선택된 Render Camera의 Camera.Render 그룹 내에 Render.TransparentGeometry의 하위 그룹으로 나타납니다.
- World Space는, Canvas가 화면에 보이는 각 World Space 카메라의 Render.TransparentGeometry의 하위 그룹으로 나타납니다.
모든 UI는 “Shader.UI/Default” 라인 또는 드로우 콜의 세부정보에서 식별할 수 있습니다. 아래 스크린 샷에서 강조된 빨간색 상자를 참고하시기 바랍니다.
UI를 조절하면서 이 항목들을 관찰하면 UI 요소들을 배치(Batch)로 결합해서 캔버스의 기능을 극대화시키는 것이 비교적 간단합니다. 배치(Batch)에 문제가 발생하는 가장 일반적인 디자인-관련 원인은 의도하지 않은 overlap(UI가 겹치는 경우)입니다.
모든 유니티 UI 컴포넌트는 쿼드(Quad)를 기반으로 해당 지오메트리(UI 메쉬)를 생성합니다. 하지만, UI 스프라이트나 UI Text 문자의 다수는, 해당 문자나 스프라이트를 화면에 나타내기 위해서 쿼드의 일부분만 차지하기때문에, 나머지는 빈 공간으로 남습니다. 결과적으로 이런 경우, UI 디자이너가 의도하지 않게 여러 쿼드를 겹쳐놓았다는 것을 발견하는 경우가 많습니다. 또한 이렇게 겹쳐진 쿼드는 서로 다른 재질이 가진 텍스쳐를 사용하기 때문에 배치(Batch)될 수 없습니다.
유니티 UI는 Transparent 큐(Queue)에서 동작하기 때문에, 배치될 수 없는 쿼드가 위에 놓여있는 쿼드는 배치될 수 없는 쿼드보다 화면에 먼저 그려져야 합니다. 그렇기 때문에 다른 쿼드와 배치(Batch)될 수 없습니다.
A, B, C 3개의 쿼드가 있다고 가정해보겠습니다. 3개의 쿼드가 모두 서로 겹쳐져 있고, A와 C는 서로 같은 재질을 사용하는 반면 B는 다른 재질을 사용한다고 가정합니다. 이 경우, 쿼드 B는 쿼드 A, C와 배치(Batch)될 수 없습니다.
계층 뷰에서 계층 순서가 A, B, C인 경우, B는 A보다는 앞에 C보다는 뒤에 그려져야하기 때문에, A와 C는 서로 배치될 수 없습니다. 하지만, 쿼드 B가 배치될 수 있는 A, C 보다 앞이나 뒤에 위치하게 되면 이들은 배치 처리가 가능합니다. 즉, 쿼드 B가 A와 C 사이에 끼어있지않고, 앞이나 뒤에 위치하면 배치 처리가 가능합니다.
이 문제에 대한 자세한 내용은 캔버스 챕터의 Child Order 섹션을 참고하시기 바랍니다.
Instruments & VTune
XCode의 Instruments 와 Intel의 VTune은 각각 Apple과 Intel CPU에서 유니티 UI 리빌드 및 캔버스 배치(Batch) 연산에 대해서 자세한 프로파일링이 가능하도록 기능을 제공합니다. 메소드(method) 이름은 이전 유니티 프로파일러 섹션에서 설명했던 내용과 동일합니다:
- Canvas::SendWillRenderCanvases는 C++ 부모 클래스로서, Canvas.SendWillRenderCanvases C# 메소드를 호출하며, 유니티 프로파일러의 Canvas.SendWillRenderCanvases 라인에 대한 정보를 관리합니다. 이전 챕터에서 설명했던대로, 리빌드(Rebuild) 프로세스를 실행하기 위해서 사용되는 코드가 포함되어 있습니다.
- Canvas::UpdateBatches는 Canvas.BuildBatch와 동일하지만, 유니티 프로파일러에서 제공하지 않는 추가 코드가 포함되어 있습니다. 이 메소드는 위에서 설명한 실제 캔버스 배치 생성 프로세스를 실행합니다.
IL2CPP를 통해서 빌드된 유니티 앱과 함께 사용할 경우, 이런 도구들을 활용해서 Canvas::SendWillRenderCanvases의 C# 코드의 내부에 대해서 자세하게 조사할 수 있습니다. 아마도 다음과 같은 메소드의 비용이 주요 관심사가 될 것입니다. (참고: 내부 메소드의 이름은 대략적으로 나타납니다.)
- IndexedSet_Sort 와 CanvasUpdateRegistry_SortLayoutList는 레이아웃이 다시 계산되기 전에, dirty 상태로 표시된 Layout 컴포넌트 목록을 정렬하는데 사용됩니다. 위에서 설명했듯이, 각 레이아웃 컴포넌트 상위의 부모 트랜스폼의 수를 계산하는 로직이 포함됩니다.
- ClipperRegistry.Cull은 ClipperRegistry.Cull에 등록된 IClipRegion 인터페이스의 구현자(implementor)를 모두 호출합니다. 내장된 구현자(Implementor)에는 IClippable 인터페이스를 사용하는 RectMask2D를 포함합니다. ClipperRegistry.Cull이 호출되는 동안, RectMask2D 컴포넌트는 이 컴포넌트 계층 구조에 포함된 모든 클리핑(clipping) 항목에 컬링 정보가 업데이트 되도록 요청합니다.
- Graphic_Rebuild는 이미지, 텍스트 또는 그 외의 그래픽 기반 컴포넌트등을 나타내기 위해서 필요한 메쉬를 실제로 계산하는 비용(cost)이 포함됩니다. 이 메소드 내부에는 Graphic_UpdateGeometry와 특히, Text_OnPopulateMesh와 같은 몇몇 다른 메소드들이 포함되어 있습니다.
- Text_OnPopulateMesh는 Best Fit 옵션이 활성화 되어 있을때 실행됩니다. 이에 대한 자세한 내용은 뒷부분에서 살펴보겠습니다.
- Shadow_ModifyMesh 및 Outline_ModifyMesh와 같은, 메쉬 수정자(modifier)도 이곳에서 실행됩니다. 이 메소드들을 통해서 drop shader, 윤곽선(outline) 및 그 외 특수 효과를 계산하는데 드는 비용을 확인할 수 있습니다.
Xcode Frame Debugger & Intel GPA
로우-레벨 프레임 디버깅 툴은 배치 처리된(batched) UI의 개별 요소를 프로파일링 하는것 뿐만 아니라 UI 오버드로우(Overdraw)에 대한 비용을 살펴보는데 필수적입니다. UI 오버드로우는 뒷부분에서 자세히 살펴보겠습니다.
Xcode 프레임 디버거 사용하기 (Using the Xcode Frame Debugger)
Xcode에 내장된 GPU 진단 도구를 이용해서 UI가 GPU에 과부하를 주고있는지 확인할 수 있습니다. 먼저, 용도에 맞게 해당 프로젝트를 Metal 또는 OpenGLES3를 사용하도록 설정한다음, 빌드를 해서 결과로 생성된 Xcode 프로젝트를 엽니다. 유니티가 OpenGLES2에서 실행중인 경우 Xcode에서 유니티를 프로파일링할 수 없기 때문에, 이 도구는 구형 디바이스에서 사용할 수 없습니다.
참고: Xcode 일부 버젼에서는 그래픽 프로파일러를 작동시키기 위해서, Build Scheme에서 적합한 그래픽 API를 선택해야합니다. 이를 위해서, Xcode의 Product 메뉴로 이동한 다음, Scheme 메뉴를 확장시키고, Edit Scheme…을 선택합니다. Run target을 선택하고 Option 탭으로 이동합니다. 프로젝트에서 사용하는 API와 일치하도록 GPU 프레임 캡쳐 옵션을 설정합니다. 유니티 프로젝트에서 자동으로 그래픽 API를 선택하도록 설정된 경우에는, 대부분의 최신 iPad에서는 기본적으로 Metal이 사용됩니다. 이를 좀 더 정확하게 확인하고 싶은 경우에는, 프로젝트를 시작하고 Xcode의 디버그 창을 확인합니다. 초기에 출력되는 정보 중에서 렌더링 패스(Rendering Path, Metal, GLES3 또는 GLES2)가 초기화 된다는 정보를 확인할 수 있습니다.
참고: 위에서 설명한 설정은 Xcode 7.4버전 부터는 필요하지 않지만, Xcode 7.3.1 이전 버젼에서는 설정이 필요한 경우가 있습니다.
iOS 장치에서 프로젝트를 빌드하고 실행합니다. 프로젝트가 실행되면, Xcode의 Debug 패널에서 GPU 프로파일러를 확인할 수 있으며, “FPS” 항목을 선택합니다.
GPU 프로파일러에서 첫째로 확인할 사항은 화면 중앙의 3개의 막대 그래프로 표시되는 “Tiler”, “Renderer”, “Device” 항목들입니다.
- “Tiler”는 일반적으로 정점 쉐이더에서 소모되는 시간을 포함한 제오메트리를 처리하는데 GPU에서 얼마나 과부하가 걸리는지를 측정합니다.
- 일반적으로, “Tiler” 사용량이 높은 경우는, 정점 쉐이더가 너무 느리거나 화면에 그려지는 정점의 수가 너무 많은 경우를 나타냅니다.
- “Renderer”는 일반적으로 GPU의 픽셀 파이프라인에 걸리는 과부하를 측정합니다.
- 일반적으로, “Renderer” 사용량이 높은 경우는, 응용 프로그램이 GPU의 최대 fill-rate를 초과하거나 비 효율적인 프래그먼트 쉐이더(Fragment Shader)를 사용하는 경우를 나타냅니다.
- “Device”는 “Tiler”와 “Renderer” 성능을 모두 포함하는 전체 GPU 사용량을 종합적으로 측정한 결과를 나타냅니다. “Tiler”또는 “Renderer” 측정 값을 대략적으로 측정하기 떄문에 일반적으로는 이 항목을 무시할 수 있습니다.
Xcode의 GPU 프로파일러에 대한 자세한 내용은 이 문서를 참고하시기 바랍니다.
Xcode의 프레임 디버거는 GPU 프로파일러의 하단에 숨겨져있는 작은 ‘카메라’ 아이콘을 클릭해서 실행시킬 수 있습니다. 다음 스크린샷에서 빨간색 화살표로 표시되어 있습니다.
잠깐 멈춘 후에, 다음과 같이 프레임 디버거의 요약정보가 나타납니다.
기본 UI 쉐이더를 사용하는 경우, 유니티 UI 시스템에서 생성하는 렌더링 지오메트리(UI 메쉬)의 비용은 “UI/Default” 쉐이더 패스 아래에 표시되며, 이 정보는 기본 UI 쉐이더가 커스텀 쉐이더로 교체되지 않은 것으로 가정한 결과입니다. 기본 UI 쉐이더 항목은 위의 스크린샷에서 Render Pipeline “UI/Default”로 표시되는 라인에서 확인할 수 있습니다.
유니티 UI는 쿼드(Quad)만 생성하기 때문에 정점 쉐이더는 대부분 GPU의 tiler 파이프라인에 과부하를 주지 않습니다. 이 쉐이더 패스에서 문제가 발생하는 경우는 주로 fill-rate가 문제의 원인인 경우가 많습니다.
프로파일러 결과 분석하기 (Analyzing profiler results)
프로파일링 데이터를 수집한 다음, 몇가지 결론을 도출해낼수 있습니다.
Canvas.BuildBatch 또는 Canvas::UpdateBatches에서 CPU 시간을 과도하게 사용하는 경우에는, 너무 많은 캔버스 컴포넌트를 단일 캔버스에서 사용하는 경우일 확률이 높습니다. 이에 대한 내용은 캔버스 챕터의 캔버스 분할하기(Splitting Canvasses) 섹션을 참고하시기 바랍니다.
GPU에서 UI를 그리는데 너무 많은 시간이 소모되고, 프레임 디버거에서 프래그먼트 쉐이더(Fragment Shader) 파이프라인이 병목지점이라고 나태내는 경우에는, GPU에서 허용할 수 있는 픽셀 fill-rate를 초과했을 확률이 높습니다. 이 경우, 가장 주된 원인은 과도한 UI 오버드로우(Overdraw)입니다. 이에 대한 내용은 Fill-rate, 캔버스 그리고 입력 챕터의 fill-rate 문제 수정하기 섹션을 참고하시기 바랍니다.
Graphic Rebuild에서 과도하게 CPU를 사용하는데, Canvas.SendWillRenderCanvases 와 Canvas::SendWillRenderCanvases가 CPU 사용시간에서 큰 부분을 차지하는 경우에는 좀 더 자세한 분석이 필요합니다. Graphic Rebuild 프로세스의 일부가 이 문제의 원인일 가능성이 높습니다.
WillRenderCanvas의 많은 부분이 ndexedSet_Sort 또는 CanvasUpdateRegistry_SortLayoutList의 내부에서 소모되는 경우, dirty 상태로 표시된 Layout 컴포넌트를 정렬하는데 시간이 소모되고 있다는 것을 의미합니다. 이 경우, 해당 캔버스에서 사용되고있는 Layout 컴포넌트의 수를 줄이는 것을 고려해보는것이 좋습니다. 이에 대한 정보는 Replacing layouts with RectTransforms 및 캔버스 분할하기 (Splitting Canvases) 섹션을 참고하시기 바랍니다.
Text_OnPopulateMesh에서 시간이 과도하게 소모되는 경우는 텍스트 메쉬를 생성하는 과정이 문제입니다. 이 문제를 해결하기 위한 내용은 Best Fit 과 캔버스 렌더러 비활성화하기(Disabling Canvas Renderers) 섹션을 참고하시기 바랍니다. 또한 다시 생성되는 텍스트의 다수가 해당 문자열 데이터를 변경하지 않는 경우, 캔버스 분할하기(Splitting Canvases) 섹션을 참고하시기 바랍니다.
Shadow_ModifyMesh 또는 Outline_ModifyMesh (또는 ModifyMesh를 구현한 다른 메소드) 내부에서 시간이 오래 소모되는 경우, 메쉬 수정자(Mesh modifier)를 계산하는데 시간이 많이 소모되고 있다는 것을 나타냅니다. 이 경우, 해당 컴포넌트를 줄이고 시각적인 효과를 이미지로 대체하는 방안을 고려해보는것이 좋습니다.
Canvas.SendWillRenderCanvases 내에 시간을 크게 소모하는 부분이 없거나, 매 프레임마다 실행되는 경우에는, 실행 중에 수정이 잦은 동적 UI 요소들과 정적 UI 요소들이 같은 그룹으로 묶여서 전체 캔버스를 너무 자주 리빌드하는 것이 문제가 되는 경우가 많습니다. 이에 대한 내용은 캔버스 분할하기(Splitting Canvases) 섹션을 참고하시기 바랍니다.
각주 (Endnotes)
UI 쉐이더가 커스텀 쉐이더로 대체되지 않았다고 가정합니다.
내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂