1. 소개
performance.measureUserAgentSpecificMemory()
API를 정의하며, 이는 웹 애플리케이션의 모든 iframe과 worker를 포함하여 메모리 사용량을 추산합니다.
새 API는 운영 환경에서 메모리 사용 데이터를 집계하는 데 사용하기 위해 설계되었습니다. 주요 사용 사례는 다음과 같습니다:
-
웹 애플리케이션의 새 버전을 배포할 때 새로운 메모리 누수를 탐지하기 위한 회귀 검출.
-
새 기능의 메모리 영향을 평가하기 위한 A/B 테스트.
-
메모리 사용량과 사용자 지표를 연관지어 메모리 사용량이 전반에 미치는 영향을 이해.
1.1. 예시
performance.measureUserAgentSpecificMemory()
호출은 Promise
를 반환하며, 페이지에서 할당된 메모리를 비동기적으로 측정합니다.
async function run() { const result= await performance. measureUserAgentSpecificMemory(); console. log( result); } run();
iframe과 worker가 없는 단순한 페이지의 결과는 다음과 같을 수 있습니다:
여기서 모든 메모리는 메인 페이지에 귀속됩니다.{ bytes: 1000000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
bytes: 0인 항목은 결과 처리를 특정 항목에 하드코딩하지 않고 일반적으로 처리하도록 유도하기 위해 breakdown 리스트에 존재합니다.
리스트가 비어있지 않으면 해당 항목이 임의의 위치에 삽입됩니다.
다른 가능한 유효 결과:
이 경우 구현체가 전체 메모리 사용량만 제공합니다.{ bytes: 1000000 , breakdown: [], }
이 예시에서는 구현체가 메모리 타입별로 분류하지 않았습니다.{ bytes: 1000000 , breakdown: [ { bytes: 0 , attribution: [], types: [], }, { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [], }, ], }
동일 출처 iframe을 포함하는 페이지의 경우, 결과는 해당 iframe에 일부 메모리를 귀속시키며 iframe을 식별할 수 있는 진단 정보를 제공합니다:
< html > < body > < iframe id = "example-id" src = "redirect.html?target=iframe.html" ></ iframe > </ body > </ html >
{ bytes: 1500000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 500000 , attribution: [ { url: "https://example.com/iframe.html" container: { id: "example-id" , src: "redirect.html?target=iframe.html" , }, scope: "Window" , } ], types: [ "JS" , "DOM" ], }, ], }
url 필드와 container.src 필드가 iframe에 대해 서로 다름을 주목하세요.
전자는 iframe의 현재 location.href를 반영하고, 후자는 iframe 요소의 src 속성 값을 나타냅니다.
iframe의 메모리를 페이지 메모리와 의미 있게 분리하는 것이 항상 가능하지는 않습니다. 구현체는 일부 또는 전체 iframe과 페이지 메모리를 합쳐서 표시할 수 있습니다:
{ bytes: 1500000 , breakdown: [ { bytes: 1500000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, { url: "https://example.com/iframe.html" , container: { id: "example-id" , src: "redirect.html?target=iframe.html" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], };
구현체가 워커와 페이지 메모리를 합쳐서 제공할 수 있습니다. 워커가 iframe에 의해 생성된 경우, 워커의 귀속 항목에 해당 iframe 요소에 대응하는{ bytes: 1800000 , breakdown: [ { bytes: 0 , attribution: [], types: [], }, { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 800000 , attribution: [ { url: "https://example.com/worker.js" , scope: "DedicatedWorkerGlobalScope" , }, ], types: [ "JS" ], }, ], };
container 필드가 있습니다.
공유 워커와 서비스 워커의 메모리는 결과에 포함되지 않습니다.
다음 구조를 가진 페이지를 예로 들 수 있습니다:
example교차 출처 iframe이 다른 iframe을 포함하고, 워커도 생성합니다. 이러한 리소스의 모든 메모리는 첫 번째 iframe에 귀속됩니다.. com( 1000000 bytes) | *-- foo. com/ iframe1( 500000 bytes) | *-- foo. com/ iframe2( 200000 bytes) | *-- bar. com/ iframe2( 300000 bytes) | *-- foo. com/ worker. js( 400000 bytes)
< html > < body > < iframe id = "example-id" src = "https://foo.com/iframe1" ></ iframe > </ body > </ html >
교차 출처 iframe 항목의{ bytes: 2400000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 1400000 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "DOM" , "JS" ], }, ], }
url과 scope 필드는 정보가 없음을 나타내는 특수 값을 가집니다.
구현체가 교차 출처 iframe을 별도의 주소 공간에서 로드하면, 해당 iframe의 메모리 사용량은 측정되지 않습니다.
이런 iframe의 breakdown 항목은 bytes: 0을 가지며, 메모리가 집계되지 않았음을 나타냅니다:
{ bytes: 100000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
location.href를 찾아 읽을 수 있으므로 정보 유출이 발생하지 않습니다.
example. com( 1000000 bytes) | *-- foo. com/ iframe1( 500000 bytes) | *-- example. com/ iframe2( 200000 bytes)
< html > < body > < iframe id = "example-id" src = "https://foo.com/iframe1" ></ iframe > </ body > </ html >
{ bytes: 1700000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 500000 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 200000 , attribution: [ { url: "https://example.com/iframe2" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, ], }
구현체가 교차 출처 iframe의 메모리 측정을 생략하면 결과는 다음과 같을 수 있습니다:
{ bytes: 1200000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 200000 , attribution: [ { url: "https://example.com/iframe2" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
2. 데이터 모델
2.1. 메모리 측정 결과
performance.measureUserAgentSpecificMemory()
함수는 Promise
를 반환하며, 이는 MemoryMeasurement
딕셔너리 인스턴스로 resolve됩니다:
dictionary {MemoryMeasurement unsigned long long ;bytes sequence <MemoryBreakdownEntry >; };breakdown
-
measurement .bytes -
전체 메모리 사용량을 나타내는 값입니다.
-
measurement .breakdown -
전체
bytes를 분할하고 귀속 및 타입 정보를 제공하는 배열입니다.
dictionary {MemoryBreakdownEntry unsigned long long ;bytes sequence <MemoryAttribution >;attribution sequence <DOMString >; };types
-
breakdown .bytes -
이 엔트리가 설명하는 메모리의 크기입니다.
-
breakdown .attribution -
해당 메모리를 사용하는 JavaScript Realm의 URL 및/또는 컨테이너 요소 배열입니다.
-
breakdown .types -
메모리와 연관된 구현 정의 메모리 타입의 배열입니다.
dictionary {MemoryAttribution USVString ;url MemoryAttributionContainer ;container DOMString ; };scope
-
attribution .url -
이 귀속 정보가 동일 출처의 JavaScript Realm에 해당하는 경우, 이 필드는 해당 Realm의 URL을 포함합니다. 그렇지 않으면 귀속 정보는 하나 이상의 교차 출처 JavaScript Realm에 대한 것이며, 이 필드는
"cross-origin-url"이라는 센티널 값을 가집니다. -
attribution .container -
JavaScript Realm을 (간접적으로라도) 포함하는 DOM 요소를 설명합니다. 동일 출처 최상위 Realm에 대한 귀속 정보라면 이 속성이 없을 수 있습니다. 교차 출처 Realm은 교차 출처 격리 때문에 최상위가 될 수 없습니다.
-
attribution .scope -
동일 출처 JavaScript Realm의 타입을 설명합니다:
"Window", "DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"또는 교차 출처의 경우"cross-origin-aggregated"가 사용됩니다.
dictionary {MemoryAttributionContainer DOMString ;id USVString ; };src
2.2. 중간 메모리 측정
이 명세서는 주어진 구현 정의 알고리즘이 현재 에이전트 클러스터의 주소 공간에서 주어진 에이전트 클러스터 집합의 메모리 사용량을 측정함을 가정합니다. 이런 알고리즘의 결과는 중간 메모리 측정이고, 이는 집합의 중간 메모리 분할 엔트리로 구성됩니다.
주소 공간 격리의 보안 보장을 지키기 위해, 현재 주소 공간 외부의 메모리를 나타내는 모든 중간 메모리 분할 엔트리는 bytes 값을 0으로 설정해야 합니다.
핑거프린팅 위험을 줄이기 위해, 결과에는 주어진 에이전트 클러스터 집합에 의해 할당되거나 사용된 웹 플랫폼 객체와 관련된 메모리만 포함해야 합니다. 예를 들어, UA(사용자 에이전트) 고유 확장과 빈 페이지의 기준 메모리는 제외해야 합니다. 메모리는 주소 공간 수준에서 집계되어야 하며, 메모리 압축, 지연 메모리 할당 등 플랫폼 전용 메모리 최적화는 제외해야 합니다.
중간 메모리 분할 엔트리는 다음 항목을 가지는 struct입니다:
- bytes
-
이 중간 메모리 분할 엔트리가 설명하는 메모리의 크기 또는 이 엔트리가 현재 주소 공간 외부의 메모리를 나타내면 0입니다.
- realms
-
메모리가 귀속되는 집합의 JavaScript Realm 들입니다.
- types
본 명세서에서 정의한 알고리즘은 중간 메모리 측정을 MemoryMeasurement
인스턴스로 변환하는 방법을 보여줍니다.
2.3. 메모리 귀속 토큰
임베드된 JavaScript Realm과 그 컨테이너 요소 사이의 연결은 일시적이며 항상 존재함이 보장되지 않습니다. 예를 들어, 컨테이너 요소 내에서 다른 문서로 내비게이션하거나 해당 요소를 DOM 트리에서 제거하면 연결이 끊어집니다.
메모리 귀속 토큰은 JavaScript Realm에서 컨테이너 요소로 이동할 수 있는 방법을 제공합니다. 이는 다음 항목을 가지는 struct입니다:
- container
-
MemoryAttributionContainer인스턴스입니다. - cross-origin aggregated flag
-
토큰이 교차 출처 JavaScript Realm의 메모리 사용량 집계를 위해 생성되었는지 표시하는 불리언 플래그입니다.
이는 WindowOrWorkerGlobalScope
의 새 내부 필드에 생성 시 저장되며, 메모리 리포팅을 위해 항상 접근 가능합니다.
3. 처리 모델
3.1.
Performance
인터페이스 확장
partial interface Performance { [Exposed =(Window ,ServiceWorker ,SharedWorker ),CrossOriginIsolated ]Promise <MemoryMeasurement >measureUserAgentSpecificMemory (); };
-
performance .measureUserAgentSpecificMemory() -
비동기 메모리 측정을 수행하는 메서드입니다. 반환 값에 대한 자세한 정보는 § 2.1 메모리 측정 결과에 있습니다.
3.2. 최상위 알고리즘
measureUserAgentSpecificMemory()
메서드 단계:
-
Assert: 현재 Realm의 settings object의 cross-origin isolated capability가 true임을 보장한다.
-
메모리 측정 허용 조건자에 현재 Realm을 넘기면 false인 경우:
-
promise를 "
SecurityError"DOMException으로 reject하여 반환한다.
-
-
the current agent cluster를 현재 Realm의 agent의 에이전트 클러스터로 할당한다.
-
agent clusters를 get all agent clusters에 현재 Realm을 넘겨 구한다.
-
promise를 새로운
Promise로 생성한다. -
the current agent cluster, agent clusters, promise를 인자로 비동기 구현 정의 메모리 측정을 시작한다.
-
promise를 반환한다.
-
global object를 realm의 global object로 할당한다.
-
global object가
SharedWorkerGlobalScope이면 true를 반환한다. -
global object가
ServiceWorkerGlobalScope이면 true를 반환한다. -
global object가
Window이면-
settings object를 realm의 settings object로 할당한다.
-
settings object의 origin이 settings object의 top-level origin과 같으면 true를 반환한다.
-
-
false를 반환한다.
-
realm의 global object가
Window이면:-
group를 브라우징 컨텍스트 그룹으로 할당하며, 이는 realm의 global object의 browsing context를 포함한다.
-
group의 agent cluster map에서 값들을 가져와서 반환한다.
-
Promise
promise에 대해 병렬로 다음 단계 실행:
-
intermediate memory measurement를 구현 정의 중간 메모리 측정을 the current agent cluster와 agent clusters에 대해 수행한다.
-
promise의 relevant global object에서 TODO 태스크 소스에 글로벌 태스크를 큐에 추가하여, resolve promise를 새로운 메모리 측정 생성 결과로 완료한다.
3.3. 중간 메모리 측정값을 결과로 변환하기
-
bytes를 0으로 둔다.
-
각각의 중간 메모리 분할 엔트리 intermediate entry에 대해 intermediate measurement에서:
-
bytes를 bytes + intermediate entry의 bytes로 설정한다.
-
-
breakdown을 새로운 리스트로 둔다.
-
breakdown에 다음 내용을 가진 새로운
MemoryBreakdownEntry를 추가한다:-
bytes값은 0, -
attribution값은 « », -
types값은 « ».
-
-
각각의 중간 메모리 분할 엔트리 intermediate entry에 대해 intermediate measurement에서:
-
breakdown entry를 새로운 메모리 분할 엔트리 생성의 결과로 둔다 (intermediate entry를 인자로).
-
breakdown에 breakdown entry를 추가한다.
-
-
breakdown의 순서를 임의로 섞는다.
-
다음 값을 가진 새로운
MemoryMeasurement를 반환한다:
-
attribution을 새로운 리스트로 둔다.
-
각각의 JavaScript realm realm에 대해 intermediate entry의 realms에서:
-
attribution entry를 새로운 메모리 귀속 정보 생성의 결과로 둔다 (realm를 인자로).
-
attribution에 attribution entry를 추가한다.
-
-
types를 intermediate entry의 types로 둔다.
-
types의 순서를 임의로 섞는다.
-
다음 값을 가진 새로운
MemoryBreakdownEntry를 반환한다:-
attribution값은 attribution, -
types값은 types.
-
token을 realm의 global object의 메모리 귀속 토큰으로 둔다.
-
token의 cross-origin aggregated flag가 true이면
-
scope name을 identifier로 설정 (realm의 global object의 interface).
-
다음 값을 가진 새로운
MemoryAttribution을 반환:-
url값은 realm의 settings object의 creation URL, -
container값은 token의 container(null이 아니면), 아니면container항목은 생략, -
scope값은 scope name.
-
3.4. 메모리 귀속 토큰 생성 또는 획득
HTMLElement
container element, 그리고 memory attribution token parent token에 대해:
-
container element가 null이면:
-
parent origin이 top-level origin과 다르면:
-
parent token을 반환.
-
-
container를 컨테이너 요소 속성 추출의 결과(container element 인자)로 둔다.
-
origin이 top-level origin과 같으면:
-
다음 값의 새로운 메모리 귀속 토큰을 반환:
-
container 값은 container,
-
cross-origin aggregated flag 값은 false.
-
-
-
다음 값의 새로운 메모리 귀속 토큰을 반환:
-
container 값은 container,
-
cross-origin aggregated flag 값은 true.
-
WorkerGlobalScope
worker global scope, environment settings object outside settings에
대해:
-
worker global scope가
DedicatedWorkerGlobalScope라면, outside settings의 global object의 메모리 귀속 토큰을 반환. -
Assert: worker global scope가
SharedWorkerGlobalScope또는ServiceWorkerGlobalScope여야 한다. -
다음 값을 가진 새로운 메모리 귀속 토큰을 반환:
-
container 값은 null,
-
cross-origin aggregated flag 값은 false.
-
HTMLElement
container element에 대해:
-
container element의 local name에 따라 처리:
- "iframe"
-
다음 값을 가진 새로운
MemoryAttributionContainer를 반환: - "frame"
-
다음 값을 가진 새로운
MemoryAttributionContainer를 반환: - "object"
-
다음 값을 가진 새로운
MemoryAttributionContainer를 반환:
4. 기존 명세와의 통합
4.1. WindowOrWorkerGlobalScope
확장
WindowOrWorkerGlobalScope에
새로운 내부 필드가 추가됩니다:
- memory attribution token
-
이 환경의 메모리 사용량을 보고하는 데 사용되는 memory attribution token입니다.
4.2. 기존 알고리즘의 확장
run a worker 알고리즘은 6단계에서 새로 생성된 글로벌 오브젝트의 memory attribution token 필드를 설정합니다:
realm execution context를 agent 및 다음 커스터마이징을 사용하여 새로운 JavaScript realm을 생성한 결과로 둔다:
...
글로벌 오브젝트의 memory attribution token을 글로벌 오브젝트와 outside settings를 인자로 worker 메모리 귀속 토큰 획득 결과로 설정한다.
create and initialize a Document object 알고리즘은 새 글로벌 오브젝트의 memory attribution token 필드를 설정합니다:
그렇지 않은 경우:
token을 빈 memory attribution token으로 둔다.
browsingContext가 top-level browsing context가 아니면:
parentToken을 parentEnvironment의 global object의 memory attribution token으로 둔다.
token을 window 메모리 귀속 토큰 획득 결과로 설정 (origin, parentEnvironment의 origin, topLevelOrigin, browsingContext의 container, parentToken 인자).
그 외에는 token을 window 메모리 귀속 토큰 획득 결과로 설정 (origin, null topLevelOrigin, null, null).
window global scope를 realm execution context의 Realm 컴포넌트의 global object로 둔다.
window global scope의 memory attribution token을 token으로 설정한다.
create a new browsing context 알고리즘은 새 글로벌 오브젝트의 memory attribution token 필드를 설정합니다:
token을 빈 토큰으로 둔다.
embedder가 null이면 token을 window 메모리 귀속 토큰 획득 결과로 설정 (origin, null, topLevelOrigin, null, null).
그 밖의 경우, token을 window 메모리 귀속 토큰 획득 결과로 설정하는데, 인자는 origin, embedder의 관련 settings object의 origin, topLevelOrigin, embedder, embedder의 관련 글로벌 객체의 메모리 귀속 토큰을 사용한다.
window global scope를 realm execution context의 Realm 컴포넌트의 global object로 둔다.
window global scope의 memory attribution token을 token으로 설정한다.
5. 프라이버시 및 보안
5.1. 교차 출처 정보 유출
결과에 나타나는 URL 및 기타 문자열 값은 API를 호출하는 origin이 이미 알고 있는 값임이 보장됩니다.
교차 출처로 노출되는 유일한 정보는 memoryMeasurement.bytes
및 memoryBreakdownEntry.bytes에
제공된 size 정보입니다.
API는 교차 출처 크기 정보 유출을 완화하기 위해 교차 출처 격리 메커니즘에 의존합니다.
구체적으로, API는 현재 주소 공간의 모든 리소스가 자신의 임베더 origin에 임베드 및 접근 가능함을 opt-in 했음을 보장하는 불변식에 의존합니다.
API는 별도 주소 공간에 로드된 교차 출처 리소스의 크기를 노출하지 않습니다.
5.2. 핑거프린팅
API의 결과는 웹 페이지 자체가 할당한 객체에만 의존하며, 빈 페이지의 기준 메모리 사용량 등과 같은 무관한 메모리는 포함하지 않습니다. 즉, 동일한 사용자 에이전트 바이너리가 두 다른 장치에서 동일한 웹 페이지에 대해 실행될 때 동일한 결과가 나와야 합니다.
웹 페이지는 아래와 같은 사용자 에이전트 정보를 추론할 수 있습니다:
-
사용자 에이전트의 비트 수 (32-비트 또는 64-비트).
-
일정 수준까지 사용자 에이전트의 버전.
유사한 정보는 기존 API(navigator.userAgent,
navigator.platform)에서도
얻을 수 있습니다.
사용자 에이전트의 비트 수 또한 32비트/64비트 연산의 실행 시간 측정으로 추론할 수 있습니다.
현재 API는 오직 최상위 origin에서만 사용할 수 있습니다. 향후에는 최상위 origin이 Permissions Policy를 사용해 다른 origin에 API 사용을 위임할 수 있게 될 예정입니다. 두 경우 모두, 교차 출처 iframe은 기본적으로 API 접근 권한이 없습니다.
6. 감사의 글
Domenic Denicola와 Shu-yu Guo에게 API 설계에 기여하고 이 명세를 리뷰해 주셔서 감사드립니다.또한 Adam Giacobbe, Anne van Kesteren, Artur Janc, Boris Zbarsky, Chris Hamilton, Chris Palmer, Daniel Vogelheim, Dominik Inführ, Hannes Payer, Joe Mason, Kentaro Hara, L. David Baron, Mathias Bynens, Matthew Bolohan, Michael Lippautz, Mike West, Neil Mckay, Olga Belomestnykh, Per Parker, Philipp Weis, 그리고 Yoav Weiss에게 피드백 및 기여에 감사드립니다.