개발

모든 조건에서 같은 값이 나왔다 — 결론이 아니라 측정 실패였다


큰 배경 이미지에 CSS 필터를 하나 걸어야 했다. 화면 전체를 덮는 이미지라, 팬·줌 할 때마다 필터가 다시 계산되면 느려질 것 같았다.

지난번에 추정이 측정에 뒤집힌 적이 있어서 이번에도 재기로 했다.

측정 방법

requestAnimationFrame 으로 프레임 간격을 모으고, 드래그를 60회 재현했다. 필터를 켠 경우와 끈 경우를 줌 단계별로 비교했다.

window.__f = []; let last = performance.now()
const tick = (t) => { window.__f.push(t - last); last = t; requestAnimationFrame(tick) }
requestAnimationFrame(tick)

결과 — 전부 같았다

줌 필터 끔 (p95) 필터 켬 (p95)
0단계 16.8ms 16.7ms
6단계 16.7ms 16.8ms
12단계 16.8ms 16.8ms

중앙값도 여섯 조건 모두 16.7ms였다. “필터는 공짜다” 하고 넘어갈 뻔했다.

16.7이라는 숫자

그런데 16.7이 눈에 걸렸다. 1000 ÷ 60 = 16.67. 60Hz 화면의 vsync 주기와 정확히 같다.

세 가지가 동시에 이상했다.

  • 여섯 조건의 값이 전부 같다
  • 그 값이 이론적 상한과 정확히 일치한다
  • 최댓값만 33.4ms로 튄다 — 16.7의 딱 두 배, 한 프레임을 놓쳤다는 뜻

즉 이 측정이 말해주는 건 “16.7ms 안에 끝났다”까지다. 실제로 2ms가 걸렸는지 15ms가 걸렸는지는 천장에 가려서 보이지 않는다. 여유가 얼마나 남았는지 모르는 상태에서 “차이 없음”이라고 말하고 있었던 것이다.

원인은 내 오타였다

vsync를 끄는 플래그를 넘겼다고 생각했는데, 인자 형식이 틀렸다.

chromium.launch(['--disable-gpu-vsync'])            // 무시됨
chromium.launch({ args: ['--disable-gpu-vsync'] })  // 올바른 형식

첫 번째 형태는 오류를 내지 않는다. 옵션 객체 자리에 배열이 들어왔으니 알 수 없는 속성만 있는 객체로 취급되고, 플래그는 조용히 사라진다. 브라우저는 정상적으로 뜨고 측정도 정상적으로 돌아간다. 다만 아무것도 측정하지 못할 뿐이다.

상한을 풀고 다시

chromium.launch({ args: ['--disable-gpu-vsync', '--disable-frame-rate-limit'] })
줌 필터 끔 (p95) 필터 켬 (p95)
0단계 17.9ms 17.8ms
12단계 17.8ms 17.8ms
18단계 17.7ms 17.8ms

결론은 같았다. 필터는 실제로 비용이 없었다.

정적 필터는 최초 래스터화 때 한 번 적용되고 그 결과가 텍스처로 캐시된다. 이후 팬은 합성만 일어나므로 필터를 다시 계산하지 않는다. 값을 프레임마다 바꾸면 이 전제가 깨지지만, 켜고 끄기만 하는 용도라면 공짜에 가깝다.

한 가지 더 드러난 게 있다. 중앙값이 0.2~0.5ms였다. 상한이 있을 때는 16.7로 보이던 값이다. 프레임 대부분은 사실상 일이 없었다는 뜻이고, 이건 상한을 풀기 전에는 알 수 없던 정보다.

결론은 같았지만 자격이 달랐다

두 측정 다 “차이 없음”으로 끝났다. 하지만 첫 번째 측정으로는 그렇게 말할 자격이 없었다. 우연히 맞은 것이다.

만약 필터 비용이 실제로 8ms였다면 어땠을까. 첫 측정은 여전히 16.7ms를 보여줬을 것이다. 여유가 절반으로 줄었는데도 숫자는 한 자리도 움직이지 않는다. 그러다 다른 작업이 조금만 더해지면 갑자기 프레임이 깨진다.

계측을 의심해야 하는 신호

  • 모든 조건이 같은 값으로 나온다
  • 그 값이 이론적 상한이나 눈금과 정확히 일치한다 — 16.7ms(60Hz), 4ms·15.6ms(타이머 해상도), 딱 떨어지는 1.000초(타임아웃)
  • 분산이 이상하게 작다
  • 최댓값만 그 값의 정수배로 튄다

정리

  • 측정 결과가 균일하면 “차이가 없다”이기 전에 “측정이 되고 있나” 를 먼저 확인한다
  • 값이 이론적 상한에 붙어 있으면 그 아래는 전부 가려진다. 상한을 풀어야 실제 값이 보인다
  • 라이브러리 인자 형식이 틀려도 오류가 안 날 수 있다. 옵션이 조용히 무시되면 코드는 정상 동작하면서 아무 일도 하지 않는다
  • 정적 CSS 필터는 최초 래스터화 후 캐시된다. 값을 애니메이션시키지만 않으면 비용이 거의 없다

지난번엔 측정이 내 추정을 뒤집었고, 이번엔 측정이 아무 말도 못 하고 있었다. 숫자가 나왔다는 사실만으로는 부족하다. 그 숫자가 무엇을 볼 수 있는 위치에 있는지부터 봐야 한다.

댓글