From 68b077cb09845c5f7a5f166890c472b0f43ffea2 Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Tue, 15 Sep 2026 12:30:29 +0900 Subject: [PATCH] docs: add contribution note for PR #65985 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #65985 에 대한 기여 노트 작성 --- contributions/65985.md | 156 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 contributions/65985.md diff --git a/contributions/65985.md b/contributions/65985.md new file mode 100644 index 00000000..43b807ed --- /dev/null +++ b/contributions/65985.md @@ -0,0 +1,156 @@ +--- +pr-url: https://github.com/nodejs/node/pull/65985 +--- + + + +## 문제 내용 + +`uvMetricsInfo`에서 매번 함수를 호출할 때마다 네이티브에서 배열을 생성하고 JS에서는 객체로 반환하는 작업을 하나의 버퍼를 공유하여 리소스를 생성하는 비용을 줄였습니다. + +`uvMetricsInfo`는 이벤트 루프에 대한 지표를 반환합니다. + +```js +const { performance } = require('node:perf_hooks'); + +console.log(performance.nodeTiming.uvMetricsInfo); +// { +// loopCount: ..., +// events: ..., +// eventsWaiting: ... +// } +``` + +- `loopCount`: event loop가 반복된 횟수 +- `events`: event handler가 처리한 이벤트 수 +- `eventsWaiting`: OS의 event provider를 호출했을 때 이미 처리를 기다리던 이벤트 수 + +해당 값은 네이티브에서 다음과 같이 처리됩니다. + +```cpp +void UvMetricsInfo(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + uv_metrics_t metrics; + CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0); + + Local data[] = { + Integer::New(isolate, metrics.loop_count), + Integer::New(isolate, metrics.events), + Integer::New(isolate, metrics.events_waiting), + }; + + Local arr = + Array::New(env->isolate(), data, arraysize(data)); + + args.GetReturnValue().Set(arr); +} +``` + +libuv는 내부적으로 event loop에 대한 정보를 누적하며 저장하고 있고 `uv_metrics_t`를 통해 함수를 호출한 순간 libuv에게 값을 가져와 새로운 배열로 만들어 JS 코드로 보내주고 있습니다. + +그리고 JS에서는 다음과 같이 값을 포장하여 반환합니다. + +```js +get: () => { + const metrics = uvMetricsInfo(); + + return { + loopCount: metrics[0], + events: metrics[1], + eventsWaiting: metrics[2], + }; +}, +``` + +`metrics`는 네이티브에서 반환된 배열이고, 배열의 각 인덱스로부터 `loopCount`, `events`, `eventsWaiting`을 뽑아 반환하는 형태입니다. + +이렇게 매 함수 호출마다 배열을 생성하고 객체를 생성하면 리소스에 대한 비용이 점차 커진다는 문제가 존재하였습니다. +- V8 객체 할당 비용 발생 +- 짧은 수명의 배열이 생기며 GC의 부담이 커짐 +- C++ -> JS 로 반환값을 매번 구성해야 함. + +## 해결 과정과 검증 + + + +`AliasedInt32Array`는 하나의 `V8 ArrayBuffer`를 C++ 포인터와 JS `Int32Array`가 함께 바라볼 수 있도록 만든 Node.js 내부 래퍼입니다. + +Node.js에서는 C++과 JS가 V8객체를 통해 서로 값을 주고 받을 수 있는데, V8을 통해 ArrayBuffer를 생성한다면 해당 버퍼 또한 네이티브와 JS에서 접근이 가능합니다. + +```cpp +// node_perf.cc +void UvMetricsInfo(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + uv_metrics_t metrics; + // uv_metrics_info always return 0 + CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0); + AliasedInt32Array& buffer = env->performance_state()->uv_metrics; + + buffer[0] = static_cast(metrics.loop_count); + buffer[1] = static_cast(metrics.events); + buffer[2] = static_cast(metrics.events_waiting); +} +``` + +libuv event loop를 `uv_metircs_info()`를 통해 `metrics`에 복사해오고 `metrics`에 저장된 값을 `AliasedInt32Array` 타입의 buffer 내부에 저장할 수 있습니다. 이제 `uv_metrics`를 JS에서 접근한다면, 따로 네이티브에서 배열을 반환하지 않아도 동일한 값을 접근할 수 있습니다. + +```cpp + target + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBuffer"), + state->uv_metrics.GetJSArray()) + .Check(); +``` + +`uvMetricsBuffer`라는 변수로 `uv_metrics`를 JS의 배열로 설정해주면, JS에서도 `InternalBinding`을 통해 접근이 가능합니다. + +```js +const { + uvMetricsInfo, + uvMetricsBuffer, +} = internalBinding('performance'); + +get: () => { + uvMetricsInfo(); + return { + loopCount: uvMetricsBuffer[0], + events: uvMetricsBuffer[1], + eventsWaiting: uvMetricsBuffer[2],2 + }; +}, +``` + +기존에는 `uvMetricsInfo()`를 통해 배열을 반환받고 내부적으로 인덱스를 통해 하나씩 값을 뽑아 객체로 반환하였습니다. 하지만 `AliasedInt32Array`를 통해 이미 네이티브 코드 내부적으로 값을 변경하였기 때문에, `uvMetricsInfo()`를 통해 네이티브 함수를 실행시키고, `uvMetricsBuffer` 값을 참조하면 갱신된 값에 접근이 가능합니다. + +이에 대해서 얼마나 개선하였는 지도 벤치마킹을 진행하였습니다. 기존 빌드를 before로 저장하고, 개선된 코드를 after로 빌드하여 벤치마크를 돌렸습니다. + +```js +./out/Release/node benchmark/compare.js \ + --old /tmp/node-before \ + --new /tmp/node-after \ + --filter nodetiming-uvmetricsinfo \ + --analyze perf_hooks +``` + +```console + confidence improvement accuracy (*) (**) (***) +perf_hooks/nodetiming-uvmetricsinfo.js events=1 n=1000000 *** +158.99 % ±12.43% ±16.73% ±22.15% +perf_hooks/nodetiming-uvmetricsinfo.js events=1000 n=1000000 *** +167.65 % ±6.95% ±9.32% ±12.29% +perf_hooks/nodetiming-uvmetricsinfo.js events=10000 n=1000000 *** +162.19 % ±7.61% ±10.22% ±13.51% + + -174.6% 0% +174.6% +perf_hooks/nodetiming-uvmetricsinfo.js events=1 n=1000000 |█████████████████░░ +158.99% *** +perf_hooks/nodetiming-uvmetricsinfo.js events=1000 n=1000000 |██████████████████░ +167.65% *** +perf_hooks/nodetiming-uvmetricsinfo.js events=10000 n=1000000 |██████████████████ +162.19% *** +``` + +대략 1.5배 정도의 성능이 향상된 것을 확인할 수 있었습니다. + + +## 기여 회고 + +`uvMetricsInfo()`를 제외하고도 비슷한 유형으로 자료구조를 사용하여 개선해볼 수 있는 점이 생각보다 많은 것 같습니다. 사랑하는 Node.js의 성능을 높일 수 있다는 점에서 뿌듯하였습니다. + +