• logo

      SeolMyeongTang

  • Firefox 브라우저, 멀티 코어와 만나다

    2023년 2월 08일
    Firefox meets multicore system
    Firefox meets multicore system

    해당 글은 MDN의 Entering the Quantum Era—How Firefox got fast again and where it’s going to get faster 글을 참고하여 작성하였습니다.

    2016년, Firefox를 관리하는 Mozilla 재단에서 Quantum 프로젝트를 발표합니다. 이 프로젝트는 나날이 성장하는 Google Chrome에 대항하기 위한 웹 브라우저를 내놓겠다는 내용이었습니다. 그리고 이 결실은 2017년에 이루어집니다. 2017년 11월, Firefox의 57.0 버전이 릴리즈 되면서 Firefox 웹 브라우저에 있어 큰 도약을 하게 됩니다. 기존의 레이아웃 엔진이였던 Gekco 엔진을 부분적으로 새 엔진인 Servo를 도입한 정식적인 버전이였습니다. 이 업데이트 이후로 Firefox는 상당히 빠르고 안정적인 웹 브라우징 경험을 사용자들한테 제공할 수 있었습니다. 이번 글은 Firefox가 어떻게 페이지를 빠르고 안정적으로 렌더링을 할 수 있었는지 알아보겠습니다.

    Quantum 프로젝트에서 가장 신경 쓴 부분은 멀티 코어 병렬처리입니다. Gecko 엔진이 나왔을 당시, CPU는 싱글 코어 구조로 Gecko 엔진 또한 코어 한 개를 기준으로 작성한 프로그램이었습니다. 2000년 중후반부터 여러 일(task)들을 독립적으로 실행할 수 있는 멀티 코어 프로세서가 시장에 하나 둘 나오게 됩니다. Gecko 엔진은 점점 시대에 맞지 않는 브라우저가 되었습니다. 결정적으로, 2008년 멀티 코어 설계에 맞춘 Google의 Chrome 브라우저가 나오면서 Firefox 브라우저의 설 자리가 좁아졌습니다. Firefox 팀도 멀티 코어 프로세서를 지원하는 웹 브라우저 엔진을 만들기로 결심합니다.

    Firefox 팀은 2008년부터 시작된 Electrolysis 프로젝트를 진행하면서 Chrome과 비슷한 멀티 코어 기반의 아키텍처를 설계했지만 실제로 Firefox 엔진에 적용하기 까지는 여러 난관이 있었습니다. 첫 번째로 싱글 코어에서는 고민하지 않아도 될 동시성 문제입니다. 멀티 코어 환경에서 병렬적으로 여러 태스크를 처리하려면 코어들 간에 메모리를 공유해야 합니다. 만약 두개의 코어가 같은 메모리에 접근해 연산 작업을 하게 된다면 race condition 문제가 발생 할 수 있습니다. 두 번째 문제로는 기존의 싱글 코어 기준으로 작성된 Gecko 엔진을 멀티 코어 설계로의 전환이 힘들었다는 점입니다.

    두 가지 문제에 대해 Firefox 팀이 내놓은 답은 생각보다 간단했습니다. 첫 번째의 문제로 멀티 코어의 메모리 공유로 인한 동시성 문제를 해결하기 위해, Firefox 팀은 두 개 이상의 코어가 메모리를 공유하지 않아도 될 정도로 프로그램을 한 덩어리 형태로 나누는 coarse-grained parallelism 방법을 도입했습니다. 이는 코어 간에 메모리 공유를 할 필요가 없어져 동시성 문제에서 벗어날 수 있습니다. 웹 브라우저에서는 탭(tab) 같이 coarse-grained parallelism 방식에 맞추기 쉬운 부분들이 있습니다. 각 탭들은 foreground에서든 background에서든 실행되어 서로 방해받지 않으면서 독립적으로 실행될 수 있습니다.

    두 번째는 기존 레거시 코드로 인한 전환 문제입니다. 이를 위해 Firefox 팀은 과감히 기존 엔진을 버리고 새롭게 엔진을 작성하는 선택을 했습니다. Firefox 팀은 2016년에 Quantum이라는 이름으로 프로젝트를 시작으로, 멀티 코어 지원을 위한 엔진 개발을 착수하여 2017년 11월에 발표한 Firefox 57 버전에 정식으로 출시합니다.

    Quantum 프로젝트는 여러 팀으로 나누어서 진행했습니다.

    • Quantum Compositor
    • Quantum DOM
    • Rust & Servo
    • Quantum CSS(Stylo)
    • Quantum Render(WebRender)
    • Quantum Flow

    Quantum 프로젝트 중 핵심 기능 중 하나인 Quantum Compositor가 있습니다. Compositor는 페이지를 layer로 나누고 각각의 layer를 rasterize 한 뒤, 다시 rasterize한 layer들을 하나의 페이지로 만드는 작업을 말합니다. Firefox 팀은 compositor를 메인 스레드로부터 분리했습니다. 스레드를 나눔으로써 GPU에서 그래픽 드라이버 문제로 페이지 작업이 충돌이 나도 메인 스레드의 영역까지 전파되지 않아 Firefox 브라우저 충돌로 이어지지 않습니다. 이 덕분에 안정적으로 브라우저를 유지할 수 있게 됩니다.

    Animation of compositing process
    Animation of compositing process 출처

    Compositor를 따로 분리해서 메인 스레드가 하는 일을 덜더라도 메인 스레드가 하는 일은 많습니다. 사용자 경험을 위해 쌓여있는 작업들을 처리하는 것도 중요하지만 작업 우선순위를 매기는 일 또한 중요합니다. 예를 들어 백그라운드 파일 다운이나 GC(Garbage Collector) 메모리 정리보다 사용자의 keypress에 대한 요청 처리를 먼저 처리해야하는 경우가 있습니다. Quantum DOM은 작업 우선순위를 결정하는 역할을 하여 Firefox가 사용자가 느끼기에 빠른 응답 속도 환경을 제공합니다.

    Main thread와 compositor thread의 분리
    Main thread와 compositor thread의 분리 출처

    Coarse-grained parallelism 방식을 적용하면 멀티 코어의 이점을 얻을 수 있지만 어떤 상황에서는 귀중한 코어 자원이 낭비되는 경우가 있습니다. 여러 페이지를 불러올 때, 만약 한 페이지에 한 코어를 담당한다면 싱글 코어 혼자 처리하는 것보다 많은 페이지를 빨리 실행할 수 있을 겁니다. 하지만 만약 4코어 환경에서 3페이지를 불러오는 작업을 한다면 1코어는 idle 상태가 되어버립니다. 또한 위의 방식은 멀티 코어라고 한들 한 코어당 한 페이지를 담당하므로 한 페이지가 실행되는데 걸리는 시간은 싱글 코어의 경우와 같습니다.

    4코어 환경에서 작업이 4개라면 1코어는 idle 상태가 된다
    4코어 환경에서 작업이 3개라면 1코어는 idle 상태가 된다 출처

    만약 한 페이지가 실행되는데 모든 코어가 쓰인다면 어떨까요? 하나의 덩어리로 작업하는 coarsed-grained 방식이 아닌 하나의 덩어리를 코어들이 일할 수 있도록 잘개 쪼개서 작업하는 fine-grained 방식을 사용한다면 더욱 효율적으로 페이지 실행을 할 수 있을겁니다. 예를 들어, Pinterest의 pin item들의 로딩 작업을 각 코어들한테 할당할 수 있습니다. 이제 작업 환경이 한 페이지 전체가 아닌 한 페이지 안에서 이루지다보니 빠르게 페이지를 불러올 수 있습니다. Fine-grained 방식은 coarsed-grained 방식처럼 멀티 코어 설계에 맞췄을 뿐만 아니라 코어가 많으면 많을수록 빨라지게 됩니다.

    Pinterest의 pin item 처리를 모든 코어에 할당한다
    Pinterest의 pin item 처리를 모든 코어에 할당한다출처

    하지만 뭔가 이상합니다. 한 페이지에 여러 코어가 붙어서 작업한다면 결국 메모리를 공유해야 할 테고 동시성 문제가 발생하게 됩니다. 동시성 문제를 피하려고 coarse-grained 방식을 선택한 건데 최적화를 더욱 하려다 보니 원점으로 돌아와 버렸습니다.

    Coarse-grained parallelism과 fine-grained parallelism 방식의 차이
    Coarse-grained parallelism과 fine-grained parallelism 방식의 차이 출처

    Firefox 팀은 궁극적으로 웹 브라우저가 나아가야하는 방향은 coarse-grained 방식이 아닌 fine-grained 방식이라 생각했습니다. 여러 연구 끝에 Firefox 팀은 race condition 문제를 해결하면서 멀티 코어 프로그래밍이 가능한 새로운 언어를 만들었습니다. 그리고 이 언어를 토대로 fine-grained parallelism 방식을 적용한 새로운 웹 브라우저 엔진을 만들었습니다. 바로 Rust 언어와 Servo 엔진입니다. 동시성 문제를 위해 Rust는 소유권(ownership)과 타입 안정성을 도입하여 해결했습니다. Rust가 동시성 문제를 해결하기 위해 어떠한 개념을 도입했는지는 다음 기회에 한번 정리해 보겠습니다.

    Rust language
    Rust language

    Stylo라고도 하는 Quantum CSS는 모든 코어를 활용하여 병렬적으로 CSS 스타일 값을 계산합니다. Stylo는 모든 코어들이 busy 상태를 유지하기 위해 일을 효율적으로 자르는 work stealing 이라는 기법을 사용합니다. Work stealing 기법을 통해 선형적으로 속도 향상을 얻을 수 있으며 코어가 많을수록 계산하는 시간이 줄어듭니다.

    하드웨어가 발전하면서 멀티 코어와 더불어 눈부신 발전을 한 부분은 GPU 입니다. GPU에는 수백 개의 GPU 코어들이 있는데 WebRender는 이 코어들을 모두 활용하여 페이지를 렌더링 합니다. GPU 가속기인 WebRender는 초당 60프레임을 뽑아내 자연스러운 페이지 애니메이션 효과를 나타냅니다.

    앞서 소개한 Compositor, DOM, Rust, Stylo, WebRender을 맡은 팀들은 기술 구현 및 최적화에 맞춰져 있습니다. Quantum Flow는 Firefox의 전체적인 성능에 집중하는 대신 특정한 상황에서의(ex. SNS 피드 조회) 성능과 버그에 집중하였습니다. Quantum Flow 팀은 버그나 이슈 상황을 찾기 위한 개발자 도구 프로그램들을 만들고 이슈를 찾는 과정들을 프로세스화 시켰습니다. Quantum Flow 팀의 역할이였던 특정 상황을 가정하고 테스트하는 작업은 다른 모든 팀들의 workflow에 자연스럽게 녹아들었습니다. 이는 생각보다 꽤나 성공적이고 만족스러운 결과를 만들냈는데, 모든 팀들이 이슈를 찾기 위한 개발자 도구 프로그램을 발전시키고 더욱 많은 엔지니어들이 버그를 Quantum Flow 팀 도움 없이 찾을 수 있는 환경이 만들어졌습니다. 모든 엔지니어들이 버그를 찾고 고쳐 성숙한 프로그램들을 만들었고 모든 팀이 QA를 하여 안정적인 프로그램을 만들 수 있는 문화가 자연스럽게 정착되었습니다.

    하지만 이 접근법에 문제점이 하나 있습니다. 특정한 한 상황에 대해 테스트를 하고 있을 때, 다른 상황에 대한 최적화를 하지 못 한다는 것입니다. 이러한 문제를 방지하기 위해 여러 절차를 만들었습니다. 성능 테스트를 위한 CI 자동화, 사용자 경험 추적 등을 도입하여 지속적인 관리를 하였습니다.

    Quantum Compositor부터 여러 기술들이 합쳐져 Firefox는 Google Chrome에 대항할 멀티 코어를 지원하는 브라우저가 되었습니다. 이번 글을 통해서 전체적인 Servo 엔진이 탑재된 Firefox 구조에 대해 알아보았습니다. Quantum CSS(Stylo)Quantum Render(WebRender)에 대한 추가적인 글이 있으니 구체적인 구조를 파악하고 싶다면 해당 글을 추천해 드립니다. 추가로 Google에서 작성한 Inside look at modern web browser 시리즈도 있으니 웹 브라우저 구조에 관심있는 분들은 참고하면 좋을 것 같습니다.