img.png

performance.measureMemory()로 웹 페이지 전체 메모리 사용량 모니터링하기


원문: https://web.dev/monitor-total-page-memory-usage/

브라우저는 웹 페이지의 메모리를 자동으로 관리한다. 웹 페이지가 객체(object)를 만들 때마다, 브라우저는 객체를 저장하기 위해 "내부적으로" 메모리 청크(chunk)를 할당한다. 메모리는 한정된 자원이기 때문에, 브라우저는 가비지 컬랙션으로 더는 객체가 필요하지 않은 시점을 감지하고 메모리 청크를 해제한다. 그러나 이런 탐지는 완벽하지 않고, 완벽한 탐지라는 것은 불가능하다고 증명되었다. 따라서 브라우저에서 "객체가 필요하다" 개념은 "객체에 접근할 수 있다" 라는 개념과 비슷한 의미를 갖는다. 웹 페이지가 변수나 필드로 객체에 도달 가능할 수 없으면, 브라우저는 안전하게 객체를 회수 할 수 있게 된다. 두 개념의 차이는 다음 예에서 알 수 있듯이 메모리 누수로 이어진다.

const object = { a: new Array(1000), b: new Array(2000) };
setInterval(() => console.log(object.a), 1000);

여기 더 큰 배열 b는 더는 필요하지 않지만, 콜백에서 여전히 object.b에 접근할 수 있기 때문에 브라우저는 회수하지 않는다. 따라서, b 배열의 메모리가 누수된다.

웹에서 메모리 누수는 흔한 일이다. 이벤트 리스너 해제를 잊어버리거나, iframe의 객체를 캡처하거나, 워커를 닫지 않거나, 배열로 객체를 축적하는 등 여러가지 이유들로 쉽게 발생할 수 있다. 웹 페이지 메모리가 누수가 발생한다면, 메모리 사용량은 시간이 지남에 따라 증가하고 사용자는 느려지고 비대해진 웹 페이지를 만나게 된다.

Performance.measureMemory()는 기존 performance.memory API와 어떻게 다른가?

기존의 비표준 API였던 performance.memory가 친숙하다면, 새로운 API와 어떻게 다른지 궁금할 것이다. 주된 차이는 기존 API가 JavaScript 힙의 크기를 반환하는 반면에, 새로운 API는 전체 웹 페이지의 메모리 사용량을 추정한다는 것이다. 이 차이는 크롬이 여러 페이지(또는 한 페이지의 여러 인스턴스)에서 같은 힙을 공유할 때 중요해진다. 이 경우에, 이전 API의 결과는 임의로 해제 될 수 있다. 또한, 이전 API는 "힙"과 같은 구현용 용어로 정의되어 있기 때문에 표준화 또한 힘들다.

또 다른 점은 새로운 API는 가비지 컬렉션을 하는 도중에 메모리를 측정한다는 것이다. 결과에서 노이즈가 줄어들지만 시간이 좀 더 걸릴 수 있다. 다른 브라우저들은 가비지 컬렉션에 의존하지 않고 새로운 API를 구현할 수 있다는 부분에 유의하라.

권장 사용 사례

웹페이지의 메모리 사용량은 이벤트 타이밍, 사용자 작업, 가비지 컬렉션에 따라 달라진다. 그렇기 때문에 메모리 측정 API는 프로덕션에서 메모리 사용량 데이터를 집계해야 한다. 개별 호출의 결과는 덜 유용하다. 예시를 한번 살펴보자.

  • 새 버전의 웹 페이지를 출시하는 동안 새로운 메모리 누수를 포착하기 위한 회귀 탐지
  • 새로운 기능의 메모리 영향도를 평가하고 메모리 누수를 감지하는 A/B 테스트
  • 세션 지속 시간에 따른 메모리 누수 여부 확인
  • 사용자 메트릭(metric)과 메모리 사용량의 상관관계를 분석, 사용자의 전반적인 메모리 사용량 이해

브라우저 호환성

