asm-js

3 개의 포스트

figma

피그마는 웹어 (새 탭에서 열림)

WebAssembly 도입으로 Figma는 애플리케이션 초기화, 디자인 파일 다운로드, 첫 렌더링을 포함한 전체 로드 시간을 약 3배 단축했다. 특히 C++ 기반 코드를 브라우저에서 실행할 때 asm.js보다 전송·파싱·네이티브 코드 변환·캐싱 측면에서 유리했다. 다만 압축 후 다운로드 크기 감소 효과는 작았고, 당시에는 브라우저별 구현 및 캐싱 지원 차이로 적용 범위가 제한됐다. ## WebAssembly의 특징과 asm.js와의 차이 - WebAssembly는 브라우저 실행을 위해 설계된 바이너리 형식의 머신 코드다. - 기존에는 C++ 코드를 asm.js라는 JavaScript 부분집합으로 변환해 실행했다. - 숫자와 포인터만 사용할 수 있으며, 포인터는 숫자 배열의 인덱스로 표현된다. - DOM 조작이나 네트워크 연결 등 브라우저 기능은 JavaScript를 호출해야 한다. - WebAssembly도 asm.js와 동일한 제약과 브라우저 샌드박스를 공유하지만, 실행 형식이 더 효율적이다. - C++처럼 이미 LLVM으로 최적화된 코드는 브라우저가 별도 최적화를 많이 수행하지 않고 네이티브 코드로 변환할 수 있다. ## WebAssembly가 빠른 이유 - **작은 바이너리 형식** - 동일한 코드의 JavaScript보다 네트워크 전송에 유리하다. - 다만 압축하면 asm.js와 WebAssembly의 크기 차이는 크게 줄어든다. - **빠른 파싱** - 사람이 작성하는 JavaScript에는 문법과 중복 정보가 많다. - WebAssembly는 브라우저가 빠르게 해석하도록 설계되어 asm.js보다 약 20배 빠르게 파싱된다. - **효율적인 네이티브 코드 변환** - LLVM이 사전에 C++ 코드를 최적화하므로 브라우저의 런타임 최적화 부담이 작다. - **변환 결과 캐싱** - 브라우저가 WebAssembly 모듈의 네이티브 코드 변환 결과를 쉽게 캐시할 수 있다. - 같은 애플리케이션을 다시 열 때 변환 시간이 거의 사라질 수 있다. - **64비트 정수 지원** - WebAssembly는 64비트 정수를 직접 지원한다. - JavaScript는 정수 정밀도가 53비트로 제한되어 64비트 정수를 에뮬레이션해야 한다. ## Figma에서의 적용과 성능 개선 - Figma는 C++로 작성된 대규모 2D WebGL 렌더링 엔진을 사용한다. - 측정한 로드 시간에는 다음 과정이 모두 포함됐다. - 애플리케이션 초기화 - 디자인 파일 다운로드 - 디자인의 최초 전체 렌더링 - WebAssembly로 전환한 뒤 문서 크기와 관계없이 로드 시간이 3배 이상 개선됐다. - 큰 디자인 문서를 자주 만들고 전환하는 Figma 사용자에게 특히 큰 효과가 있었다. - 앱을 한 번 실행한 뒤에는 WebAssembly의 네이티브 변환 결과가 캐시되므로, 이후 로드 시간은 애플리케이션 코드 크기에 덜 영향을 받는다. ## 다운로드 크기와 실제 개선의 차이 - WebAssembly로 전환하면 전송 크기도 크게 줄어들 것으로 예상했지만 실제 감소 폭은 작았다. - asm.js 코드도 압축하면 WebAssembly와 비슷한 크기까지 줄어들기 때문이다. - 따라서 Figma에서 가장 큰 이점은 파일 크기 감소가 아니라 파싱, 코드 변환, 캐싱에 따른 실행 및 로드 시간 단축이었다. ## 브라우저 지원의 한계 - 당시 WebAssembly는 Firefox와 Chrome에서 기본 활성화되어 있었다. - Edge와 Safari는 아직 구현 중이어서 브라우저별 지원 상황을 확인해야 했다. - Figma는 Chrome에서도 WebAssembly를 사용할 수 있었지만, 구현상의 문제 때문에 실제로는 Firefox에서만 활성화했다. - 특히 Chrome의 네이티브 변환 코드 캐싱 방식이 Firefox와 달라 페이지를 열 때마다 애플리케이션 전체를 다시 변환해야 하는 문제가 있었다. WebAssembly는 대규모 C/C++ 코드베이스를 웹으로 옮길 때 특히 효과적이다. 단순히 다운로드 용량을 줄이는 기술이라기보다, 빠른 파싱과 네이티브 코드 변환, 실행 결과 캐싱을 통해 초기 로드 시간을 줄이는 기술로 보는 것이 적절하다.

