WWDC/iOS

[iOS] WWDC21 테크톡 - Explore UI animation hitches and the render loop

유정주 2023. 9. 10. 11:44
반응형

서론

WWDC21 테크톡 - Explore UI animation hitches and the render loop에서는 hitch와 render loop가 무엇인지 알아보고, hitch의 종류, hitch를 측정하는 방법에 대해 알아봅니다.

 

이번 포스팅에서는 측정하는 방법까지만 다루고, 다음 포스팅인 Find and fix hitches in the commit phase에서 hitch를 제거하는 방법을 알아보겠습니다.

 

 

What is a hitch

앱에서 애니메이션은 사용자와 앱 사이의 시각적 연결을 도와줍니다.

예를 들어, 사용자가 손가락을 화면을 쓸어넘기면 화면이 변해야 합니다.

 

hitch는 예상보다 늦게 화면에 나타나는 프레임으로, 다음 프레임의 생성이 늦어져 애니메이션이 끊기는 시간입니다.

hitch는 애니메이션 끊김을 유발해서 이러한 사용자 경험을 떨어뜨립니다.

 

이걸 프레임별로 살펴보겠습니다.

프레임 1, 2, 3까지는 순차적으로 이동합니다.

 

하지만 네 번째 칸에서는 프레임3을 한 번 더 보여줍니다.

프레임4가 늦게 완료되서 사용자가 hitch를 본 것입니다.

 

 

Thre Render Loop

hitch가 발생하는 이유는 render loop가 프레임을 제 시간에 완성하지 못했기 때문입니다.

 

Render loop는 터치 이벤트가 앱에 전달되고, 그에 따른 UI 변화가 OS로 전달되는 과정을 말합니다.

render loop가 한 번 실행될 때 하나의 프레임이 완성됩니다.

 

render loop는 refresh rate마다 발생합니다.

아이폰 및 아이패드에서는 초당 60 프레임, 매 16.67 ms마다 새로운 프레임을 표시하고,

120Hz 기종에서는 초당 120 프레임, 매 8.33 ms마다 새로운 프레임을 표시합니다.

 

화면에서 프레임이 교체되는 시점에 하드웨어는 VSYNC 이벤트를 발생시킵니다.

VSYNC는 새로운 프레임이 준비되어야 하는 시점으로, VSYNC가 발생하기 전까지 프레임이 완성돼야 합니다.

VSYNC 타이밍에 맞춰 render loop도 시작합니다.

 

 

3 phase of Render Loop

render loop는 App, Render Server, On the display 총 3단계로 이루어져 있습니다.

 

App 단계는 이벤트가 처리되고, UI 변화가 생기는 단계입니다.

이 일이 다음 VSYNC 전에 완료되어야 지연 없이 다음 단계를 수행할 수 있습니다.

 

다음은 Render Server 단계입니다.

UI가 실제로 렌더링되는 단계로, 이 단계도 다음 VSYNC 전에 완료되어야 새로운 프레임을 보여줄 수 있습니다.

 

마지막은 On the display 단계입니다.

완성된 새로운 프레임이 화면에 표시됩니다.

 

 

Double Buffering

render loop는 Double Buffering으로 동작합니다.

Double Buffering은 2 프레임 전에 render loop가 시작하는 방식입니다.

 

 

Triple Buffering

Triple Buffering 방식도 있습니다.

이 방법은 3 프레임 전에 render loop가 시작됩니다.

 

이 경우 render server는 작업을 완료하기 위해 추가 프레임 시간을 받습니다. (render server가 두 칸인 이유임)

triple buffering 모드는 fallback 모드이므로, 이번 영상에서는 double buffering에 초점을 맞춰서 설명합니다.

 

+)

triple buffering 방식은 메모리 사용량이 많기 때문에 iOS에서는 double buffering을 사용합니다.

triple buffering은 PC 게임과 같이 높은 그래픽 성능을 요구하는 환경에서 사용된다고 합니다.

 

 

5 phase of Render Loop

render loop의 3단계를 더 세부적인 5단계로 나눌 수 있습니다.

 

App 단계는 Event, Commit으로 나뉩니다.

