deno 2차 외주 후기

4 min read

첫번째 외주 이후 deno쪽에서 외주 한번 더 맡기고 싶다고 했다. 처음엔 날 고용하고 싶은데 자기가 swc 가져다 쓰는 것에 기술적으로 깊게 관여한 게 아니라서 무슨 일을 맡겨야할지 모르겠다고 나한테 제안해보라고 했다. 난 deno용 ide 플러그인 같은 걸 생각하고 있었는데, Ryan Dahl이 swc 쓰는 것에 깊게 관여한 분과 얘기해서 필요한 기능을 정리해줬다. (참고로 ide 플러그인은 이미 있었다.)

시급제

역시나 시급제였다. 끝나고 생각해보니까 너무 빨리 끝나서 뭔가 억울하다. 좋은 아이디어를 내서 빨리 끝낸 건데...

요구사항

요구사항이 여러가지였고 기간은 3주였다. 내가 학생이고 방학이 8월 말까지라 2~3주만에 끝낼만한 것에 대해서 얘기를 나눴었다. 그래서 3주로 잡은 것 같다.

  • 여러가지 기능을 swc_ecmascript로 묶어서 버전 관리

기존에는

swc_ecma_ast = "0.17"
swc_ecma_codegen = "0.21"
swc_ecma_parser = "0.33"
swc_ecma_transforms = "0.12"

이런 식으로 써야 했다. 위의 버전은 대충 적은 것이고 무슨 버전끼리 호환되는지는 나도 코드 보기 전까진 모른다. 이걸 한 패키지로 묶어서

swc_ecmascript = "0.2"

이런 식으로 쓰게 해달라는 말이다.

  • 파싱할 때 swc_common::GLOBALS 설정하지 않아도 되게 변경

저게 tls(Thread local storage)인데 tls는 멀티쓰레드 환경에서 매우 불편하다. swc 프로젝트의 일부인 spack 도 쓰레드를 여러 개 사용하기 때문에 나도 저걸 바꾸고 싶었다.

  • 싱글 쓰레드에서 작동하는 파서

Mutex 같은 것들을 쓰지 않아도 되게 해달라는 말이다.

  • 주석 API 개선

기존 주석 API는 전역 변수와 비슷한 방식으로 작동했다. Tls를 썼기에 전역 변수하곤 좀 달랐지만, 내부적으로 Atomic 데이터 타입을 이용했다.

  • 윈도우 디버그 빌드에서 스택 오버플로 / recursion limit

이거 고치는 건 일반적으로 보통 일이 아니다. 이 항목은 어떻게 고칠 계획인지 따로 물어보더라.

  • https://github.com/swc-project/swc/issues/583

파서가 Span을 적절하게 설정하는지 체크하는 테스트를 만들어달라는 얘기다. 그것도 시각적으로. 아이디어 내는 데 오래 걸릴 줄 알았다.

  • swc_ecma_parser에 있는 함수들 크기 축소

이건 처음에 어떻게 접근해야 할지 감이 안 왔다. 자바스크립트도, 타입스크립트도 문법이 엄청 복잡해서 파서에 특수 케이스들을 처리하기 위한 코드가 많았는데, 당연히 코드가 많으면 크기가 크다. 근데 cargo bloat 돌려보니까 생각보다 많이 커서 고치기로 했다.

  • 의존성 최소화

deno 팀이 원하는 건 최종 바이너리의 크기를 줄이는 것이었다. 작업하기 전 크기는 45MB. V8을 제외하면 대부분 러스트일텐데 결코 작은 크기는 아니다.

고민

어떻게 하면 빠르게 할 수 있을지 고민하면서 정리해봤다.

  • Span 테스트는 창의적인 방법이 없으면 아예 불가능하다.
  • 파싱할 때 swc_common::GLOBALS가 필요하지 않게 만드는 건 쉽다.
  • 주석 API를 개선하는 것과 싱글 쓰레드에서 파서가 돌게 하는 건 쉽다.
  • 러스트 특성상 의존성 최소화하는 건 쉬운 부분이 많다.
  • deno 팀은 parking_lot을 의존성에서 없애고 싶어했는데, 이거 없애는 건 상당히 복잡한 작업이다.
  • swc_ecmascript를 만든는 건 쉽지만 먼저 해버리면 다른 작업을 할 때 버전을 여러 번 올려야 한다.
  • 윈도우 스택 디버깅은 다른 작업을 한 뒤 천천히 해도 된다.
  • 파서 사이즈를 줄이는 문제 역시 마찬가지다.

하다보면 다른 걸 해결할 방법이 생각나리라 믿고 방법을 확실히 알고 있는 것부터 하기로 했다.

작업

의존성 줄이기

