이 명세는 클라이언트 JavaScript 실행 시간을 측정하기 위한 샘플링 프로파일러를 웹 애플리케이션이 제어할 수 있도록 하는 API를 설명한다.

소개

현재 복잡한 웹 애플리케이션은 클라이언트에서 JS 실행 시간이 어디에 소비되는지 파악할 수 있는 수단이 제한적이다. 스택 샘플을 효율적으로 수집할 수 없으므로, 애플리케이션은 부정확하고 실행 속도를 크게 저하시킬 수 있는 프로파일링 훅을 코드에 계측해야 한다. 샘플링 프로파일러를 조작하는 API를 제공함으로써 애플리케이션은 최소한의 오버헤드로 집계 및 분석을 위한 풍부한 실행 데이터를 수집할 수 있다.

예시

다음 예시는 사용자가 비용이 많이 드는 작업을 프로파일링하면서 10ms마다 JS 실행 샘플을 수집하는 방법을 보여 준다. 이상값을 디버깅하고 집계된 JS 실행 특성을 분석하기 위해 추적 데이터를 서버로 전송할 수 있다.

        const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });
        const start = performance.now();
        for (let i = 0; i < 1000000; i++) {
             doWork();
        }
        const duration = performance.now() - start;
        const trace = await profiler.stop();
        const traceJson = JSON.stringify({
          duration,
          trace,
        });
        sendTrace(traceJson);
        

또 다른 일반적인 실제 사용 사례는 페이지 로드 전반에서 JS를 프로파일링하는 것이다. 이 예시는 onload 이벤트를 프로파일링하고 추적 데이터와 함께 성능 타이밍 데이터를 전송한다.

        const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });

        window.addEventListener('load', async () => {
          const trace = await profiler.stop();
          const traceJson = JSON.stringify({
            timing: performance.timing,
            trace,
          });
          sendTrace(traceJson);
        });

        // 페이지의 나머지 JS 초기화 로직
        

정의

샘플은 주어진 시점에서의 순간적인 실행 상태를 설명하는 기술자이다. 각 샘플은 스택과 연결된다.

스택은 가장 바깥쪽 프레임부터 가장 안쪽 프레임까지 순차적으로 정렬되어야 하는 프레임 목록이다.

프레임은 현재 실행 상태에 관한 정보를 포함하는 스택 컨텍스트의 요소이다.

프로파일링 세션

프로파일링 세션샘플을 생성하는 추상적인 생성자이다. 각 세션에는 다음이 있다.

  1. {started, paused, stopped} 중 하나인 상태.
  2. 세션이 샘플을 획득하는 주기성으로 정의되는 샘플 간격.

    UA는 반드시 이 빈도로 샘플을 수집할 필요는 없다. 그러나 더 높은 품질의 추적 데이터를 생성하기 위해 이 빈도로 샘플을 수집하도록 샘플링에 우선순위를 부여하는 것이 좋다.

  3. 프로파일링할 에이전트.
  4. 프로파일링할 렐름.
  5. 샘플의 타임스탬프가 이를 기준으로 측정되는 시간 원점.
  6. 샘플 버퍼 크기 제한.
  7. 캡처한 샘플을 저장하는 ProfilerTrace.

동일한 페이지에서 여러 프로파일링 세션을 지원하는 것이 좋다.

상태

started 상태에서 UA는 샘플 간격이 경과할 때마다 샘플 수집 알고리즘을 [= in parallel =] 실행하여 샘플을 캡처하도록 최선의 노력을 기울이는 것이 좋다. pausedstopped 상태에서 UA는 샘플을 캡처하지 않는 것이 좋다.

프로파일링 세션은 반드시 started 상태에서 시작해야 한다.

UA는 세션을 started에서 paused로, 그리고 paused에서 started로 전환할 수 있다.

브라우징 컨텍스트가 포그라운드에 있지 않은 경우 사용자 에이전트는 프로파일링 세션의 샘플링을 일시 중지하는 것이 좋다.

stopped 세션은 started 또는 paused 상태로 전환해서는 안 된다.

처리 모델

