본문으로 바로가기
고양이의 창가 노트 목록

LAB NOTE / 02

고양이는 쉬는데, 아이폰은 뜨거웠다

조용히 움직이는 고양이를 아이폰에 올렸더니 발열이 신경 쓰였다. 그림과 움직임을 유지하면서, 매 순간 반복하는 계산을 줄여본 과정.

고양이가 창가에서 천천히 고개를 돌리는 앱을 만들었다. 웹에서 확인하던 화면을 아이폰으로 옮겨 실행했는데, 휴대폰이 뜨거워졌다. 고양이는 한가롭게 쉬고 있는데, 그 모습을 보여주는 휴대폰은 꽤 바쁘게 일하고 있었다.

바라보며 쉬려고 만든 앱인데 손에 쥔 기기가 뜨거우면 오래 켜두기 어렵다. 처음에는 메모리를 너무 많이 쓰는 건 아닐까 싶었다. 무엇이 부담을 주고 있는지, 고양이를 움직이는 과정부터 살펴보기로 했다.

작은 고양이도 계산은 작지 않았다

화면 속 고양이는 작았지만, 그 그림을 움직이는 점은 기본 자세에서 약 1만 4천 개였다. 이런 점을 정점이라고 부른다. 그림 위에 촘촘한 그물을 얹어두고, 그물의 점을 움직여 그림이 휘거나 고개를 돌리는 것처럼 보이게 한다고 생각하면 된다.

고양이를 화면에서 30% 크기로 줄여도 이 점의 개수가 줄어드는 건 아니었다. 사용하는 원본 이미지의 해상도도 그대로였다. 눈에 작게 보인다는 이유만으로 계산량과 이미지 메모리가 함께 작아지지는 않았다.

이미 WebGL로 그림을 그리고 있었지만, 점들을 어디로 움직일지 계산하는 일은 JavaScript에서 하고 있었다. 고양이가 숨 쉬고 고개를 돌릴 때마다 모든 점을 계산한 뒤, 그 결과를 GPU에 다시 보내는 구조였다. GPU로 그리고 있다는 것과 움직임의 계산까지 GPU에서 한다는 것은 다른 이야기였다.

먼저, 매번 하지 않아도 되는 일을 줄였다

처음부터 그림을 바꾸거나 움직임을 덜어내고 싶지는 않았다. 대신 매 프레임, 즉 화면을 한 번 그릴 때마다 반복하는 일을 정리했다.

계산할 때마다 임시 배열을 새로 만드는 대신 이미 만든 공간을 다시 썼다. 움직임의 영향을 받지 않는 부분은 계산을 건너뛰고, 화면에 보이지 않는 개발용 UI도 계속 갱신하지 않게 했다. 앱이 화면에서 사라지면 그림을 갱신하는 일도 멈추도록 했다.

화면을 그리는 횟수에도 상한을 뒀다. 모바일에서는 초당 30회, 저전력 모드이거나 기기가 높은 발열 상태를 알리면 20회, 더 심한 상태에서는 15회로 낮췄다. 움직임은 실제로 흐른 시간을 기준으로 계산해서, 그리는 횟수가 줄어도 고양이가 슬로 모션처럼 움직이지 않게 했다.

첫 수정 전후에 iPhone 12 mini에서 약 20초씩 측정했다. 프레임 한 번에 JavaScript가 쓰는 평균 draw 시간은 21.74ms에서 14.27ms로 줄었다. 1초 동안의 draw 시간을 모두 더한 값은 약 808ms에서 286ms로 줄었다.

두 실행 모두 기기가 높은 발열 상태를 알리고 있었고, 수정 후에는 그 상태에 맞춰 초당 20회 제한이 적용됐다. 따라서 초당 작업 시간의 감소에는 반복 계산을 줄인 효과와 프레임 수를 낮춘 효과가 함께 들어 있다. 같은 손길을 그대로 재생한 실험도 아니어서, 이 숫자를 온도나 배터리 소모량의 감소율로 읽으면 안 된다.

횟수만큼 간격도 중요했다

그리는 횟수를 제한한 뒤에도 가끔 움직임이 버벅였다. 다음 프레임의 기준 시각을 매번 실제 실행 시각으로 옮기는 부분을 살펴봤다. 화면을 그리는 타이밍이 조금씩 늦어지면, 다음 기준도 함께 밀리면서 갱신 횟수가 줄어들 수 있었다.

기준 간격을 유지하도록 바꾸고, 약간 이르게 도착한 프레임에는 작은 허용 오차를 뒀다. 계산 결과를 담는 공간도 더 재사용했다. 단순히 초당 몇 장을 그리느냐뿐 아니라, 그 장들이 얼마나 고르게 나오는지도 봐야 했다.

같은 그림을, 다른 곳에서 계산하기