figma

Emscripten으로 데이터 (새 탭에서 열림)

Figma의 저장 파일이 간헐적으로 손상되는 문제는 재현이 어려운 비결정적 이벤트와 C++의 메모리 안전성 문제 때문에 장기간 해결되지 않았다. 일반적인 메모리 디버깅 도구로 원인을 찾지 못한 뒤, 키보드·마우스 입력을 생성하는 퍼저로 이벤트를 반복 실행해 문제를 재현하는 데 성공했다. 최종 수정은 FlatBuffers의 잘못된 메모리 접근을 고치는 세 줄짜리 커밋이었지만, 원인을 추적하는 과정에서 Emscripten과 브라우저의 메모리 모델까지 분석해야 했다. ## 간헐적으로 발생한 저장 파일 손상 - Figma는 때때로 다시 읽을 수 없는 잘못된 저장 파일을 생성했다. - 당시 저장 형식은 다음 구조였다. - ZIP 파일 - 내부에 Google FlatBuffers로 인코딩된 문서 - 파일의 전체 바이트 구조는 대체로 정상처럼 보였지만, 뒤쪽 데이터를 가리키는 일부 오프셋이 0으로 변해 있었다. - 데이터가 원래 위치가 아닌 곳에 기록된 현상은 다음과 같은 메모리 안전성 위반을 의심하게 했다. - 초기화되지 않은 메모리 사용 - 해제된 메모리 접근(use-after-free) - 배열 범위를 벗어난 읽기·쓰기 ## C++ 메모리 오류의 추적 난이도 - Figma 에디터는 C++로 작성되어 있었다. - C++는 다음과 같은 장점 때문에 그래픽 소프트웨어에 적합하다. - FreeType, HarfBuzz, Skia 같은 저수준 라이브러리 활용 - 하드웨어와 직접 연결되는 언어 기능 - 성숙한 디버깅 및 최적화 도구 - 높은 성능과 세밀한 메모리 제어 - 반면 C++는 언어 자체가 메모리 안전성을 보장하지 않는다. - 복잡한 언어 설계, 오류가 발생하기 쉬운 표준 라이브러리 API, C에서 물려받은 레거시가 결합되어 대규모 프로젝트에서 메모리 오류를 완전히 피하기 어렵다. ## 일반적인 디버깅 방법의 한계 개발팀은 다음과 같은 방법을 시도했지만 문제를 발견하지 못했다. - 메모리를 해제하지 않아 use-after-free 가능성 제거 - macOS의 malloc 디버깅 옵션 활성화 - Valgrind가 보고한 모든 문제 수정 - 컴파일러 업그레이드 - Clang 정적 분석기가 발견한 문제 수정 이런 도구들은 많은 오류를 찾아내지만, 실행 조건에 따라 드물게 발생하거나 특정 메모리 배치에서만 나타나는 문제까지 항상 잡아내지는 못한다. ## 이벤트 기록과 퍼징으로 재현성 확보 - 문제의 핵심은 비동기 타이머와 네트워크 이벤트 등 웹 앱의 비결정성이었다. - 해결 전략은 다음과 같았다. - 사용자 이벤트를 기록한다. - 동일한 이벤트를 순서대로 재생한다. - 문제가 발생할 때까지 세션을 반복 실행한다. - 이벤트를 무작위로 제거하면서도 문제가 유지되는 최소 테스트 케이스를 만든다. - 실제 사용자 세션에는 너무 다양한 이벤트가 포함되므로, 범위를 키보드와 마우스 이벤트로 제한했다. - 실제 세션을 수집하는 대신 퍼저가 무작위 키보드·마우스 입력을 생성하도록 했다. - 며칠 동안 실행한 결과 여러 건의 저장 실패를 재현할 수 있었고, 이후 반복 가능한 입력 시퀀스를 기반으로 본격적인 디버깅이 가능해졌다. ## Emscripten이 C++를 브라우저에서 실행하는 방식 - Figma는 플러그인 없이 브라우저에서 실행되는 웹 앱이다. - C++ 코드는 Emscripten을 통해 JavaScript로 컴파일된다. - JavaScript는 기본적으로 메모리 안전성과 가비지 컬렉션을 제공하지만, WebGL과 Typed Arrays 덕분에 C의 메모리 모델을 흉내 낼 수 있다. - Typed Array의 특징은 다음과 같다. - 고정된 크기를 가진다. - 모든 원소가 같은 타입이다. - 여러 Typed Array가 하나의 ArrayBuffer를 공유할 수 있다. - 같은 메모리를 `Float32Array`와 `Uint8Array` 등 서로 다른 타입으로 해석할 수 있기 때문에 C/C++의 포인터 캐스팅과 유사한 동작을 구현할 수 있다. ## Emscripten의 포인터와 메모리 변환 Emscripten은 C++의 주요 요소를 JavaScript 구조로 변환한다. - 포인터 읽기: Typed Array 읽기 - 포인터 쓰기: Typed Array 쓰기 - 레지스터: JavaScript 지역 변수 - 스택: `STACKTOP`이라는 스택 포인터로 관리 - 타입 변환: 동일한 ArrayBuffer를 공유하는 Typed Array를 통해 구현 - 생성된 코드는 asm.js라는 제한적인 JavaScript 부분집합을 사용해 JIT 컴파일러가 타입을 추론하고 빠르게 최적화하도록 한다. - 예를 들어 `+$value`는 값을 double로 취급하도록 JIT에 힌트를 주며, 비트 연산은 정수 연산으로 최적화될 가능성을 높인다. ## 실용적인 결론 재현이 어려운 데이터 손상 문제는 도구를 하나씩 추가하는 것만으로 해결되지 않을 수 있다. 비결정적 입력을 통제하고, 퍼징과 이벤트 재생으로 실패 조건을 반복 가능하게 만드는 것이 핵심이며, 컴파일된 실행 환경에서는 원본 언어뿐 아니라 Emscripten의 메모리 모델과 런타임 동작까지 함께 이해해야 한다.

