Frontend

브라우저 Memory leak 디버깅 해보기

mechaniccoder 2024. 3. 10. 02:35

최근 서비스에서 메모리 누수가 발생하고 있는 것을 발견했습니다. 아무리 개발 환경이지만 JavaScript VM 힙 메모리에 1GB 가량 할당되어 있었죠. 그러다보니 시간이 지나면 앱이 너무 느려져 프로세스를 끄고 다시 실행하곤 했습니다.

 

크롬 메모리 패널

메모리 누수가 발생하는 곳을 찾기 위해선 먼저 메모리 패널을 어떻게 사용해야 하는지 알아야했습니다.

 

1. Heap snapshot

현재 JavaScript VM에 할당된 객체와 DOM에 대한 정보를 볼 수 있습니다.

 

2. Allocation instrumentation on timeline.

시간에 따라 메모리 할당과 해제되는 것을 보고 싶으면 이 기능을 사용하면 됩니다. 메모리 누수를 찾을 때 이 기능을 활용하면 쉽게 확인할 수 있습니다.

메모리 누수 지점 파악하기

먼저 어떤 객체들이 메모리 할당됐는지 보기 위해 Heap snapshot을 사용했습니다. 아래 이미지의 오른쪽 상단을 보면 shallow size와 retained size를 확인할 수 있습니다.

  • shallow size: 특정 생성자로부터 직접적으로 생성된 객체들의 메모리 사이즈입니다.
  • retained size: 생성된 객체들을 삭제했을때 해제되는 메모리 사이즈입니다. 해당 객체에서 여러 객체를 참조할 수 있고 이를 해제했을때 더 이상 도달할 수 없는 객체들은 GC에 의해 메모리 수거되기 때문입니다.

shallow size가 11%이고 retained size가 72%임을 봤을때 참조하는 객체가 많으며 이 객체를 삭제했을때 같이 해제되는 메모리도 크다라는 의미입니다.

이미지 하단의 박스는 이 객체를 어디서 참조하고 있는지를 보여줍니다. 예를 들어, Konva in Window는 Window 객체의 Konva 프로퍼티로 이 객체를 참조하고 있다는 의미입니다. 그 외에 여러 경로를 통해 이 객체를 참조하고 있어 도달 가능한 객체이므로 GC가 메모리를 수거하지 않을겁니다.

Memory panel heap snapshot

 

이 서비스에서 canvas library인 konva를 사용중이었고 영상의 자막을 렌더링 하기 위해 Text 생성자를 사용해 자막의 너비를 계산하는 로직을 발견했습니다. 확실히 원인을 파악하기 위해 Allocation instrumentation on timeline 기능을 활용해 해당 로직을 실행하며 시간에 따른 메모리 할당과 해제를 확인해봤습니다.

아래 이미지 상단의 박스를 확인해보면 파란색 막대가 보입니다. 메모리가 할당됐다는 의미입니다. GC에 의해 메모리가 수거되면 회색 막대로 변경됩니다. 시간이 지났음에도 파란색 막대가 남아있는걸 보니 메모리가 수거되지 않았고 어딘가에서 객체를 참조하고 있는 것입니다. 즉, 해당 로직이 실행될때마다 객체가 생성되고 참조를 들고 있어 메모리 누수가 발생하는 것을 파악했습니다.

Memory leak profiling

메모리 누수 해결

메모리 누수를 발생시키는 코드를 확인해보니 Text 생성자로 textNode를 생성한 뒤, 텍스트 너비를 구해서 반환하고 있습니다. 여기서 의아했던 부분은 함수의 호출이 끝난 뒤 인스턴스를 가르키는 textNode 변수가 정리되기 때문에 더 이상 도달하지 못해서 GC가 수거할 것 같은데 라는 생각이었습니다.

하지만 메모리 패널로 확인한 결과를 보면 전역(Window) 및 다른 객체에서도 참조를 들고 있다는 것을 다시 한번 생각했습니다.

export const measureTextWidth = () => {
  const textNode = new Konva.Text();
  
  const textWidth = //...

  return textWidth;
};

 

생성한 인스턴스는 필요한 결과를 다른 변수에 저장한 뒤 직접 삭제하여 메모리 누수를 해결했습니다.

export const measureTextWidth = () => {
  const textNode = new Konva.Text();
  
  const textWidth = //...
  
  textNode.destory() // 직접 인스턴스를 삭제

  return textWidth;
};

결과

위의 이미지에서 파란색 막대가 남아있던 것과는 다르게 사라진 것을 확인할 수 있습니다. 즉 메모리가 잘 수거됐다는 것이죠. 👍

Resolve memory leak profiling

마치며

메모리 프로파일링을 사용하며 메모리 누수의 원인을 파악하고 실제 코드에서 해결하는 과정을 겪어보니 재밌었습니다. 직감에 의존해서 그냥 여기인 것 같은데? 가 아니라 몇 가지 단서로부터 범인을 찾아나가는 마치 탐정이 된 것 같은 기분이었죠.

메모리 문제가 발생했을때 활용해야할 도구들과 어떤 과정을 거쳐서 원인을 파악해야할 지 알게 됐습니다. 다음 번에 문제를 마주했을 땐 더 빠르게 파악하고 해결할 수가 있을 것 같네요.