[번역] Blink, CC 및 스케줄러를 사용한 Chromium의 웹 페이지 렌더링

sejin kim·2024년 3월 24일
1

번역

목록 보기
6/9

이 글은 웹 개발자 Roman Maksimov님이 작성한 다음의 글을 한국어로 옮긴 것입니다 : Chromium. Web page rendering using Blink, CC and scheduler


Google의 크로미움 엔진은 방대한 내부 메커니즘과 하위 시스템 및 기타 엔진들로 구성되어 있습니다. 이 글에서는 웹 페이지를 구성하고 렌더링하는 프로세스에 대해 살펴보고, Blink 엔진과 composer*(또는 content collator**, 라고도 지칭됨) 및 작업 스케줄러에 대해 조금 더 자세히 알아볼 것입니다.

역주)
* '합성기(합성자)'. 이때 '합성'이란 브라우저 엔진에서 레이어로 분리된 각각의 요소들을 합성(composite)하고 웹 페이지로 구성(compose)하는 맥락에서의 행위, 기술을 가리키고 있습니다.
** '콘텐츠 조합기(조합자)'. 마찬가지로 합성기와 같은 맥락에서 지칭되는 composer의 대안적 명칭입니다.

참고 문서 : https://github.com/naver/whale-browser-developers/blob/master/translation/chromium/docs/how_cc_works.md





웹 페이지 구문 분석

먼저, 웹 페이지의 렌더링이 실제로 어떻게 이루어지는지 상기해 봅시다.

HTML 문서를 수신한 후 브라우저는 이를 구문 분석(parsing)합니다. HTML은 본래 기존의 XML 구조와 호환되도록 개발되었기 때문에, 이 단계에서는 딱히 흥미로운 기능은 없습니다. 분석의 결과로 브라우저는 계층 구조의 객체 트리인 DOM(Document Object Model)을 얻습니다.

브라우저는 HTML의 구조를 살펴보고 DOM으로 구문 분석할 때, 스타일 및 JS 스크립트(인라인 및 리모트 리소스 모두)와 같은 요소와 마주치게 됩니다. 이러한 요소들은 일련의 추가적인 처리가 필요합니다. JS 스크립트는 JavaScript 엔진에서 AST 구조로 구문 분석한 다음 엔진 자체의 내부 객체 형태로 메모리에 배치됩니다. 반면 스타일은 계단식 구조의 CSSOM 트리로 배열됩니다. 이때 요소의 인라인 스타일도 트리에 추가됩니다.

DOMCSSOM을 확보한 브라우저는 이제 각 요소를 배치 및 표시하기 위해 필요한 모든 계산을 수행할 수 있으며, 결과적으로 그래픽이 화면에 직접 렌더링되는 단일 렌더 트리(Render Tree)를 구성할 수 있게 됩니다.





웹 페이지 렌더링

오늘날 대부분의 브라우저는 멀티 스레드이며, 크로미움 엔진을 기반으로 하는 브라우저들 역시 예외는 아닙니다.


Blink라는 별도의 독립적인 시스템이 브라우저 탭의 모든 콘텐츠 렌더링을 담당합니다. 전반적으로 Blink는 크고 복잡한 엔진으로서, DOM, CSS 및 Web IDL 측면에서의 HTML 스펙 구현, V8 엔진 통합 및 JavaScript 코드 실행, Skia 엔진을 통한 그래픽 렌더링, 네트워크 리소스 요청, 입력 작업 처리, 트리 구축(DOM, CSSOM, 렌더 트리), 스타일 및 위치 계산 등의 기능을 아우르며 Chrome Compositor(CC)를 포함합니다.

엔진 그 자체로는 '즉시 사용 가능한(out-of-the-box)' 솔루션이 아니며, 독립적으로(standalone) 실행할 수 없습니다. WebKit 엔진의 WebCore 컴포넌트의 일종의 포크입니다.
Blink크로미움, 안드로이드 웹뷰, 오페라, 마이크로소프트 엣지 및 기타 수많은 크로미움 기반의 브라우저와 같은 플랫폼에서 사용됩니다.


Chrome Compositor (CC)

이러한 메커니즘은 크로미움의 코드베이스에서 cc/ 디렉토리에 위치하고 있습니다. 역사적으로 'CC'라는 약어는 Chrome Compositor로 해석되었지만, 현재로서는 컴포지터 자체로 기능하지는 않습니다. Dana Jensens(danakj)는 Content Collator라는 대안적 명칭을 제안하기도 했습니다.