figma

웹에서 전문적인 디자인 도구 (새 탭에서 열림)

Figma는 전문 디자이너가 받아들일 수 있는 고품질 편집 경험을 웹 브라우저에서 제공하기 위해, 사실상 “브라우저 안의 브라우저”를 구축했다. 웹 플랫폼의 제한적인 추상화 대신 WebGL·asm.js 같은 저수준 기술을 활용하고, C++ 기반 편집기와 자체 메모리·렌더링 시스템으로 성능과 플랫폼 간 일관성을 확보했다. 특히 Emscripten을 통해 네이티브에 가까운 성능과 예측 가능한 프레임률을 달성하는 것이 핵심 전략이다. ## 웹에서 전문 디자인 도구를 만들기 어려운 이유 - 웹은 원래 문서 표시를 위해 설계되었고, 애플리케이션 개발 기능은 이후 개별 API 형태로 덧붙여졌다. - 따라서 특정 기능은 제공하지만, 개발자가 이를 조합해 새로운 동작을 구현할 수 있는 범용적인 저수준 primitive가 부족하다. - CSS는 복잡한 텍스트 배치 알고리즘을 제공하지만: - 알고리즘을 직접 커스터마이즈하기 어렵고 - 브라우저가 계산한 결과를 읽어 다른 알고리즘에 재사용하기도 어렵다. - 브라우저의 GPU 컴포지터는 고성능이지만: - 렌더링 과정에 직접 개입하기 어렵고 - 사용자 정의 블렌드 모드나 애플리케이션 특화 최적화를 추가할 수 없다. - 이미지 디코더는 하드웨어 가속과 비동기 처리를 지원하지만: - EXIF 방향 정보를 어떻게 처리할지 지정하기 어렵고 - 디스플레이 색 공간을 이미지 데이터에 미리 반영하지 않도록 제어하기 어렵다. - WebGL과 asm.js의 등장으로 개발자가 브라우저에 기능 추가를 기다리지 않고 하드웨어에 가까운 수준에서 필요한 기능을 직접 구현할 수 있게 되었다. ## Emscripten으로 C++ 편집기 실행 - Figma 편집기는 C++로 작성하고 Emscripten으로 JavaScript로 크로스 컴파일했다. - Emscripten은 asm.js를 대상으로 하며, asm.js는 JIT가 예측 가능하고 compact한 기계어를 생성하기 쉬운 JavaScript 부분집합이다. - 이 방식의 장점: - 메모리 배치를 직접 제어할 수 있어 64비트 부동소수점 중심인 JavaScript보다 32비트 float나 byte를 효율적으로 사용할 수 있다. - 객체를 미리 할당한 typed array 영역에 배치해 JavaScript 가비지 컬렉터의 개입을 피한다. - GC 중단으로 인한 프레임 저하를 줄여 60fps 달성에 유리하다. - LLVM 최적화와 C++ 템플릿 특수화를 활용해 네이티브 성능의 약 2배 이내 수준까지 접근할 수 있다. - asm.js에는 일반 JavaScript의 JIT 추론에 따른 deoptimization 지점이 없어 실행 성능이 더 예측 가능하다. ## 대용량 메모리와 브라우저 제약 대응 - Emscripten은 전체 메모리 공간을 하나의 큰 typed array에 담기 때문에 연속된 주소 공간을 충분히 확보해야 한다. - 특히 32비트 Chrome on Windows에서는 ASLR이 주소 공간을 파편화해 256MB typed array조차 할당하지 못하는 문제가 있었다. - Figma는 대형 이미지·기하 버퍼를 주 힙 외부의 별도 typed array에 저장하고, 이를 C++에서 참조하는 `IndirectBuffer` API를 만들었다. - 이 방식은: - 장시간 실행 시 메모리 파편화를 줄이고 - 32비트 브라우저의 제한된 주소 공간을 더 효율적으로 사용하며 - 64비트 브라우저의 31비트 typed array 크기 제한을 우회한다. - 해당 `IndirectBuffer` 구현은 오픈소스로 공개되었다. ## asm.js 이후의 발전 방향 - WebAssembly는 asm.js 코드를 바이너리 형식으로 표현해 JavaScript 파싱 시간을 크게 줄이는 것을 목표로 한다. - 당시 웹의 멀티스레딩은 Web Worker와 메시지 전달 방식에 의존했다. - Shared Typed Array가 도입되면 여러 실행 흐름이 메모리를 공유하는 진정한 공유 메모리 기반 멀티스레딩이 가능해질 것으로 전망했다. ## 자체 렌더링 엔진 - 브라우저의 그래픽 기능을 그대로 사용하는 대신, 콘텐츠를 빠르고 플랫폼 간 일관되게 표시하기 위해 자체 렌더링 엔진을 구현했다. - 이는 브라우저마다 다른 그래픽 구현과 동작 차이를 통제하고, 전문 디자인 도구에 필요한 고성능 렌더링을 직접 최적화하기 위한 선택이다. - 제공된 글 내용은 자체 렌더링 엔진의 필요성을 설명하는 부분에서 끝나므로, 구체적인 렌더링 구조와 최적화 기법은 확인할 수 없다. 브라우저 기반 고성능 그래픽 애플리케이션을 만들 때는 DOM과 일반 JavaScript API만으로 해결하려 하기보다, C++/WebAssembly 계열의 실행 모델과 명시적인 메모리 관리, 자체 렌더링 계층을 고려하는 것이 효과적이다. 특히 60fps가 중요한 편집 도구라면 가비지 컬렉션과 브라우저별 렌더링 차이를 구조적으로 줄이는 설계가 중요하다.