Wasm 관련 삽질기
이슈
특정한 경우에 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
로, rkyv
를 v0.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-serde
의 to_vec
을 사용했는데 안 됐다.
에러 메시지도 그닥 도움되는 종류는 아니어서 쓸데없는 짓을 한건가 싶은 마응으로 serde_json
을 시도해봤다.
작동헀다.
희망을 봤다고 해야하나?
bincode
도 시도해봤다.
그런데 bincode
는 serde::Deserializer::deserialize_any
를 구현하지 않기 때문에 사용할 수 없었다.
그래서 고민했다.
serde_json
은 너무 느리고, 빠른 포맷은 대부분 deserialize_any
를 지원하지 않을 것이었다.
근데 생각해보니까 이상했다.
msgpack
포맷에 대해 잘 아는 건 아니지만, rmp-serde
가 deserialize_any
를 구현하는 건 확실했다.
그리고 위에서 말한 도움이 되지 않는 에러 메시지는 직렬화된 데이터를 전혀 다른 필드에 갖다 꽂으면 나올 수 있는 메시지였다.
포맷 내부에 필드 이름 같은 걸 인코딩하는 방법이 있을 거라는 결론을 내리고 문서를 보니까 rmp-serde
에 to_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를 쓰게 될 것 같다.