우선 swc_common에서 deno가 쓰지 않는 기능을 cargo feature을 이용해서 제거했다. 두 개를 추가했는데, 하나는 tty-emitter이었고, 하나는 sourcemap이었다. tty-emitter는 말 그래도 tty 콘솔에 색 입히고 터미널 창 크기 같은 걸 계산하는 코드를 모아놓은 cargo feature로 만들었고, sourcemap은 웹에서 쓰이는 .map 파일을 생성하기 위한 코드를 모아놓은 cargo feature로 만들었다.

이어서 swc_ecma_parseronce_cellregex를 쓰지 않게 바꾸었다. Regexp가 딱히 필요하지 않은 코드였기에 간단했다. 그 다음엔 파싱할 때 swc_common::GLOBALS가 필요하지 않게 바꿨다. 이것 역시 간단했다. swc는 원래 Span을 압축해서 저장했는데, 이에 관련된 코드들을 지워버렸다. 물론 벤치마크 돌려보고 결과가 괜찮게 나와서 바꾼 것이다.

파서 API 개선

그 다음으론 파서 API를 개선했다. swc_common이랑 관련 있는 문제였는데, 파싱하면서 발견한 오류을 바로 swc_common::Handler에 넘겨주는 대신 파서가 저장하고 있다가 take_errors라는 메소드로 가져갈 수 있게 바꿨다. 또한, stack overflow 이슈를 위해 미리 파서 에러 struct의 크기를 8바이트로 줄였다.

Span 테스트

위의 작업을 마무리할 때 쯤 Span 테스트를 할 방법이 생각났다. 구현하는 건 별로 어렵지 않은 일이었기에 테스트 suite를 구현해놓고, 코드 리뷰 문제 때문에 일단 아주 간단한 예시만 몇개 넣어놨다.

주석 API 개선

주석 API를 개선하는 건 간단했다. trait object로 만들고, SingleThreadedComments라는 걸 만들어서 deno에서 가져다 쓸 수 있게 했다.

swc_ecmascript

어차피 swc_common의 API 버전을 올려야 하는 상황이었기에 이것까지 했다. 이 작업까지 마친 후 deno가 배포됐고, 40MB로 크기가 줄었다.

근데 cargo bloat를 돌려보니 파서 사이즈도 줄었다. 오래 걸릴 것 같던 작업 하나가 사라졌다.

cargo mono 제작

버전 업데이트가 너무 복잡한 것 같아서 CLI 도구를 하나 만들었다. 러스트 기반의 Monolithic 레포에서 쓰기 위한 툴인데, 버전을 한번에 올리거나 의존성 그래프를 만든 뒤 차례대로 배포해주는 기능이 있다.

당연히 오픈소스고, 소스코드는 github 에 공개되어 있다.

parking_lot 의존성 없애기

cargo feature은 원래 additive이어야 한다. 저게 무슨 말이냐면, feature을 임의의 라이브러리가 활성화해도 컴파일이 정상적으로 돼야 한다는 말이다. 근데 그렇게 만들려니까 손이 너무 많이 갈 것 같아서 방향을 바꿨다. 어차피 deno 입장에서 보면 저 feature을 켜는 crate를 쓸 일이 없으니까 addtive든 아니든 상관없다. 그래서 그냥 그렇게 갔다. 해당 cargo feature의 이름은 concurrent 인데, 저 기능을 켜면 Rc<RefCell<T>>Arc<Mutex<T>>로 바뀌는 등, 동시성에 관련된 타입들이 멀티 쓰레드에서 쓸 수 있게 업그레이드 된다.

근데 여기서 여러운 게 조금 있었다. swc_ecma_transformsArc에 저장된 전역변수에 의존한다는 점이 문제였는데, 전역변수를 아예 없애버리는 방식으로 해결했다.

Span 테스트

자바스크립트 / 타입스크립트 파일 적당히 작성하고 테스트 돌려서 생성된 테스트 파일 보면서 Span이 적절한지 살펴보고 그게 다였다. 어려운 건 없었고, 일일이 확인하는 게 상당히 귀찮았다. 테스트 파일은 63개 정도밖에 작성 안 했는데 PR 머지할 때 보니까 7천줄 넘게 늘어났더라.

윈도우 디버그 빌드 스택 오버플로

우선 에러 struct의 크기가 크게 줄어든만큼, 우선 stack overflow를 내는 코드를 찾아야했다. 그래서 러스트로 간단히 깃허브에서 특정 organization의 레포지토리를 전부 받아와서 컴파일 가능한 js, jsx, ts, tsx 파일들을 파싱하는 프로그램을 만들었다. 근데 스택 오버플로가 발생하지 않아서 그대로 작업이 끝나버렸다.