그다음에는 정점을 움직이는 계산을 GPU로 옮겼다. 고양이의 행동과 관절 방향 같은 제어값은 JavaScript에서 정하고, 그 값에 따라 수많은 점을 실제로 움직이는 일은 GPU가 맡게 했다.

이전에는 움직인 점의 최종 좌표를 매번 전부 보냈다. 바꾼 뒤에는 움직임을 정하는 작은 제어값을 전달한다. 변하지 않는 정보는 처음 불러올 때 한 번 준비해둔다. 그림, 색상, 점의 개수와 움직임의 타이밍은 그대로 두고 계산하는 위치를 바꾼 셈이다.

RENDERING고양이는 그대로, 계산하는 위치를 바꾸기
이전모든 점의 위치를 CPU에서 계산
  1. 행동과 관절 방향 정하기JavaScript
  2. 약 1만 4천 개 점의 위치 계산계산한 좌표 전체를 매번 전달
  3. GPU로 그림 그리기전달받은 좌표 사용
변경 후점을 움직이는 계산은 GPU에
  1. 행동과 관절 방향 정하기JavaScript
  2. 작은 제어값 전달하기변하지 않는 정보는 처음 한 번만
  3. GPU에서 점을 움직이고 그리기같은 그림과 움직임 유지
기본 자세의 처리 흐름을 단순화한 그림입니다. 눈꺼리의 일부 정점은 기존 CPU 계산을 유지합니다.

모든 부분을 한꺼번에 바꾸지는 않았다. 눈꺼리는 이전에도 그리는 과정에서 문제가 있었던 부분이라, 눈을 감는 정도에 따라 달라지는 154개 정점은 기존 CPU 계산을 남겼다. GPU 방식을 지원하지 못하거나 준비에 실패하는 경우에도 기존 방식으로 돌아가게 했다.

계산 시간을 줄여도 고양이의 얼굴이 달라지면 곤란하다. 그래서 네 가지 자세를 여러 크기와 표정으로 만들어, 총 72개 장면을 기존 방식과 비교했다. 고개를 돌릴 때의 연결, 꼬리 아래 몸통, 발의 위치와 쓰다듬기 반응도 함께 확인했다.

숫자와 손에 느껴지는 변화

Mac의 Chrome에서 같은 모바일 화면 크기로 비교했을 때, 서 있는 자세의 프레임당 JavaScript draw 제출 시간은 평균 3.30ms에서 0.53ms로 줄었다. 다른 자세에서도 이 시간은 줄었다. 다만 이것은 JavaScript가 그리기 작업을 준비하고 넘기는 시간이지, 앱 전체의 CPU 사용률이나 아이폰 성능을 뜻하는 수치는 아니다.

일을 넘겨받은 GPU 쪽 시간은 기본 자세에서 평균 0.36ms에서 0.65ms로 늘었다. 작업 위치를 옮긴 결과다. CPU와 GPU의 작업은 겹쳐 진행되기 때문에 두 숫자를 단순히 더해 전체 처리 시간이라고 볼 수도 없다. GPU용 데이터도 추가됐으므로, 이 전환 자체가 모든 메모리를 줄인 것은 아니다.

다시 iPhone 12 mini에 설치한 뒤에는 서 있는 고양이를 약 20초 동안 확인했다. 저전력 모드가 켜져 있어 초당 약 20회로 그렸고, JavaScript draw 시간은 평균 2.58ms였다. 그때 기기가 알려준 발열 상태는 정상이었다. 직접 써봤을 때도 이전보다 괜찮아진 것 같았다.

그렇다고 발열이 완전히 해결됐다고 말할 수는 없다. 표면 온도나 배터리 소모량을 따로 측정하지 않았고, 이전 기록과 전력 설정이나 OS 조건도 같지 않았다. 수정 후에도 긴 프레임이 일부 남아 있었다. 지금 확인한 것은 계산 시간이 줄었다는 것과, 아이폰에서 느껴지는 상태가 나아졌다는 데까지다.

편히 쉬는 화면을 만들려면

이번 작업에서 기억에 남은 건, 조용해 보이는 화면도 안에서는 많은 일을 할 수 있다는 점이다. 고양이를 작게 그리거나 움직임을 느리게 만든다고 해서 그 비용까지 저절로 작아지지는 않았다.

고양이를 덜 움직이게 하기 전에, 같은 모습을 보여주기 위해 매번 무슨 일을 하고 있는지 살펴보는 게 도움이 됐다. 반복할 필요가 없는 일을 줄이고, 많은 점을 움직이는 계산은 GPU에 맡기고, 민감한 부분은 기존 방식을 남겼다.

고양이의 표정만큼, 그 고양이를 오래 바라볼 수 있는 상태도 중요하다. 쉬어가는 앱을 만들려면 화면 안의 분위기와 함께, 화면 밖에서 기기를 쥐고 있는 사람의 손도 신경 써야 한다는 걸 배웠다.