프로파일링 세션이 주어졌을 때 샘플을 수집하려면 다음 단계를 수행한다.

  1. ProfilerTrace.samples의 길이가 프로파일링 세션과 연결된 샘플 버퍼 크기 제한보다 크거나 같으면, 연결된 Profilersamplebufferfull 유형의 새 이벤트를 발생시키고 상태를 stopped로 전환한 후 반환한다.
  2. sample을 새 ProfilerSample로 설정한다.
  3. sampleProfilerSample.timestamp 속성을 프로파일링 세션시간 원점을 기준으로 한 현재 고해상도 시간으로 설정한다.
  4. stack을 프로파일링 세션의 에이전트와 연결된 실행 컨텍스트 스택으로 설정한다.
  5. sampleProfilerSample.stackId 속성을 stack스택 ID 가져오기 알고리즘을 실행한 결과로 설정한다.
  6. sample을 세션의 ProfilerTrace와 연결된 ProfilerTrace.samples에 추가한다.

stack에 바인딩된 실행 컨텍스트 스택이 주어졌을 때 스택 ID를 가져오려면 다음 단계를 수행한다.

  1. stack이 비어 있으면 undefined를 반환한다.
  2. headstack의 최상위 요소로 설정하고, tail을 최상위 요소를 제거한 후 남은 stack으로 설정한다.
  3. parentIdtail스택 ID 가져오기를 재귀적으로 호출한 결과로 설정한다.
  4. frameIdhead프레임 ID 가져오기를 호출한 결과로 설정한다.
  5. frameIdundefined이면 parentId를 반환한다.
  6. profilerStackProfilerStack.frameIdframeId와 같고 ProfilerStack.parentIdparentId와 같은 새 ProfilerStack으로 설정한다.
  7. profilerStackProfilerTrace.stacks요소 ID 가져오기를 실행한 결과를 반환한다.

context에 바인딩된 실행 컨텍스트가 주어졌을 때 프레임 ID를 가져오려면 다음 단계를 수행한다.

  1. context와 연결된 [= realm =]이 프로파일링 세션과 연결된 렐름과 일치하지 않으면 undefined를 반환한다.
  2. instancecontext와 연결된 함수 인스턴스와 같게 설정한다.
  3. scriptOrModulecontext와 연결된 ScriptOrModule과 같게 설정한다.
  4. |attributedScriptOrModule : ScriptOrModule|을 다음 알고리즘을 실행한 결과와 같게 설정한다.
    1. |scriptOrModule|이 null이 아니면 |scriptOrModule|을 반환한다.
    2. |instance|가 내장 함수 객체이면 |instance|를 호출한 함수를 포함하는 ScriptOrModule을 반환한다.

      위 로직의 목적은 접근할 수 없는 스크립트가 호출한 내장 함수가 추적 데이터에 노출되지 않도록, 해당 함수의 귀속에 그 함수를 호출한 ScriptOrModule을 사용하는 것이다.

      “|instance|를 호출한 함수를 포함하는 ScriptOrModule”은 더 엄밀하게 정의해야 한다. 이를 제공하기 위해 스택에서 ScriptOrModule을 정의하는 최상위 실행 컨텍스트를 활용할 수 있지만, 이는 이상적이지 않다. 이론적으로 내장 함수가 실행 컨텍스트 스택에 대기열로 추가되는 다른 메커니즘이 존재할 수 있으며, 이 경우 귀속은 유효하지 않게 된다.

    3. 그렇지 않으면 null을 반환한다.
  5. |attributedScriptOrModule|이 null이면 undefined를 반환한다.
  6. |attributedScript : Script|를 |attributedScriptOrModule|.[[\HostDefined]]에서 얻은 [= script =]로 설정한다.
  7. |attributedScript|가 [= classic script =]이고 해당 오류 음소거 불리언이 true와 같으면 undefined를 반환한다.

    이 검사는 CORS 교차 출처 응답으로 제공된 교차 출처 스크립트의 스택 프레임을 포함하지 않도록 보장한다. 이 사용 사례를 더 잘 반영하도록 오류 음소거의 이름을 변경하는 것을 고려할 수 있다.

  8. frame을 새 ProfilerFrame으로 설정한다.
  9. frameProfilerFrame.name을 |instance|와 연결된 함수 인스턴스 이름으로 설정한다.
  10. |scriptOrModule|이 null이 아니면:
    1. scriptscriptOrModule.[[\HostDefined]]에서 얻은 스크립트로 설정한다.
    2. resourceStringscript기준 URL과 같게 설정한다.
    3. ProfilerFrame.resourceIdresourceStringProfilerTrace.resources요소 ID 가져오기를 실행한 결과로 설정한다.
    4. frameProfilerFrame.line을 |script|에서 instance가 정의된 줄의 1부터 시작하는 인덱스로 설정한다.
    5. frameProfilerFrame.column을 |script|에서 instance가 정의된 열의 1부터 시작하는 인덱스로 설정한다.
  11. frameProfilerTrace.frames요소 ID 가져오기를 실행한 결과를 반환한다.

