루아 프로파일러를 활용하여 최적화하기

# 학습 과정 소개

제작한 게임을 플레이하다 보면 다양한 상황에서 느려지는 것을 경험할 수 있습니다. 이처럼 게임의 처리 속도에서 부하가 걸리는 병목 지점(Bottleneck)을 찾아 수정하는 것을 최적화라고 합니다. 일부 병목 지점만 해결해도 게임의 전체적인 성능 향상을 기대할 수 있기 때문에 최적화는 상당히 중요한 작업입니다. 루아 프로파일러는 이러한 스크립트 최적화 작업을 더욱 손쉽게 진행할 수 있도록 도와줍니다. 이번 시간에는 루아 프로파일러가 무엇인지 알아보고 활용법을 살펴보도록 합시다.

더 알아보기

> 루아 프로파일러를 사용하기 위해서는 최신 업데이트된 윈도우7 이상의 운영체제를 필요로 합니다. 또한 `.NET Core 3.1 Desktop Runtime` 이상의 버전을 설치해야 합니다. 루아 프로파일러를 실행할 수 없다면  설치 후 다시 시도해보세요.

##### 참고 가이드

[루아 프로파일링 요약 보고서](docs/?postId=1046)

# 루아 프로파일러 소개

## 루아 프로파일러 실행

### 제작 중인 월드에서 실행

게임 만들기 화면에서 Window - Lua Profiler를 누르면 루아 프로파일러가 실행됩니다. [png](16588152380315f27ceb0abf148f5952f65813e870f7f)   
 시작을 누르고 플레이 상태에서 루아 프로파일러의 [Recording] 버튼을 누르면 기록이 시작됩니다. 이미 플레이 테스트 중인 상태로 루아 프로파일러를 실행하면 실행 즉시 기록이 시작됩니다. [gif](16599472883762d82c3ab3435482ab40812db3ef2c76f)

### 배포된 게임에서 실행

배포된 게임을 플레이할 때도 루아 프로파일러를 실행할 수 있습니다.

  1. 먼저 게임 우측 상단의 설정 버튼을 누릅니다. [png](1659057882093e645caa2c4204a45828e93b96a599f60)   

  2. 기타 탭을 선택한 후 Lua Profiler 사용을 켜고 Lua Profiler 실행 버튼을 누르면 루아 프로파일러가 실행되면서 바로 기록이 시작됩니다. [png](165905803015405f2966f34094161937c4055c7268b0a)



## 루아 프로파일러 살펴보기

루아 프로파일러 UI를 자세히 살펴봅시다. [png](165968854387833959c5ede1d42b59fbac5dc06f301f3)

### ① File

[png](168413955956620e175ed60fa4ea180d5c1dc01a99bbb)

  * Load / Save : 프로파일링으로 수집한 데이터를 XML로 저장하거나 불러올 수 있습니다.
  * Export Report : [루아 프로파일링 요약 보고서](/docs/?postId=1046)를 XML로 Export할 수 있습니다.
  * Options [png](1658887084903492a0fe98e0f4f3381e0f12434ac2320)

[{"목록":"Record Max Frames","하위 목록":"-","설명":"기록이 많이 쌓이면 루아 프로파일러가 느려집니다. 그래서 지정한 프레임 수 이상으로 기록되면 이전 데이터를 버리도록 처리하고 있습니다. 기본값은 1,000프레임인데 1초에 60프레임이라 가정하면 약 16초 정도 기록되는 것입니다. 더 길게 기록하고 싶다면 프레임 수를 변경해봅시다."},{"목록":"Record Timeline Max Seconds","하위 목록":"-","설명":"타임라인 차트에서 데이터를 저장할 최대 시간입니다."},{"목록":"Active Module","하위 목록":"Execution Time","설명":"함수 혹은 Scope의 실행 시간입니다. 실행 시간은 지표에 가장 중요한 부분이므로 반드시 측정해야 합니다."},{"목록":"Active Module","하위 목록":"Native scope","설명":"스크립트가 사용되지 않은 Native 영역의 시간을 측정합니다."},{"목록":"Active Module","하위 목록":"Memory","설명":"함수 혹은 Scope가 실행되는 동안 발생한 Lua Memory Allocation Size입니다."},{"목록":"Active Module","하위 목록":"Network","설명":"함수 혹은 Scope가 실행되는 동안 발생한 Network Write입니다. 실행 제어 호출이 대부분이며 다른 스레드에서 발생한 Network Write가 기록되는 경우도 있습니다."}]




