Wasm 관련 삽질기

4 min read

이슈

특정한 경우에 rkyv에서 LayoutError로 패닉이 난다는 이슈 제보가 있었다. 꽤 오래된 이슈인데, 알고보니까 러스트 컴파일러 버전에 따라 다르게 작동하는 것이었다. 이것저것 코드를 보다가 rkyv@v0.7.37에 UB가 있어서 깨지는 것이라고 결론을 내렸다. 방법은 rkyv를 업데이트하거나 다른 걸로 바꾸는 것뿐이었는데, 어떻게 처리할지 오래 고민했다. rkyv@0.7.39엔 데이터가 날아가버리는 버그가 있어서 업데이트하기도 곤란했다.

한 플러그인 저자분이 스테이블인 1.67으로 컴파일해도 깨진다고 하셔서 rkyv의 UB인 게 확실해졌다. 다른 분이 테스트한 결과, nightly-2022-11-23까지 작동하고 nightly-2022-11-24부터 깨진다고 한다.

그런데 내가 이 이슈를 팀하고 공유하는 걸 까먹은지라 팀원이 next.js의 rustc 버전을 올렸고, 덕분에 next.js v13.2.4에서 swc 플러그인을 실행할 수 없다는 이슈가 올라왔다. 발등에 불이 떨어진 셈이라 빠르게 해결해야했다.

rustc & rkyv@v0.7.40 업데이트

rkyv 버그는 호스트나 플러그인 둘 중 하나만 깨지면 작동 안하는 버그라 @swc/core의 러스트 컴파일러 버전을 한동안 못 올리고 있었다. 최신 버전 rkyv가 새로운 rustc에선 작동하는지 검증할 겸해서 rsutc를 nightly-2023-03-28로, rkyvv0.7.40으로 올렸다. 근데 러스트는 컴파일러 버전을 업그레이드하면 린트 룰이 많이 추가되기 때문에 이 작업에 시간이 꽤 많이 들었다. 힘들긴 했는데 그래도 보니까 잘 작동하는 것 같아서 기분이 좋았다. 이 이슈가 오랫동안 골치였는데 그래도 잘 해결됐구나 싶었다.

배포 실패

그런데 배포를 시도해보니까 문제가 발생했다. swc는 배포하기 전에 도커를 이용해 각 CPU 아키텍쳐별로 테스트를 돌리는데 리눅스에서 jest가 별다른 에러 메시지 없이 죽어버리는 문제가 발생했다. rkyv@v0.7.37, rkyv@v0.7.39 때문에 전에도 삽질을 엄청 했던지라 당연히 rkyv 문제일거라 생각했다. 그래서 일단 Revert하고 고민하기로 했다.

Revert

위에서 말한 rustc 업데이트 작업에 시간이 꽤 들었기 때문에 Revert 할 때 기분이 안 좋았다. CI 기다리는 걸 좋아하지 않지만 이떄는 특히 짜증났다. 그래도 뭐... 선택지가 없으니 했다.

rkyv => rmp-serde

당연히 rkyv가 문제일 거라 생각했기에 아예 드랍하려고 했다. 조금 느려지더라도 serde 기반으로 바꾸면 하위 호환성 문제도 어느 정도 해결될 것이라서 열심히 작업했다. 근데 serde는 여러가지 메시지 포맷을 지원하는 프레임워크라 여러가지 포맷을 시도해봤다.

일단 rkyv 의존성을 없앴는데 쉬운 작업은 아니었지만 그래도 어찌어찌 하긴 했다. vscode의 프로젝트 검색 기능으로 rkyv를 찾은 뒤 하나하나 수작업으로 지웠다. 그러면서 rkyv::Archive 트레잇 바운드를 serde::Serialize로 바꾸는 등, rkyv 타입 사용을 전부 serde 기반으로 바꿨다.