Event는 앱이 터치 이벤트를 처리한 뒤 UI를 변화시킬지 결정하고,

Commit은 Render Server에게 업데이트한 UI의 렌더링 요청을 보냅니다.

 

Render Server 단계는 Render prepare와 Render execute로 나뉩니다.

Render prepare는 다음 VSYNC에서 제출본을 받고, 새로운 UI를 GPU에서 그릴 준비를 합니다.

Render execute는 GPU에서 UI를 최종 이미지로 그립니다.

 

Display에서는 다음 VSYNC에서 사용자에게 최종 이미지인 프레임이 보여집니다.

 

5 phase of Render Loop

Render Loop 단계는 모두 중요합니다.

각 단계가 제대로 수행돼야 사용자에게 프레임 단위로 부드러운 경험을 줄 수 있기 때문입니다.

각 단계에서 어떤 일을 하는지 자세히 알아보겠습니다.

 

1. Event phase

사용자 터치, 네트워크 콜백, 키보드 입력같은 이벤트를 처리하고 UI를 변경시킵니다.

레이어 계층 구조를 변경해서 발생한 이벤트에 응답할 수 있습니다.

 

앱의 bounds를 변경할 때를 보겠습니다.

 

앱이 레이어의 bounds를 바꾸면 CoreAnimation은 setNeedsLayout을 자동으로 호출합니다.

또는 개발자가 setNeedsLayout( )을 직접 호출할 수도 있습니다.

 

setNeedsLayout이 호출되면 레이아웃 재계산이 필요한 레이어들을 식별하고, Commit phase로 넘어갑니다.

 

2. Commit phase

Commit phase는 레이어의 레이아웃을 배치하고, draw 합니다.

 

시스템은 레이아웃 업데이트 요청을 병합해서 중복을 제거하고 순서대로 처리합니다.

레이아웃의 변경이 필요한 레이어들을 부모부터 자식까지 하나씩 배치해 Layer Tree를 만듭니다.

레이아웃은 일반적인 성능 병목(bottleneck) 현상이므로 이 작업이 몇 ms 안에 이뤄진다는 것을 고려해야 합니다.

 

레이블, 이미지뷰나 drawRect를 오버라이딩한 커스텀 뷰는 커스텀 드로잉을 요구합니다.

만약 이런 뷰가 비주얼 업데이트를 요청하면 setNeedsDisplay를 반드시 호출해야 합니다.

레이아웃처럼 이 요청도 모든 레이아웃이 완료된 후 병합하여 한 번에 수행됩니다.

 

3. Render prepare phase

Render server는 레이어 트리를 진짜 화면에 표시되는 이미지로 바꿔줍니다.

prepare 단계에서 render server는 레이어 트리를 반복하고 GPU가 실행할 수 있는 선형 파이프라인을 준비합니다.

 

prepare 단계의 파이프라인은 최상위 레이어부터 시작해서, 뒤에서 앞의 순서로 뷰들을 정렬합니다.

(뒤에서 앞이란 하이어러키 기준이며, 부모에서 자식, 형제에서 형제 순서를 말합니다.)

 

4. Render execute phase

Execute 과정에서 선형 파이프라인이 GPU를 통과하여 최종 텍스처로 합쳐집니다.

이 과정에서 몇몇 레이어들의 렌더링이 또다른 병목 현상을 발생시키기도 합니다.

 

GPU가 실행되어 이미지를 렌더링하면 다음 VSYNC에 표시될 준비가 완료됩니다.

 

5. Display

준비가 된 이미지를 표시합니다.

 

Render Loop Parallelism

각 차례의 Render Loop는 성능에 민감하고

다음 VSYNC까지 완료되어야 합니다. (위 사진의 흰 선)

 

목표 프레임률을 달성하고, 낮은 입력 대기시간을 유지하기 위해 전체 프로세스가 병렬로 실행됩니다.

병렬 수행이기 때문에 시스템이 이전 프레임을 준비하는 동안 새로운 프레임을 준비할 수 있습니다.

 

 

Types of hitches

hitch에는 두 가지 종류가 있습니다.