CCBlink 엔진에 의해 기동되며, 싱글 스레드 및 멀티 스레드 모드에서 모두 동작할 수 있습니다. CC의 구체적인 동작에 대해서는 별개의 글에서 다루어야 하므로 여기서는 모든 메커니즘을 자세히 설명하지는 않겠습니다. CC가 다루는 주요 엔티티는 레이어와 레이어 트리라는 점만 짚고 넘어가면 됩니다. 이러한 엔터티는 CC에서 API로 제공됩니다. 이때 레이어는 그림(picture) 레이어, 텍스처(texture) 레이어, 표면(surface) 레이어 등과 같은 다양한 유형이 될 수 있습니다. CC 클라이언트(지금의 경우에서는 BlinkCC의 클라이언트 역할을 합니다)의 작업은 레이어 트리(LayerTreeHost)를 구성하고 CC에 렌더링할 준비가 되었음을 알리는 것입니다. 이러한 접근법은 최종 합성(composition) 작업을 생성하는 프로세스가 원자적으로 이루어질 수 있게 합니다.


기본 렌더링 체계

크로미움은 멀티 스레드 엔진입니다. 많은 특정 렌더링 작업들에는 각각 별도의 스레드가 할당됩니다. 기본 스레드는 메인 스레드컴포지터 스레드로 간주될 수 있습니다.

메인 스레드Blink가 동작하는 일반적인 스레드입니다. 여기에서 최종적으로 RenderTreeLayerTreeHost가 구성됩니다. 그룹화된 입력 연산도 여기서 처리되며, JavaScript 코드가 실행됩니다. 필요한 모든 작업과 연산이 완료된 이후 BlinkCC에 트리를 렌더링할 준비가 되었음을 알립니다.

컴포지터 스레드CC의 작업, 즉 렌더링 작업을 스케줄링하고 화면에 직접 그리는 작업을 담당합니다.

이때 Blink는 60 FPS(초당 60프레임)의 속도로 그래픽을 표시하기 위해 노력합니다. 즉, 하나의 프레임은 약 16.6ms 이내에 화면에 출력되어야 합니다. 이 프레임 속도는 일반적으로 사람이 인식하기에 최적인 것으로 간주됩니다. 이보다 낮으면 잔상(junking), 흔들림(juddering), 떨림(jittering)을 야기할 수 있습니다.

위의 다이어그램에서는 단순화된 렌더링 체계를 나타내고 있습니다. 앞서 언급했듯이, CC는 별도의 스레드에서 실행됩니다. 특정 시점이 되면 프레임 렌더링을 시작할 때가 되었다고 판단합니다. CC는 메인 스레드에 새로운 프레임을 시작하라는 신호를 보냅니다. Blink는 메인 스레드에서 이를 수신하고, 입력의 일괄 처리, JavaScript 코드 실행(더 정확하게는 이벤트 루프의 JavaScript 작업), RenderTree 업데이트 등 필요한 예약된 작업을 수행합니다. RenderTree가 준비되면 엔진은 임시 복사본인 LayerTreeHost에서 해당 변경을 수행하며, CC에 commit 신호를 보내면 CC는 모든 계산을 검색하고 OpenGLDirectX와 같은 OS 그래픽 라이브러리의 API를 사용하여 최종 디바이스에서 그래픽을 그리는 작업을 전송합니다.

이러한 전체 프로세스에는 약 1000/60 = ~16.6 ms의 시간이 주어집니다. 엔진이 이 시간 내에 필요한 모든 작업을 완료하지 못하면 프레임이 지연되어 프레임 속도가 감소합니다. 따라서 Blink의 중요한 작업 중 하나는 다가오는 작업의 실행 시간을 계산하고 예측하는 것입니다. 특정 작업에 소요되는 시간을 안다면, 엔진은 할당된 시간 내에 처리할 수 있는 작업만 수행하고 나머지 작업은 이후로 연기할 수 있습니다.

스크롤과 같은 특정 연산을 포함하지 않으며 JavaScript를 사용하지 않는 작업도 있습니다. CC는 이러한 작업을 자체 스레드에서 독립적으로 수행할 수 있으므로 메인 스레드를 차단하지 않습니다. 하지만 반대로 애니메이션의 경우에는 수많은 프레임에 걸쳐 메인 스레드에서 집중적인 연산이 필요합니다. 메인 스레드가 다른 우선 순위가 높은 작업으로 가득 차게 되면, 애니메이션 작업의 일부가 지연될 수 있습니다.





작업 스케줄러

