FPS Arena가 웹에서 뚝뚝 끊긴다는 제보가 계속 왔습니다. 그런데 평균을 재면 13ms, 초당 60프레임입니다. 숫자로는 멀쩡합니다.

끊김은 평균에 안 보입니다. 스무 프레임 중 하나가 33ms로 튀면 나머지 열아홉이 아무리 좋아도 눈에는 그 하나가 전부입니다.

먼저 화면에 끊긴 횟수를 띄웠습니다

평균과 최악만으로는 진행 상황을 알 수 없어서, 33.4ms를 넘긴 프레임의 횟수를 띄웠습니다.

16.7ms (60 fps) · 최악 16.7ms · 끊김 0회/20s

고칠 때마다 이 숫자가 줄어드는지를 봤습니다. 평균은 거의 안 움직이는데 이쪽은 움직입니다.

봇 아홉이 같은 프레임에 몰려 있었습니다

첫 번째로 잡은 것입니다. 봇이 아홉이면 시야 검사를 같은 프레임에 몰아서 했습니다. 상대마다 레이캐스트 한 번씩인데, 맵 충돌 메시가 20만 삼각형이라 그 프레임만 훅 튑니다.

감지 시각을 흩뿌리고, 스킬 판단이 또 쏘던 레이캐스트는 방금 잰 값을 쓰게 했습니다. 한국 마을 웹 실측으로 평균 19.0 → 15.5ms, 최악 46 → 39ms.

여기서 제가 원인을 잘못 짚었습니다

그 다음에 "봇 시야 레이캐스트가 프레임당 6.6~28ms"라고 적었습니다. 틀렸습니다.

레이캐스트를 끄는 진단 스위치가 레이캐스트만 끄는 게 아니라 봇이 표적을 못 잡게 만들어서 전투 자체를 멈추고 있었습니다. 그러니 그 스위치를 켜고 끈 차이는 "레이캐스트 비용"이 아니라 "전투가 도는 비용"이었습니다. 저는 그 차이를 레이캐스트 값으로 읽었습니다.

직접 재 보니 봇 아홉이 쏘는 레이는 프레임당 0.1ms도 안 됩니다.

그래서 진단에 "사격 끔"을 따로 넣고, 레이캐스트만 직접 재는 계측을 새로 넣었습니다. 스위치 하나가 여러 가지를 동시에 끄면, 그 스위치로 잰 값은 무엇의 값도 아닙니다.

충돌은 쪼개고 그리기는 합쳐 둡니다

레이캐스트가 비싼 건 맞았습니다. 재질별로 합친 메시로 충돌까지 세우면 한 덩어리가 맵 전체를 덮습니다. 총알과 시야 레이 하나하나가 20만 삼각형짜리 상자를 때립니다.

충돌만 원본 오브젝트마다 세우니 물리 브로드페이즈가 먼저 걸러 줍니다. 같은 씨앗으로 3,000발씩 쏴서 재니 레이 하나가 14.6μs → 11.4μs가 됐습니다.

그리기는 합친 채로 뒀습니다. 격자로도 쪼개 봤는데, 프러스텀 컬링이 살아나 삼각형은 38% 줄었지만 그리기 호출이 400에서 870으로 늘어 웹에서는 오히려 느려졌습니다 (15.5 → 17.8ms). 웹은 단일 스레드라 호출 수가 더 비쌉니다.

같은 메시를 충돌은 쪼개고 그리기는 합치는 게 답이었습니다. 둘의 병목이 다릅니다.

2.9초짜리 구멍은 게임 밖에 있었습니다

가장 큰 것이 남아 있었습니다. 판 도중 2.9초씩 멈췄습니다.

게임 안에 건 계측에는 하나도 안 잡혔습니다. 브라우저 표본 프로파일을 뜨니 25초를 재는 동안 셰이더 링크를 기다리는 자리에 2.9~6.0초가 들어가 있었습니다. 멈추는 곳이 스크립트가 아니라 드라이버 안이라 제 계측이 볼 수 없었던 것입니다.

구멍 길이(2890·2919·2936·2951·2969·2995ms)가 그 값과 거의 같아서 범인이 맞았습니다. 힙도 안 변해 GC도 아니었고, 같은 브라우저·같은 조건에서 빈 페이지를 재니 평균 0.27ms에 구멍이 없어서 환경 탓도 아니었습니다.

판이 서면 작은 뷰포트에 같은 월드를 두 번 그려서 셰이더를 미리 굽습니다. 맵 전체를 위에서 한 번 — 사람들이 다 선 뒤에 해야 캐릭터 재질도 같이 잡힙니다. 그리고 맵에서 800m 아래에 탄착·피·예광·탄피·파편·연막·섬광을 한 번씩 내고 봅니다 — 판 시작에는 화면에 없어서 첫 번째로는 안 잡히는 것들입니다.

튄 프레임 하나를 뜯어보게 했습니다

"끊김 2회/20초"처럼 드문 스파이크는 합계에 묻힙니다. 알고 싶은 건 그 한 프레임에 뭐가 있었나입니다. 33.4ms를 넘기면 그 프레임에 쌓인 일을 한 줄로 찍게 했습니다.

[튄프레임] 127.8ms 짜리 · 소리 재생 9.90ms · 죽음 처리 1.80ms · 킬로그 1.30ms

그렇게 잡힌 것이 킬 소리가 처음 날 때의 9.9ms였습니다. 스트림을 처음 물릴 때 드는 값이라 두 번째부터는 안 듭니다. 판이 서기 전에 열세 가지를 −80dB로 한 번씩 울려 미리 물려 뒀습니다.

같은 계측이 초상 촬영이 14초씩 걸린다고도 알려 줬는데, 그중 12.9초는 벤치가 탭에 포커스를 주기 전이라 브라우저가 프레임을 멈춰 둔 것이었습니다. 사람이 하는 판에서는 안 그렇습니다. 다시 헷갈리지 않게 그 대기 시간을 따로 찍게 했습니다.

남는 것

이번 작업에서 실제로 고친 것보다 계측을 만든 시간이 더 길었습니다. 끊긴 횟수, 레이캐스트 전용 측정, 튄 프레임 분해까지 셋을 새로 만들었습니다.

그리고 그중 하나는 잘못된 스위치로 재서 틀린 결론을 냈고, 그 결론을 글에도 적었습니다. 측정 도구가 무엇을 끄고 있는지를 확인하지 않으면, 그 도구로 잰 숫자는 이름만 그럴듯한 값이 됩니다.