list에 있는 item요소 ID를 가져오려면 다음 단계를 실행한다.

  1. listitem과 구성 요소별로 동일한 요소가 있으면 해당 인덱스를 반환한다.
  2. 그렇지 않으면 itemlist의 끝에 추가하고 해당 인덱스를 반환한다.

Profiler 인터페이스

      [Exposed=(Window, Worker)]
      interface Profiler : EventTarget {
        readonly attribute DOMHighResTimeStamp sampleInterval;
        readonly attribute boolean stopped;

        constructor(ProfilerInitOptions options);
        Promise<ProfilerTrace> stop();
      };
      

Profiler는 반드시 정확히 하나의 프로파일링 세션과 연결되어야 한다.

sampleInterval 속성은 연결된 프로파일링 세션샘플 간격DOMHighResTimeStamp로 표현하여 반영해야 한다.

stopped 속성은 프로파일링 세션의 상태가 stopped인 경우에만 true여야 한다.

new Profiler(options)

new Profiler(options)ProfilerInitOptions 유형의 객체 options가 주어졌을 때 다음 단계를 실행한다.
  1. options의 {{ProfilerInitOptions/sampleInterval}}이 0보다 작으면 RangeError를 발생시킨다.
  2. |globalObject|를 현재 전역 객체로 설정한다.
  3. |globalObject|가 Window 또는 WorkerGlobalScope이면:
    1. |globalObject|에서 "js-profiling-mode"에 대한 정책 값을 가져온다. 그 결과를 |profilingMode|로 설정한다.
    2. |profilingMode|가 "eager" 또는 "lazy"가 아니면:
      1. |globalObject|에서 "js-profiling"에 대한 정책 값을 가져온다. 그 결과를 |jsProfilingEnabled|로 설정한다.
      2. |jsProfilingEnabled|가 false이면 "NotAllowedError" DOMException을 발생시킨다.
  4. 그렇지 않으면 "NotAllowedError" DOMException을 발생시킨다.
  5. 다음과 같은 새 프로파일링 세션을 생성한다.
    1. 연결된 샘플 간격ProfilerInitOptions.sampleInterval 또는 UA가 지원하는 그다음으로 낮은 간격으로 설정한다.
    2. 연결된 시간 원점을 |globalObject|의 시간 원점과 같게 설정한다.
    3. 연결된 샘플 버퍼 크기 제한을 {{ProfilerInitOptions/maxBufferSize}}로 설정한다.
    4. 연결된 [= agent =]를 주변 에이전트로 설정한다.
    5. 연결된 [= realm =]을 현재 렐름 레코드로 설정한다.
    6. 연결된 ProfilerTrace«[{{ProfilerTrace/resources}} → «», {{ProfilerTrace/frames}} → «», {{ProfilerTrace/stacks}} → «», {{ProfilerTrace/samples}} → «»]»로 설정한다.
  6. 새로 생성한 프로파일링 세션과 연결된 새 Profiler를 반환한다.

stop() 메서드

