1인 회사 시스템 build

튜닝으로 11%를 줄이다 판을 바꾸니 10배가 됐다 — 병목은 인코딩이 아니었다

5분 영상 렌더가 15분 걸리던 파이프라인에서 인코더·GPU·동시성 튜닝의 천장을 확인하고, 프레임별 래스터화라는 진짜 병목을 스틸+concat 방식으로 뒤집은 기록입니다.


5분 영상에 15분이 걸렸습니다

제가 만든 영상 렌더 파이프라인은 브라우저에서 화면을 프레임 단위로 그려 이어 붙이는 구조였습니다. 화면을 실제 웹페이지로 렌더하니 레이아웃과 애니메이션을 그대로 재사용할 수 있어 편했습니다. 문제는 속도였습니다. 5분짜리 결과물 하나를 뽑는 데 15분이 걸렸습니다. 검수 한 번 하려고 15분을 기다리는 일이 반복되니, 이건 손봐야 하는 지점이 분명했습니다.

처음 의심한 건 당연히 인코딩이었습니다. 영상을 뽑는 일이니 병목도 영상을 압축하는 단계에 있으리라 짐작한 겁니다. 그래서 익숙한 손잡이부터 하나씩 돌렸습니다.

손잡이를 다 돌렸는데 11%에서 멈췄습니다

먼저 ffmpeg 인코더 프리셋을 손댔습니다. ultrafast로 바꾸면 압축에 드는 시간이 줄어드니 전체가 빨라질 거라 기대했습니다. 결과는 미지근했습니다. 전체 시간은 눈에 띄게 줄지 않았고, 대신 결과 파일이 9.85MB에서 28.8MB로 약 3배 부풀었습니다. 압축을 덜 한 만큼 용량만 커진 셈이라, 이건 이득이라 부르기 어려웠습니다.

다음은 GPU였습니다. gl=angle로 GPU 백엔드를 켜 보았습니다. 그런데 이득이 거의 없었습니다. 곰곰이 따져 보니 당연한 결과였습니다. 제 화면은 DOM과 CSS 합성으로 그려지는 평범한 웹페이지였고, GPU가 달려들어 처리할 3D 연산이나 셰이더가 없었습니다. GPU를 켜 봐야 정작 시킬 일이 없었던 겁니다.

동시성도 조정했습니다. 이것저것 병렬로 돌려 보고, 프리셋과 GPU 설정을 조합해 가며 가장 나은 지점을 찾았습니다. 이렇게 붙잡을 수 있는 손잡이를 전부 돌린 끝에 나온 숫자가 84.5초에서 79.4초, 딱 11% 단축이었습니다.

여기서 이상하다는 감이 왔습니다. 프리셋을 극단까지 밀고 GPU를 켜고 동시성을 조여도 개선폭이 한 자릿수 퍼센트에서 천장을 치면, 지금 조이는 나사가 애초에 병목이 아니라는 뜻입니다. 인코딩을 아무리 빠르게 해도 전체가 안 빨라진다면, 시간은 다른 데서 새고 있는 겁니다.

진짜 병목은 프레임을 그리는 일이었습니다

측정 대상을 인코딩에서 그 앞 단계로 옮겨 보니 답이 나왔습니다. 브라우저가 프레임 한 장 한 장을 래스터화하는 속도가 약 20fps에 고정되어 있었습니다. 압축이 느린 게 아니라, 압축에 넣을 프레임을 그려 내는 일 자체가 느렸던 겁니다. 5분 영상이면 프레임이 9,000장입니다. 한 장 한 장을 20fps로 그려 내고 있었으니, 인코더를 어떻게 손봐도 이 9,000번의 그리기가 시간을 다 잡아먹고 있었습니다.

병목의 정체를 알고 나니 질문이 바뀌었습니다. “어떻게 하면 프레임을 더 빨리 그릴까”가 아니라 “9,000장을 정말 다 그려야 하는가”였습니다. 영상 안을 들여다보니 대부분의 구간은 화면이 거의 변하지 않았습니다. 같은 장면이 몇 초씩 이어지는데, 그 초 동안 똑같은 그림을 수십 번 다시 그리고 있었던 겁니다.

그래서 접근을 통째로 바꿨습니다. 변화가 없는 구간마다 스틸 이미지를 딱 1장만 캡처하고, 각 스틸에 그 구간이 화면에 머물 지속 시간을 지정했습니다. 그리고 이 스틸들을 ffmpeg concat으로 이어 붙여 하나의 영상으로 만들었습니다.