작업 스케줄러는 프레임 업데이트가 지연될 가능성을 최소화하도록 설계되었습니다. 메인 스레드에서 실행되며, 각 작업은 엔진의 메커니즘에 의해 해당 작업 유형에 맞는 큐(대기열) 중 하나에 배치됩니다. CC 작업, 입력 처리 작업, JavaScript 코드 실행, 페이지 로딩 프로세스들은 제각기의 자체 큐로 이동됩니다.

큐 내의 작업은 배치된 순서대로 실행됩니다(이벤트 루프를 기억하세요). 하지만 다음 작업을 실행할 큐를 동적으로 자유롭게 선택할 수 있는 스케줄러는 자체 재량에 따라 대기열의 우선순위를 선택합니다. 언제, 어떤 우선순위를 선택할지는 스케줄러가 여러 다른 시스템에서 수신한 신호에 따라 결정됩니다. 예를 들어, 페이지가 아직 로드 중인 경우에는 네트워크 요청과 HTML 구문 분석에 우선순위가 부여됩니다. 그리고 터치 이벤트가 감지되면 스케줄러는 가능한 제스처를 정확하게 인식하기 위해 다음 100ms 동안 입력 작업의 우선순위를 일시적으로 높입니다. 이 시간 동안에는 다음의 이벤트로 스크롤, 탭, 확대/축소 등이 포함될 수 있다고 가정합니다.

스케줄러는 큐와 그 내부의 작업에 대한 전체적인 정보와 다른 컴포넌트들의 신호를 통해 시스템의 대략적인 유휴 시간을 계산할 수 있습니다. 바로 위에서 프레임 렌더링의 예를 살펴보았습니다. 프레임 렌더링 시작에 대한 CC의 신호와 함께 프레임을 사용할 수 있는 경우 다음 프레임의 예상 시간도 함께 전송됩니다(+16.6ms). 프레임 렌더링, 입력 처리 및 실행할 자바스크립트 코드에 필요한 작업을 사용할 수 있는지 여부를 알면 스케줄러는 이러한 작업에 소요되는 시간을 예측할 수 있습니다. 또한 다음 프레임의 시간을 알면 유휴 시간 역시 계산할 수 있습니다. 사실 이 시간은 정확히 말하면 유휴 상태는 아닙니다. 우선순위가 낮은 여러 작업(유휴 작업)을 수행하는 데 사용할 수 있습니다. 이러한 작업은 자체 큐에 배치되어 다른 대기열이 비워진 후에만 제한된 시간 내에 부분적으로 실행될 수 있습니다. 특히 가비지 콜렉터는 이 큐를 적극적으로 활용합니다. 죽은 객체를 표시하고 메모리를 조각 모음하는 대부분의 작업이 이곳에서 수행됩니다. 보수적인 가비지 콜렉팅은 메모리 부족이 감지되는 등의 극단적인 경우에만 높은 우선순위로 트리거됩니다. 이에 대해서는 V8의 가비지 콜렉션 아티클에서 더 자세히 논의하겠습니다.





프레임 속도 규칙성

크로미움의 프레임 속도는 60 FPS를 목표로 한다고 언급했었습니다. 이를 달성하기 위해 엔진에는 스케줄러, CC 및 기타 여러 시스템이 탑재되어 있습니다. 그러나 실제로는 내부 (하나 이상의 작업이 스케줄러에서 예상한 것보다 완료하는 데 시간이 오래 걸릴 수 있음) 및 외부 (예를 들면 다른 프로세스의 CPU나 GPU 로드 같은)에서 프로세스에 영향을 줄 수 있는 예기치 않은 상황이 많이 발생할 수 있으므로, 완벽한 규칙성을 달성하는 것은 거의 불가능합니다.

추적 도구에서 https://www.google.com/chrome 페이지의 스크롤이 대략적으로 관찰되는 모습입니다. 할당된 16.6ms의 window에 모든 애니메이션 프레임이 들어갈 수 있는 것은 아닙니다.

게다가 이러한 렌더링 지연 외에도, 일부 프레임은 Blink 엔진 자체에서 거부될(rejected) 수도 있습니다. 예를 들면 애니메이션 중에 이런 현상이 빈번하게 발생합니다. 애니메이션 프레임이 누락되는(dropped) 데에는 여러 이유가 있습니다. Blink는 약 스무가지의 이유를 가정하고 있습니다(이 글을 작성할 당시의 크로미움 버전은 124.0.6326.0이었습니다).

/third_party/blink/renderer/core/animation/compositor_animations.h#65

enum FailureReason : uint32_t {
  kNoFailure = 0,
  