현재는 Chrome 83에서 시험 단계로 지원하고 있다. API의 결과는 브라우저가 메모리에서 객체를 나타내는 방식과 메모리 사용량을 추정하는 방식이 다르기 때문에 구현 의존도가 높다. 브라우저에서 계산의 비용이 너무 많이 들거나, 실행할 수 없는 경우 메모리 계산이 제외되어 있다. 따라서, 브라우저 간 결과를 비교할 수 없다. 같은 브라우저의 결과를 비교하는 것만 의미 있다.

현재 상태

단계 상태
1. 설명자 작성 완료
2. 스펙 초안 작성 진행 중
3. 피드백 수집 및 설계 진행 중
4. 시험 진행 중
5. 출시 대기

performance.measureMemory() 사용하기

시험 기능 활성화 하기

performance.measureMemory() API는 현재 Chrome 83에서 시험 기능으로 사용해 볼 수 있다. Chrome 84에서 테스트가 종료될 것으로 예상된다.

Origin trial을 통해 새로운 기능을 시도하고 웹 표준 커뮤니티에 유용성, 실용성 및 효율성에 대한 피드백을 제공할 수 있다. 자세한 내용은 웹 개발자를 위한 Origin trial 가이드를 참고하라. 이 기능 또는 다른 시험 기능을 사용하려면 등록 페이지를 방문하라.

등록하기

  1. 토큰을 요청하라
  2. 페이지에 토큰을 등록하라. 등록에는 아래 두 가지 방법이 있다. _ 각 페이지의 head에 origin-trial <meta> 태그를 등록하라.
    <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE"> 같은 형태를 띨 것이다. _ 서버를 구성할 수 있는 경우, Origin-Trial HTTP 헤더를 통해 토큰을 등록할 수 있다. Origin-Trial: TOKEN_GOES_HERE 같은 형태를 띨 것이다.

chrome://flags 에서 활성화 하기

performance.measureMemory()를 토큰 없이 실험해보려면, chrome://flags에서 #experimental-web-platform-features를 활성화 하라.

기능 탐지

실행 환경이 교차 출처(cross-origin)간 정보 유출을 방지하기 위한 보안 요구사항을 충족하지 않으면 performance.measureMemory()SecurityError와 함께 실패할 수 있다. Chrome의 시험 단계 동안에는 사이트 격리(Site isolation)가 사용 설정되어있어야 한다. 출시 이후에는 API는 교차 출처 격리에 의존한다. 웹 페이지는 COOP+COEP 헤더를 설정함으로써 교차 출처 격리를 옵트인 할 수 있다.

if (performance.measureMemory) {
  let result;
  try {
    result = await performance.measureMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === "SecurityError") {
      console.log("The context is not secure.");
    } else {
      throw error;
    }
  }
  console.log(result);
}

로컬 테스트

크롬은 가비지 콜렉션 시 메모리를 측정한다. 이는 API가 결과를 즉시 해결하는 것 대신 다음 가비지 컬렉션을 기다리는 것을 의미한다. API는 타임아웃 후 강제로 가비지 컬렉션을 수행하며, 현재 시간은 20초로 설정되어 있다. --enable-blink-features='ForceEagerMeasureMemory' 명령어와 함께 크롬을 시작하면, 타임아웃 시간은 0으로 줄어들고 로컬 테스트나 디버깅을 유용하게 할 수 있게 된다.

예시

API의 권장 사용법은 전역 메모리 모니터를 정의하는 것이다. 전역 메모리 모니터는 전체 웹 페이지의 메모리 사용량을 샘플링하고, 결과를 집계하고 분석하기 위해 서버로 전송한다. 가장 쉬운 방법은 매 M분마다 주기적으로 샘플링을 하는 것이다. 그러나 샘플마다 메모리가 최대로 사용될 수 있어 편향이 생길 수 있다. 다음 예시는 포아송 과정(Poisson process)를 사용해 편향되지 않는 메모리 측정 수행 방법을 보여준다. 이는 샘플이 어느 시점에서도 동일하게 발생할 가능성이 있음을 보장한다.(데모, 소스)

먼저 setTimeout()을 사용해 무작위 간격으로 다음 메모리 측정을 예약하는 함수를 정의한다. 이 기능은 main window가 로드된 뒤 호출되어야 한다.