프로파일러를 중지하고 추적 데이터를 반환한다. 이 메서드는 반드시 다음 단계를 실행해야 한다.

  1. 연결된 [= profiling session =]의 상태가 stopped이면, "InvalidStateError" DOMException으로 [= a promise rejected with =]를 반환한다.
  2. [= profiling session =]의 상태를 stopped로 설정한다.
  3. |p:Promise|를 [= a new promise =]로 설정한다.
  4. 다음 단계를 [= in parallel =] 실행한다.
    1. [= profiling session =]을 중지하기 위해 [= implementation-defined =] 작업을 수행한다.
    2. 프로파일러의 [= profiling session =]과 연결된 {{ProfilerTrace}}로 |p|를 이행한다.
  5. |p|를 반환한다.

stop()을 호출한 후 수집된 모든 샘플프로파일링 세션에 포함하지 않는 것이 좋다.

ProfilerTrace 딕셔너리

      typedef DOMString ProfilerResource;

      dictionary ProfilerTrace {
        required sequence<ProfilerResource> resources;
        required sequence<ProfilerFrame> frames;
        required sequence<ProfilerStack> stacks;
        required sequence<ProfilerSample> samples;
      };
      

resources 속성은 샘플 수집 알고리즘이 설정한 ProfilerResource 목록을 반환해야 한다.

frames 속성은 샘플 수집 알고리즘이 설정한 ProfilerFrame 목록을 반환해야 한다.

stacks 속성은 샘플 수집 알고리즘이 설정한 ProfilerStack 목록을 반환해야 한다.

samples 속성은 샘플 수집 알고리즘이 설정한 ProfilerSample 목록을 반환해야 한다.

이 표현은 V8 추적 이벤트 형식Gecko 프로파일 형식에서 영감을 얻었으며, 쉽고 효율적으로 직렬화할 수 있도록 설계되었다.

ProfilerSample 딕셔너리

        dictionary ProfilerSample {
          required DOMHighResTimeStamp timestamp;
          unsigned long long stackId;
        };
        

timestamp는 초기화된 값을 반환해야 한다.

stackId는 초기화된 값을 반환해야 한다.

ProfilerStack 딕셔너리

        dictionary ProfilerStack {
          unsigned long long parentId;
          required unsigned long long frameId;
        };
        

parentId는 초기화된 값을 반환해야 한다.

frameId는 초기화된 값을 반환해야 한다.

ProfilerFrame 딕셔너리

        dictionary ProfilerFrame {
          required DOMString name;
          unsigned long long resourceId;
          unsigned long long line;
          unsigned long long column;
        };
        

name은 초기화된 값을 반환해야 한다.

resourceId는 초기화된 값을 반환해야 한다.

line은 초기화된 값을 반환해야 한다.

column은 초기화된 값을 반환해야 한다.

ProfilerInitOptions 딕셔너리

      dictionary ProfilerInitOptions {
        required DOMHighResTimeStamp sampleInterval;
        required unsigned long maxBufferSize;
      };
      

ProfilerInitOptions는 다음 필드를 지원해야 한다.

문서 정책

이 명세는 프로파일링 기능과 초기화 동작을 제어하기 위해 문서 정책구성 지점을 정의한다.

js-profiling-mode

이 명세는 이름이 js-profiling-mode구성 지점을 정의한다. 해당 유형enum이며 허용되는 값은 "eager""lazy"이다. 기본 값은 빈 문자열이다.

이 정책이 설정되면 스크립트가 JS 자체 프로파일링 API를 사용하도록 허용한다.

eager

js-profiling-mode"eager"로 설정되면 페이지 로드 중에 프로파일링이 예상된다는 것을 UA에 알린다.

UA는 이 힌트를 사용하여 프로파일링 인프라를 조기에 초기화하고, 문서 로드 중 가능한 한 이른 시점에 필요한 메타데이터를 저장하고 프로파일링 구성 요소를 준비할 수 있다. 그러나 프로파일링을 실제로 사용하지 않더라도 무시할 수 없는 성능 오버헤드가 발생할 수 있다. 이 오버헤드는 최초 콘텐츠풀 페인트(FCP) 및 최대 콘텐츠풀 페인트(LCP)와 같은 지표에 부정적인 영향을 줄 수 있다.

lazy

js-profiling-mode"lazy"로 설정되면 프로파일링이 조건부로 사용될 것임을 UA에 알린다.