  // 외부적인 요인으로 합성(compositing)이 불가한 경우
  kAcceleratedAnimationsDisabled = 1 << 0,
  kEffectSuppressedByDevtools = 1 << 1,
  
  // 애니메이션이 유효하지 않을 수 있는 경우가 다수 존재함
  // (e.g. 재생되지 않거나 효과가 없는 등)
  // 이런 경우에는 어떤 곳에서도 합성할 수 없으므로 함께 합쳐서 사용함
  kInvalidAnimationOrEffect = 1 << 2,
  
  // 컴포지터는 타이밍 값의 모든 설정을 지원할 수 없음;
  // CompositorAnimations::ConvertTimingForCompositor를 참조할 것
  kEffectHasUnsupportedTimingParameters = 1 << 3,
  
  // 현재 컴포지터는 다음과 같은 합성 모드 외에는 지원하지 않음
  // 'replace'.
  kEffectHasNonReplaceCompositeMode = 1 << 4,
  
  // 대상 요소(element)가 유효한 합성 상태가 아닌 경우
  kTargetHasInvalidCompositingState = 1 << 5,
  
  // 대상이 유효하지 않은 경우 (하지만 해결이 가능한 경우)
  kTargetHasIncompatibleAnimations = 1 << 6,
  kTargetHasCSSOffset = 1 << 7,
  
  // 서로 다른 transform 속성(예: rotate vs scale)을 대상으로 하는 경우
  // 동일한 대상에 여러 transform 관련 애니메이션이 허용될 수 있으므로
  // 이러한 실패 사유는 더 이상 사용되지 않음
  kObsoleteTargetHasMultipleTransformProperties = 1 << 8,
  
  // 애니메이션이 적용되는 속성과 관련된 경우
  kAnimationAffectsNonCSSProperties = 1 << 9,
  kTransformRelatedPropertyCannotBeAcceleratedOnTarget = 1 << 10,
  kFilterRelatedPropertyMayMovePixels = 1 << 12,
  kUnsupportedCSSProperty = 1 << 13,
  
  // 서로 다른 transform 속성(예: rotate vs scale)을 대상으로 하는 경우
  // 동일한 대상에 여러 transform 관련 애니메이션이 허용될 수 있으므로
  // 이러한 실패 사유는 더 이상 사용되지 않음
  kObsoleteMultipleTransformAnimationsOnSameTarget = 1 << 14,
  
  kMixedKeyframeValueTypes = 1 << 15,
  
  // 스크롤 타임라인 소스가 합성되지 않는 경우
  kTimelineSourceHasInvalidCompositingState = 1 << 16,
  
  // 컴포지터 속성의 애니메이션이 존재하지만
  // 해당 속성의 애니메이션이 효과가 없도록 최적화되어 있는 경우
  kCompositorPropertyAnimationsHaveNoEffect = 1 << 17,
  
  // important 표시된 속성에 애니메이션을 적용하는 경우
  kAffectsImportantProperty = 1 << 18,
  
  kSVGTargetHasIndependentTransformProperty = 1 << 19,
  
  // 새로운 값을 추가할 때 아래 count를 업데이트*하고*
  // tools/metrics/histograms/enums.xmlCompositorAnimationsFailureReason의
  // CompositorAnimationsFailureReason에 값에 대한 설명을 추가할 것
  // 이 enum의 최대 플래그 개수임(자신 제외)
  // 새 플래그는 이 count를 증가시켜야 하지만
  // 이 값은 UMA 히스토그램에서 사용되므로 절대 감소되어서는 안 됨
  // 또한 kNoFailure 값은 제외된다는 점에 유의할 것
  kFailureReasonCount = 20,
};

전체 중 가장 일반적인 사례는 시각적 애니메이션 효과(kCompositorPropertyAnimationsHaveNoEffect)가 없는 경우입니다. 애니메이션의 결과가 그래픽의 변경을 유발하지 않으므로 다시 그릴 필요가 없기 때문입니다. 또한, 프레임 리셋은 지원되지 않는 CSS 속성(kUnsupportedCSSProperty)으로 인해 발생할 수도 있습니다. 이는 속성 자체가 완전히 유효하더라도, 엔진이 특정 속성을 다시 계산하는 방법을 이해하지 못하는 경우 발생할 수 있습니다.

<style>
  #block1 {
    animation: expand 1s linear infinite;
  }

  @keyframes expand {
    to {
      height: auto;
    }
  }
</style>

<div id="block1"></div>

