structuredClone은 객체를 deep copy 하는 API 이다. 만약 structuredClone 가 Node.js의 JavaScript 영역에 구현되어있다면 Native 영역으로 구현부를 이동시킬때 유의미한 성능향상이 있을 것이다. src: implement structuredClone 은 그런면에서 흥미로운 내용일 것이라 생각하고 리뷰해보았다.
기존 구현
Web Platform API인 structuredClone은 원래 global 영역에 노출되어있지 않은 API였다. Deep Copy를 JavaScript 에서 제공하지 않지만, Web Spec. 인 MessageChannel 이나 IndexedDB 같은 기능을 위해선 Object의 직렬화/역직렬화 기능은 필요할 수 밖에 없었다. 예를들어 Web Worker에서 한 Thread에서 다른 Thread로 전달하는 것은 서로 다른 JavaScript Context로 데이터를 넘기는 일이기 때문에 Deep Copy 기능이 필요하다.
Node.js 에는 structuredClone API 앞서 MessageChannel이 구현되어 있었는데 이를 활용한 hack 을 이용하면 structuredClone의 결과와 같은 Deep Copy를 구현할 수 있었다. 그래서 기존에는 아래와 같은 방식으로 구현되었었다.
13 | function structuredClone(value, options = undefined) { |
아이디어는 MessageChannel의 인스턴스를 생성하고 해당 channel로 데이터를 전달함으로서 그 과정에서 수행되는 structuredCloned 을 이용하는 것이다.
MessageChannel 내부
MessageChannel::postMessage(value[,{ transfer }])
내부 구현에서
structuredClone의 구현은 아래와 같은 두개의 Step을 통해 구현된다.
- Step 1. clone 하거나 transfer 해야할 객체를 리스트업 한다.
- Step 2. 직렬화/역직렬화 과정을 수행한다.
기존 구현의 TODO로 기술되어있듯이 원 저자 역시 structuredClone을 위해
MessageChannel을 사용하는 것이 그렇게 효율적인 방법은 아니라는 것을 기술해
두었다. PR Author는 MessageChannel::postMessage()
에서 상기 Step 1 동작을
수행하는 부분을 직접 JavaScript 영역에서 호출하면 MessageChannel
을 이용하지
않을 수 있을 것이라 생각했다. 현재 코드 상에서 큰 리팩토링을 우려했지만
생각보다는 적은 수고로 가능한 내용이었다.
- As-Is: structuredClone (JS)
- ...
(1)
newMessageChannel
MessageChannel::postMessage
(C++)- ...
(2)
MessageChannel 구현 - Step 1
- Step 2
- ...
(3)
- ...
- ...
위는 기존 구현부를 단순화한 내용이다. PR의 아이디어는 MessageChannel
와 관련된 위의
(1), (2), (3) 해당하는 부분을 제거한다. Step 1에 해당하는 부분을 MessageChannel
와
공유하고 Step 2 구현을 구현하여 structuredClone의 구현체를 단순화 시키는 내용이다.
Step 1
transfer를 해야할 객체의 리스트업을 다음 함수 GetTransferList로 분리한 뒤 새로
native side에서 구현한 structuredClone과 기존
MessageChannel::postMessage()
에서 공동으로 사용하도록 한다.
1 | bool GetTransferList(Environment* env, |
Step 2
이후 직렬화/역직렬화를 수행해 clone을 수행한다.
- To-Be: structuredClone (JS)
internalBinding('messaging').structuredClone
(C++)- Step 1
- Step 2
1 | Local<Value> value = args[0]; |
Step 3
직렬화/역질화 의 내부 알고리즘은 V8 내부에서 수행하며, src/node_messaging.cc
에는 Node.js에서 정의한 객체에 대한 clonning을 어떻게 할지가 정의되어 있다.
성능의 향상?
이 PR의 제목만 보면 JavaScript side에서 수행하는 Cloning을 native side로
이동시키면서 상당한 성능의 향상이 있을 것이란 생각이 들었다. 그러나 실제로는
MessageChannel
의 instance 를 생성하는 부하만 줄이는 것이며 실제로 Cloning되는
메카니즘은 그대로이다. 그래서 structuredClone 자체의 성능은 더 개선할 내용이 있는 것이다.
GetTransferList
안은 ReadIterable
이 구현되어있으며 transfer 목록을 순회하기
위해 iterator를 호출한다. 이 과정에서 JavaScript Side와 Native Side간 Context
Switching 이 일어나 속도가 저하된다. 이 부분은 여전히 TODO로 남겨졌다.
1 | static Maybe<bool> ReadIterable(Environment* env, |
결과
structuredClone을 위해 MessageChannel instance가 생성되는 시간을 줄이고 기존에 JavaScript side에서 구현되어 snapshot에 포함되지 못했던 structuredClone 기능을 snapshot에 포함할 수 있었던 것이 해당 PR의 의미라고 할 수 있다.
1 | // snapshot 생성시 포함되도록 등록 |
한가지 의문은 Web Spec.에 의하면 trasfer 는 An array of transferable objects
that will be moved rather than cloned to the returned object. 이라고 정의
되어있는데 왜 iterable로서 순회해야하는지는 의문이다. Array 객체로서 순회하는
것은 V8 API로 존재하고 있기 때문에 iteratable interface에 따라 next()
호출을
해주지 않아도 구현이 가능할 것 같은데 말이다. 아무튼, iterable로 처리되야한다면
상기 언급했던 Step 1를 JavaScript 쪽에서 목록을 만들고 C++ 로 넘길 수 있으면 TODO로
남겨진 성능 저하를 개선 할 수 있을 것이라 생각된다.