UA는 이 힌트를 사용하여 첫 번째 Profiler가 인스턴스화될 때까지 프로파일링 관련 초기화 오버헤드를 지연함으로써, 프로파일링을 실제로 사용하지 않을 때 중요한 렌더링 기간의 성능 비용을 방지할 수 있다. 그러나 사용자 상호작용을 처리하는 동안 초기화가 발생하면 다음 페인트까지의 상호작용(INP)에 부정적인 영향을 줄 수 있다. 이 모드는 샘플링 결정에 따라 프로파일링을 조건부로 활성화하는 문서에 특히 적합하다.

js-profiling(사용 중단됨)

이 명세는 이름이 js-profiling구성 지점도 정의한다. 해당 유형boolean이며 기본 값false이다.

이 정책이 활성화되면 스크립트가 JS 자체 프로파일링 API를 사용하도록 허용하고, UA에 프로파일링 인프라를 조기에 초기화하도록 알린다. 이는 의미론적으로 js-profiling-mode=eager와 동일하다.

js-profiling 불리언 구성 지점은 js-profiling-mode를 위해 사용 중단되었다. 둘 다 지정된 경우 js-profiling-mode가 우선하며 js-profiling은 반드시 무시해야 한다. 구현은 이전 버전과의 호환성을 위해 js-profiling을 지원하는 것이 좋지만 향후 지원을 제거할 수 있다.

자동화

사용자 에이전트 자동화 및 애플리케이션 테스트를 위해 이 문서는 다음 [[WebDriver]] 확장 명령을 정의한다.

강제 샘플 수집

HTTP 메서드 URI 템플릿
POST `/session/{session id}/forcesample`

강제 샘플 수집 확장 명령은 더 결정적인 테스트를 가능하게 하기 위해 모든 [=프로파일링 세션=]이 [=샘플을 수집=]하도록 강제한다.

원격 끝 단계는 다음과 같다.

  1. |sessions:list|를 현재 브라우징 컨텍스트에서 생성된 모든 [=프로파일링 세션=]의 [=목록=]으로 설정한다.
  2. |sessions|의 각 |session:profiling session|에 대해:
    1. |session|의 [=상태=]가 started이면 |session|으로 [=샘플을 수집=]한다.
  3. 데이터 null과 함께 성공을 반환한다.

개인정보 보호 및 보안

다음 절에서는 API의 개인정보 보호 및 보안 관련 선택 사항을 자세히 설명하고, 다양한 유형의 공격에 대한 보호 전략을 보여 준다.

교차 출처 스크립트 콘텐츠

API는 샘플 수집 알고리즘을 통해 포함되는 모든 함수가 오류 음소거 속성을 이용하여 CORS 동일 출처로 제공된 스크립트에 정의되도록 요구함으로써 교차 출처 스크립트의 콘텐츠가 노출되는 것을 방지한다. 브라우저 내장 기능(예: performance.now())도 [= CORS-same-origin =] 스크립트에서 호출된 경우에만 포함해야 한다.

따라서 API는 수동 계측으로 이미 확인할 수 있는 것 이상으로 교차 출처 스크립트의 콘텐츠나 실행 특성에 관한 새로운 정보를 노출하지 않는다. UA가 매우 낮은 샘플 간격 값 (예: 1밀리초 미만)을 지원하기로 선택한 경우에도 이 특성이 유지되는지 확인하는 것이 권장된다.

교차 출처 실행

교차 출처 실행 컨텍스트는 샘플 수집 알고리즘의 렐름 검사를 통해 API에서 관찰할 수 없어야 한다. 따라서 프로파일러와 에이전트를 공유하는 교차 출처 iframe 및 기타 실행 컨텍스트의 실행은 이 API를 통해 관찰할 수 없다.

타이밍 공격

새로운 고해상도 타이밍 정보 소스를 도입할 수 있는 모든 API에서 타이밍 공격은 여전히 우려 사항이다. 추적 데이터에서 수집되는 타임스탬프는 새로운 부채널 공격 벡터가 노출되지 않도록 [[?HR-Time]]의 현재 고해상도 시간과 동일한 소스에서 얻어야 한다.

[[?HR-Time]]의 시계 해상도에 관한 논의를 참조한다.