위의 예에서 엔진은 block의 높이를 계산하는 방법을 알지 못합니다. 최종 값이 정의되어 있지 않아 애니메이션 단계에서 계산할 수 없기 때문입니다. 결국 엔진은 더 이상 프레임을 계산하는 것이 의미가 없으므로 애니메이션의 첫 번째 프레임을 리셋한 이후로는 더 이상 계산을 시도하지 않습니다.

이 경우 추적 도구에서는 다음과 같은 레코드를 찾을 수 있습니다:

{"args":{"data":{"compositeFailed":8224,"unsupportedProperties":["height"]}},"cat":"blink.animations,...

이 모든 것들은 초당 렌더링되는 실제 프레임 수가 60 미만일 수 있다는 사실로 이어지게 됩니다.





프레임 속도 규칙성을 위한 지표로서의 불일치

애니메이션 기반의 애플리케이션에서는 평균 프레임 속도 외에도 프레임 렌더링의 규칙성이 중요합니다. 애플리케이션이 초당 60프레임(또는 이에 근접한)을 렌더링하지만, 프레임 간 간격이 크게 다른 경우 사용자는 잔상, 흔들림 또는 떨림을 경험하게 될 수 있습니다. 이러한 현상을 측정하는 방법으로는 단순히 가장 긴 프레임을 측정하는 것부터 프레임 길이의 차이를 계산하는 것까지 다양하게 존재합니다. 각각의 방법에는 장점이 있지만 특정한 사례에만 적용되며 프레임의 시간적 순서까지는 고려하지 않습니다. 특히, 누락된 두 프레임이 서로 가까이 있는 상황과 멀리 떨어져 있는 상황을 구분할 수 없습니다.

Google의 개발자들은 프레임 규칙성을 평가하기 위한 자체적인 방법을 제안했습니다. 이 방법은 몬테 카를로 적분의 수학적 방법과 유사한 프레임 지속 시간의 순서의 불일치를 기반으로 합니다.

이 방법의 이론적 기초는 2016년 제 37회 연례 ACM SIGPLAN 컨퍼런스에서 발표된 바 있습니다.

아래 그림은 프레임 규칙성의 불일치 예를 보여줍니다.

각 라인은 타임스탬프의 집합을 나타냅니다. 렌더된 프레임은 검은색 점으로 표시되고 누락된 프레임은 흰색 점으로 표시됩니다. 점 사이의 거리는 1 VSYNC*를 의미하는데, 60Hz 주사율에서는 16.6ms와 같습니다. 최종적으로 불일치는 VSYNC의 간격으로 계산됩니다:

역주)
* '수직 동기화(Vertical synchronization)'. 컴퓨터 그래픽스에서 디스플레이의 화면 업데이트 주기(주사율)와 그래픽카드가 프레임을 렌더링하는 속도를 동기화하는 기술을 말합니다. 이 글에서는 모니터 주사율(60Hz)과 동기화된 규칙적인 상태를 전제로 프레임 간의 간격을 단위 시간으로 나타내고 있으므로, 16.6ms와 같다고 설명될 수 있습니다.

D(S1) = 1
D(S2) = 2
D(S3) = 2
D(S4) = 25/9
D(S5) = 3

완벽한 경우(S1), 불일치는 프레임 간 간격(1 VSYNC)과 같습니다. 프레임 중 하나가 삭제된 경우(S2), 불일치는 렌더링된 두 프레임 사이의 가장 큰 거리와 같습니다. S2의 경우 가장 큰 거리는 포인트 2와 3 사이로, 2 VSYNC와 같습니다. 누락된 프레임이 두 개이지만 서로 멀리 떨어져 있는 경우에도 동일하게 적용됩니다(S3). 이 방법은 계산 공식에 반영된 대로 평균이 아닌 최악의 성능을 식별하는 것을 목표로 하기 때문입니다. 따라서 이 방법은 평균 프레임 지속 시간과 결합하여 단일 드롭아웃 프레임과 일련의 반복된 드롭아웃(마지막 프레임이 분명히 더 나쁨)을 구분합니다. S4의 경우, 서로 가까운 두 개의 드롭아웃 프레임이 보입니다. 이러한 프레임은 하나의 누락된 영역으로 간주되며, 여기서 불일치는 25/9(~2.7) VSYNC가 됩니다. S5의 경우 두 드롭된 프레임 사이에 렌더링된 프레임이 없기 때문에 상황은 더욱 악화됩니다. 여기서 렌더링된 프레임 사이의 가장 큰 거리는 포인트 2와 4 사이이며, 이는 3 간격(3 VSYNC)에 해당합니다.

profile
퇴고를 좋아하는 주니어 웹 개발자입니다.

0개의 댓글