serde 기반으로 플러그인 시스템을 재구성한 후, rmp-serdeto_vec을 사용했는데 안 됐다. 에러 메시지도 그닥 도움되는 종류는 아니어서 쓸데없는 짓을 한건가 싶은 마응으로 serde_json을 시도해봤다. 작동헀다. 희망을 봤다고 해야하나? bincode도 시도해봤다. 그런데 bincodeserde::Deserializer::deserialize_any를 구현하지 않기 때문에 사용할 수 없었다. 그래서 고민했다. serde_json은 너무 느리고, 빠른 포맷은 대부분 deserialize_any를 지원하지 않을 것이었다. 근데 생각해보니까 이상했다. msgpack 포맷에 대해 잘 아는 건 아니지만, rmp-serdedeserialize_any를 구현하는 건 확실했다. 그리고 위에서 말한 도움이 되지 않는 에러 메시지는 직렬화된 데이터를 전혀 다른 필드에 갖다 꽂으면 나올 수 있는 메시지였다. 포맷 내부에 필드 이름 같은 걸 인코딩하는 방법이 있을 거라는 결론을 내리고 문서를 보니까 rmp-serdeto_vec_named라는 메소드가 있더라. noop 플러그인 테스트를 돌려보니까 잘 작동했다. noop 플러그인 테스트는 파서 테스트셋 전체를 noop 플러그인에 넘겼다가 돋려받아서 데이터가 유지되는지 확인하는 테스트인데, 이게 작동한다는 건 AST의 직렬화와 비직렬화가 올바르게 동작한다는 것을 의미했다. 그래서 프로파일링을 돌려봤다. 성능은 2배 이상 느려졌지만, 머리 아픈 이슈도 해결하고 어느 정도의 하위호환성까지 얻는 거라 감수할 수 있다고 생각하고 기쁜 마음으로 PR을 마무리하고 다른 작업으로 넘어갔다.

CI 실패

그런데 CI 테스트가 실패해서 자동 머지가 작동하지 않았다. CI 로그 보고나서 로컬에서 디버깅을 해보니까 튜플 타입에 문제가 있더라. 대체 어느 쪽 버그인지는 모르겠는데, 관련해서 디버깅을 하다가 갑자기 든 생각이 하나 있었다.

rustc & rkyv@v0.7.40 업데이트 재시도

그 생각이란 이미 swc_plugin_runner의 테스트에서 rkyv를 지겹도록 테스트하고 있다는 것이다. swc_plugin_runner 테스트는 모든 PR에 대해서 도는 테스트기 때문에, rkyv가 깨진 거라면 위의 PR에서 잡혔어야한다. 그리고 배포 파이프라인 로그를 보면 테스트가 리눅스에서만 실패했다. 그래서 위의 Revert PR을 Revert 한 뒤, 깃허브 코드스페이스를 이용해 리눅스 환경에서 빌드했다. 그러고나서 테스트를 돌려보는데 rkyv 테스트들은 잘 작동했다. 그리고 디버깅하다가보니까 전체 테스트를 돌리니까 yarn test가 멈추는 경우가 있었다. rkyv하고 관계 없는 테스트였는데, 위의 배포 실패 로그를 보니까 멈추는 위치가 yarn test가 멈추는 시점과 같았다.

PASS unit tests node-swc/__tests__/transform/plugin_test.js
PASS unit tests node-swc/__tests__/dynamic_tsx.mjs
PASS unit tests node-swc/__tests__/preserve_comments.mjs
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

즉, rkyv와 관련 없는 swc 코드가 깨졌다는 소리다. rustc의 miscompilation이라는 결론을 내리고 러스트 버전을 nightly-2023-03-20로 낮춰봤다. 잘 작동하더라. 하... 나이틀리를 7년 넘게 써왔는데 miscompilation 버그는 처음 밟았봤다. 아무튼 작동하니까 PR 합치고 rmp-serde PR은 닫았다. 그리고 여러 PR을 리뷰해서 합친 뒤 새 버전의 @swc/core과 새 버전의 플러그인들을 배포했다.

검증

한참을 기다린 뒤, 배포가 끝났다. 그해서 @swc/plugin-jest을 이용해서 작동하는 걸 확인했다. 그런 뒤 vercel/turbo에 보낸 PR 업데이트하고 Undraft 처리했다. 머지되면 next.js도 업데이트할 예정이다.

그리고 앞으로는 최대한 안정 버전의 rustc를 쓰게 될 것 같다.