commit hitch는 앱 프로세스 내에서 발생하고, Render hitch는 렌더 서버 내에서 발생합니다.

 

 

Commit hitch

commit hitch는 앱이 프로세스 이벤트나 커밋을 처리하는데 오래 걸리는 경우입니다.

 

위 사진에서 커밋은 너무 오래 걸려서 데드라인을 놓쳤습니다.

다음 VSYNC에서 렌더 서버는 처리할 내용이 없으므로 그다음 VSYNC까지 기다려야 합니다.

 

그래서 프레임 배송 시간을 한 프레임 늦추는데 이 지연 시간을 hitch time이라고 합니다. (주황색이 한 칸 늘어남)

아이폰, 아이패드의 60Hz 기종에서는 16.67ms 입니다.

 

Commit hitch가 더 늦어서 두 프레임(33.34 ms)를 늦으면 사용자가 부드러운 스크롤링을 볼 수 없습니다.

 

Commit hitch에 대해 더 이해하고, 어떻게 고치는지 알고 싶다면 "Find and fix hitches in the commit phase"를 보세요.

(다음 포스팅에서 다룹니다.)

 

 

Render hitch

Render hitch는 렌더 서버가 레이어 트리를 제 시간에 준비하지 못하거나, 실행할 수 없을 때 발생합니다.

 

여기서 렌더 서버는 데드라인을 넘겼습니다. (위 사진에서 약간 뒤어나옴)

따라서 프레임은 제 시간에 준비되지 못하고, 예상보다 한 프레임 늦게 표시됐습니다.

다행히 16.67ms의 hitch 타임이 있어서 부드러운 스크롤을 볼 수 있습니다.

 

render hitch와 레이어 트리 최적화에 대해 보고 싶다면 "Demysify and eleminate hitches in the render phase"를 확인하세요.

 

 

Measuring hitch time

지금까지 알아본 hitch time은 스크롤, 애니메이션, 트랜지션같이 오래 걸리는 이벤트에서는 다루기 어려울 수 있습니다.

각 이벤트의 시간(= 프레임 수)이 정확하게 동일하지 않고, iOS 기기가 항상 화면을 업데이트하지 않기 때문입니다.

iOS 기기는 렌더 서버에 커밋이 전송되지 않는다면 화면을 업데이트하지 않기 때문에 테스트 및 기기 간에 hitch time을 비교하기 어렵습니다.

 

hitch time ratio

그래서 hitch time ratio라는 측정법을 사용합니다.

hitch time ratio는 총 hitch time을 지속 시간으로 나눈 값입니다.

전체 시간으로 정규화되기 때문에 여러 상황을 비교할 수 있습니다.

 

hitch time ratio로 앱 성능을 측정하는 방법은 "WWDC20 - What's New in metricKit"에서,

테스트 suite에서 hitch time ratio를 추적하는 방법은 "WWDC20 - Eliminate Animation Hitches with XCTest"에서 확인할 수 있습니다.

 

hitch time ratio 예시

hitch time ratio을 사용할 수 있는 예시를 알아봅시다.

 

여기 30 프레임이 있고, 아이폰에서는 0.5초 정도의 작업입니다.

각 프레임이 데드라인 전에 완료되서 hitch가 전혀 없습니다.

hitch time은 0이며, hitch time ratio도 0입니다.

 

반대로 이 예시는 같은 30프레임에서 많은 hitch가 있습니다.

어떤 프레임은 다른 프레임보다 화면에 길게 나타나고, 다른 commit과 render들도 hitch를 발생 시킵니다.

이 hitch time을 합하면 100.02 ms이고, 0.5초의 hitch time ratio은 200.04 ms/s가 됩니다.

 

Good은 사용자가 대부분 눈치채지 못하고,

Warning은 사용자가 몇 개의 인터럽트를 눈치챕니다.

 

만약 hitch time ratio가 10 ms/s를 초과한다면 반드시 render loop를 최적화하는 방법을 고민해야 합니다.

사용자 경험에 큰 악영향을 주기 때문입니다.

 

감사합니다.

 

참고

https://developer.apple.com/videos/play/tech-talks/10855


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

 

반응형