### ② Connect / Disconnect

  * 루아 프로파일러는 플레이 중인 게임에 접속하여 데이터를 수집하는 형태로 동작합니다.
  * Disconnect 버튼을 누르면 루아 프로파일러의 게임 접속이 종료됩니다.



### ③ Context

  * Context는 Core에서의 실행 흐름을 뜻합니다.
  * Server, Client, Instance, Editor 등의 Context 중에서 선택할 수 있습니다.
  * Context를 선택하면 ⑤ 차트 영역과 ⑨ 프로파일링 결과 영역에서 데이터를 확인할 수 있습니다.



### ④ 그래프 선택

  * Elapsed Time, Network Send, Network Receive, Lua Memory 중에서 어떤 그래프를 ⑤의 차트 영역에 표시할 것인지 선택합니다.
  * File - Option에서 Network를 체크하고 기록하면 Network Send, Network Receive 정보를 볼 수 있습니다.
  * File - Option에서 Memory를 체크하고 기록하면 Lua Memory 정보를 볼 수 있습니다.



### ⑤ 차트

  * FrameChart와 TimelineChart 중 선택할 수 있으며 일반적으로 FrameChart를 많이 활용합니다.
  * 차트 영역을 클릭해 특정 프레임을 선택할 수 있습니다.
  * 차트 영역에는 ④에서 체크한 그래프를 볼 수 있습니다. 차트 위에서 우클릭하면 다음 메뉴를 확인할 수 있습니다. [png](1658893776888960b3923990b449bb99c0ee2defb2c0f)
    * Save Image : 현재 차트를 png 이미지로 저장합니다.
    * Copy Image : 현재 차트 이미지를 클립보드에 복사합니다.
    * Zoom to Fit Data : 전체 차트를 화면 사이즈에 맞게 보여줍니다. 차트를 확대해서 보다가 다시 전체 차트를 보려고 할 때 유용합니다.
    * Help : 차트 조작법 안내입니다.
    * Open in New Window : 현재 차트를 별도 창으로 엽니다.



### ⑥ Recording

  * 버튼을 누르면 프로파일링을 시작하고 한 번 더 누르면 프로파일링을 종료합니다.



### ⑦ Clear

  * 지금까지 기록한 프로파일링 데이터를 삭제합니다.



### ⑧ Frame Data

  * All을 선택해 전체 프레임을 합산해서 보거나 개별 프레임을 선택해 데이터를 볼 수 있습니다. [png](16588974397120d5a96e886b647eca63856d08516834a)



### ⑨ 프로파일링 결과

  * 프로파일링 결과를 확인할 수 있습니다.



[{"항목":"Name","설명":"함수 혹은 Scope의 이름입니다. 프로퍼티에서 Get, Set도 함수로 취급합니다."},{"항목":"Category","설명":"크게 Lua와 Native로 나뉩니다. 하위 소분류로 Function, Getter, Setter, Scope가 있습니다."},{"항목":"Call Count","설명":"호출 횟수입니다."},{"항목":"Total Time (ms)","설명":"총 실행 시간입니다."},{"항목":"Total Self Time (ms)","설명":"하위 샘플을 제외하고 해당 함수 자체에서 소요된 시간입니다."},{"항목":"Average Time (ms)","설명":"평균 실행 시간입니다. Total Time\/Call Count 값입니다."},{"항목":"Peak Time (ms)","설명":"1회 호출에서 가장 긴 실행 시간입니다."},{"항목":"Peak Self Time (ms)","설명":"1회 호출에서 가장 긴 함수 자체 실행 시간입니다."},{"항목":"Network Usage","설명":"함수 실행 시간 동안 발생한 Network Send Size입니다. 일반적으로 실행 제어 함수가 대부분을 차지합니다."},{"항목":"Lua Memory","설명":"함수를 실행하는 동안 발생한 Lua memory allocation 크기입니다."},{"항목":"Lua Memory (Self)","설명":"함수 자체에서 발생한 Lua memory allocation 크기입니다."}]