function scheduleMeasurement() {
  if (!performance.measureMemory) {
    console.log("performance.measureMemory() is not available.");
    return;
  }
  const interval = measurementInterval();
  console.log(
    "Scheduling memory measurement in " +
      Math.round(interval / 1000) +
      " seconds."
  );
  setTimeout(performMeasurement, interval);
}

// main window가 로드된 뒤 측정을 시작한다.
window.onload = function() {
  scheduleMeasurement();
};

measurementInterval() 함수는 평균적으로 5분마다 한 번의 측정이 있을 정도로 간격을 밀리초 단위로 계산한다. 함수에 작성된 수식의 원리가 궁금한 경우 지수 분포를 참고하라.

function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}

마지막으로 performMeasurement() 함수는 비동기로 API를 호출하고, 결과를 기록한 뒤, 다음 측정을 예약한다.

async function performMeasurement() {
  // 1. performance.measureMemory() 호출
  let result;
  try {
    result = await performance.measureMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === "SecurityError") {
      console.log("The context is not secure.");
      return;
    }
    // 다른 에러 처리
    throw error;
  }
  // 2. 결과 기록
  console.log("Memory usage:", result);
  // 3. 다음 측정 예약
  scheduleMeasurement();
}

결과는 아래와 같다.

{
  bytes: 60_000_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: ["https://foo.com"],
      userAgentSpecificTypes: ["Window", "JS"]
    },
    {
      bytes: 20_000_000,
      attribution: ["https://foo.com/iframe"],
      userAgentSpecificTypes: ["Window", "JS"]
    }
  ]
}

총 메모리 사용 추정치는 bytes 필드에 반환된다. 바이트 값은 Numeric separator 구문으로 작성된다. 이 값은 구현에 따라 크게 다르며 브라우저 간 비교는 불가능하다. 동일한 브라우저의 다른 버전 간에도 다를 수 있다. 시험 기간 동안은 main window와 관련 window 및 iframe의 JavaScript 메모리 사용량이 포함된다. 정식 출시 이후에는 JavaScript와 iframe의 DOM 메모리, 관련된 윈도우들, 웹 워커 등 모두를 고려해 계산될 것이다.

breakdown 목록은 사용된 메모리에 대한 추가 정보를 제공한다. 각 항목은 메모리의 각 부분을 설명하고, window나 iframe 그리고 워크들을 URL로 구분한다. userAgentSpecificTypes는 메모리와 연관된 구현별 메모리 유형을 나열한다.

모든 목록을 일반적인 방법으로 처리하고, 특정 브라우저를 근거한 가정들로 하드코딩하지 않는 것이 중요하다. 예를 들어 일부 브라우저는 비어있는 breakdown이나 비어있는 attribution을 반환할 수 있다. 다른 브라우저는 attribution으로 여러 URL을 반환해 어떤 URL이 메모리를 소유하는지 구별 못 할 수 있다.

피드백

Web Performance Community Group과 크롬팀은 performance.measureMemory()에 대한 당신의 생각과 경험에 대해 듣고싶다.

API 설계에 대한 이슈

API가 예상대로 작동하지 않거나 구현하는데 필요한 속성을 놓친 부분이 있다면 performance.measureMemory 깃헙에 스펙 이슈를 제기하거나 기존 이슈에 의견을 남길 수 있다.

구현에 관한 문제 보고

크롬 구현에 대한 버그를 찾았는가? 아니면 스펙과 구현이 다른가? new.crbug.com에 버그를 제출하라. 가능한 한 자세히 내용을 작성하고, 버그를 재현하기 위한 간단한 방법을 공유하라. 그리고 ComponentsBlink>PerformanceAPIs로 설정하라. Glitch는 빠르고 쉽게 재현해 공유하는데 효과적이다.

프로젝트 지원 여부

performance.measureMemory()를 사용할 계획이 있는가? 크롬 팀에 당신의 지원 여부를 알려주는것은 우선순위를 정하고 다른 브라우저 벤더사가 이 기능을 지원하는 게 얼마나 중요한 것인지를 보여준다. @ChromiumDev에 트윗으로 어디서 어떻게 사용하고 있는지 알려달라.

Hero image by Harrison Broadbent on Unsplash

도움이 되는 링크