# 구간별 스틸 1장 + 지속 시간을 concat 목록으로
file 'seg_001.png'
duration 3.20
file 'seg_002.png'
duration 1.05
...

프레임으로 따지면 9,000장이 99장으로 줄었습니다. 그리기 횟수가 두 자릿수로 떨어지니 렌더 시간은 15분에서 90.2초로, 대략 10배가 빨라졌습니다. 튜닝으로 몇 퍼센트를 깎던 세계와는 아예 자릿수가 다른 결과였습니다.

걱정한 건 정확도였습니다. 프레임을 다 그리지 않고 지속 시간으로 때우면 영상 길이가 어긋날까 봤는데, 각 구간의 지속 시간을 제대로 계산해 넣으니 최종 길이 오차가 ±0.02초에 그쳤습니다. 눈으로도 재생 시간으로도 문제가 없는 수준이었습니다.

검수용과 납품용은 다른 물건이었습니다

이 일을 겪고 나서 두 가지가 남았습니다.

하나는 파라미터 튜닝의 천장이 보이면 측정 대상 자체를 의심하라는 것입니다. 프리셋과 GPU와 동시성을 다 돌려도 11%에서 멈췄던 이유는, 제가 처음부터 병목이 아닌 것을 붙잡고 있었기 때문입니다. 손잡이를 부지런히 돌린다고 병목이 옮겨 오지는 않습니다. 개선폭이 자꾸 한 자릿수에서 멈춘다면, 나사를 더 세게 조이기 전에 프로파일을 다시 떠서 시간이 실제로 어디서 새는지부터 봐야 했습니다.

다른 하나는 결과물의 용도를 구분하는 일입니다. 프레임을 한 장씩 그리는 방식은 프레임 사이의 미세한 움직임까지 살아 있는, 말하자면 납품용에 가까운 화질을 냅니다. 반면 스틸 concat 방식은 변화 없는 구간을 뭉뚱그리는 대신 10배 빠릅니다. 검수는 내용이 맞는지 빠르게 확인하는 일이지 최종 화질을 감상하는 일이 아니었습니다. 검수용과 납품용은 애초에 다른 물건이었는데, 저는 그 둘을 같은 파이프라인 하나로 만들려다 15분을 태우고 있었던 겁니다.

지금은 빠르게 확인할 때와 최종본을 뽑을 때를 나눠서 다룹니다. 성능 문제라고 다 같은 성능 문제가 아니라는 것을, 11%와 10배 사이에서 배웠습니다.

자주 묻는 질문

ffmpeg 인코더 프리셋을 ultrafast로 바꾸면 렌더가 빨라지나요?

인코딩 자체가 병목일 때만 효과가 있습니다. 제 경우 ultrafast로 바꿔도 전체 시간은 11% 남짓 줄어드는 데 그쳤고, 대신 파일 크기가 9.85MB에서 28.8MB로 약 3배 커졌습니다. 병목이 인코딩이 아니라면 프리셋은 크기만 키우고 시간은 거의 못 줄입니다.

브라우저로 영상을 렌더할 때 GPU를 켜면 빨라지나요?

내용에 따라 다릅니다. DOM과 CSS 합성으로 화면을 그리는 경우 GPU가 실제로 할 연산이 거의 없어서, gl=angle 같은 GPU 백엔드를 켜도 체감 이득이 거의 없었습니다. 3D나 WebGL 캔버스를 많이 쓴다면 이야기가 달라집니다.

프레임별로 렌더하지 않고 영상 길이를 맞추는 방법이 있나요?

변화가 없는 구간은 스틸 이미지 1장으로 캡처하고, 각 스틸에 지속 시간을 지정해 ffmpeg concat으로 이어 붙이면 됩니다. 저는 9,000프레임을 99장으로 줄이고도 최종 영상 길이 오차를 ±0.02초로 맞출 수 있었습니다.

성능 튜닝이 한계에 부딪혔을 때 무엇을 먼저 의심해야 하나요?

파라미터를 아무리 조여도 개선폭이 한 자릿수 퍼센트에서 멈춘다면, 지금 튜닝하는 대상이 진짜 병목이 아닐 가능성이 큽니다. 프로파일을 다시 떠서 시간이 실제로 어디서 소모되는지, 측정 대상 자체를 의심하는 편이 빠릅니다.

#ffmpeg#영상 렌더링#성능 최적화#병목 분석#1인 개발