Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions contributions/65985.md
Original file line number Diff line number Diff line change
@@ -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<Value>& 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<Value> data[] = {
Integer::New(isolate, metrics.loop_count),
Integer::New(isolate, metrics.events),
Integer::New(isolate, metrics.events_waiting),
};

Local<Array> 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<Value>& 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<int32_t>(metrics.loop_count);
buffer[1] = static_cast<int32_t>(metrics.events);
buffer[2] = static_cast<int32_t>(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의 성능을 높일 수 있다는 점에서 뿌듯하였습니다.