### ⑩ Log

  * 루아 프로파일러 로그를 표시합니다.
  * Clear 버튼을 누르면 이전 로그를 삭제합니다.



# 데이터 분석

샘플 데이터를 분석해보면서 루아 프로파일링을 어떻게 진행하면 좋을지 참고해봅시다. [png](165994423724064220755849b4970b212baf200482c9b)  Context로 Server를 선택하고  Frame Data는 All을 선택합니다. 이처럼 프로파일링 데이터가 수집되었을 때 아래의 항목을 먼저 체크해봅시다.

  * Frame의 Total Self Time ()
  * 시간 점유율이 높은 함수 ()
  * Peak Time이 높은 함수 ()



위의 , , 에 대해 자세히 살펴봅시다.

## Frame의 Total Self Time

샘플 데이터를 보면 Frame의 Total Self Time이 전체의 62% 이상을 차지하는 것을 알 수 있습니다. Frame에서의 Self Time은 스크립트 실행을 제외하고 Native에서 소요된 시간이므로 Native Scope의 세부 분석이 필요한 상황입니다. 루아 프로파일러의 File - Option에서 Native Scope에 체크한 후 다시 프로파일링해봅시다. [png](16590581743509659d9f6757e4497ac32594f3d715eb9)   
다시 수집한 데이터의 카테고리를 살펴보면 Native Scope 세부 분석이 이루어졌음을 알 수 있습니다. [png](1659944748212c0a5c7969ae24263842732d10d984ba7) Frame의 Total Self Time은 확연히 줄어들었고, 대신 Native Function인 TriggerComponent의 OnUpdate가 18% 이상의 상당한 점유율을 차지하고 있습니다. 호출 횟수도 많고 Peak Time도 높으므로 최적화를 고민해봐야 함을 알 수 있습니다.

## 시간 점유율이 높은 함수

가장 높은 시간 점유율을 가지는 함수도 우선 분석 대상입니다. [png](1659945181899df0e60b1ce264dc3ab43d41b59e3cfe3) 이 샘플에서 MonsterController - OnUpdate 함수의 ServerElapsedSeconds를 살펴보면 평균 실행 시간은 매우 짧지만 호출 횟수가 많아 총 실행 시간이 길어짐을 확인할 수 있습니다. 이처럼 단순한 프로퍼티 접근이라도 호출 횟수가 많으면 그만큼 비용이 증가함을 알 수 있습니다. 제작한 게임을 프로파일링했을 때 이런 부분이 발견된다면 스크립트 최적화를 고려해봅시다.

## Peak Time이 높은 함수

호출 횟수가 적더라도 실행 시간 자체가 높은 함수가 있다면 검토해봐야 합니다. 차트 영역에서 Shift + 클릭하면 주변 10% 범위에서 총 실행 시간이 가장 높은 프레임이 선택되는데 이를 통해 Peak Time의 원인이 되는 함수를 파악하고 수정 여부를 판단할 수 있습니다. 단, 한 번의 Peak가 관찰되었다고 해서 무조건 해당 함수에 문제가 있다고 볼 수는 없습니다. 프로파일링 노이즈가 발생했을 가능성도 있으므로 여러 차례 프로파일링하여 같은 부분에서 Peak가 발생한다면 최적화를 고려해봅시다.

# 스크립트 함수 표본 추출

프로파일링 결과 수정이 필요해 보이는 스크립트 함수를 발견했으나 해당 함수의 내용이 너무 길다면 세부적인 분석이 필요할 수 있습니다. 이런 경우에는 표본 추출을 활용해봅시다.
    
    
    @Component
    script EntityAccessProfiler extends Component
        method void Function1(string arg1)
        profiler.beginscope("FirstLoop")
        for i = 1, 10000 do
        	local en = self.Entity
        end
        profiler.endscope()
    
        profiler.beginscope("SecondLoop")
        for i = 1, 10000 do
        	local en = self.Entity
        end
        profiler.endscope()
        end
    end
    

  
분석하려는 함수에 위와 같이 `profiler.beginscope("name")` ~ `profiler.endscope()`를 설정하면 아래와 같이 프로파일링 데이터에서 표본을 뽑을 수 있습니다. [png](1659945606718a0afa01deb2f43c7ba0905c583f6d2d5)