1. 소개
이 섹션은 비규범적입니다.
스케줄링은 웹사이트 성능을 개선하기 위한 중요한 개발자 도구가 될 수 있습니다. 넓은 의미에서 스케줄링은 사용자에게 인지되는 지연과 응답성이라는 두 영역에서 유의미한 영향을 줄 수 있습니다. 스케줄링은 낮은 우선순위 작업을 미루고 사용자 경험 품질에 직접적인 영향을 주는 높은 우선순위 작업을 우선시함으로써 사용자에게 인지되는 지연을 개선할 수 있습니다. 예를 들어, 페이지 로드 중 특정 서드파티(3P) 라이브러리 스크립트의 실행을 미루면 화면에 픽셀이 더 빨리 표시되어 사용자에게 이득이 될 수 있습니다. 동일하게 뷰포트 내 콘텐츠와 관련된 작업을 우선순위화하는 것도 적용됩니다. 메인 스레드에서 실행되는 스크립트의 경우, 긴 작업은 입력 및 시각적 응답성 모두에 부정적인 영향을 미쳐 입력과 UI 업데이트의 실행을 차단할 수 있습니다. 이러한 작업을 더 작은 조각으로 나누고 이 조각들(또는 작업의 연속)을 스케줄링하는 것은 응답성을 개선하기 위해 애플리케이션 및 프레임워크 개발자들이 사용하는 검증된 접근법입니다.
사용자 공간 스케줄러는 일반적으로 작업을 스케줄링하고 해당 작업이 언제 실행될지 제어하는 메서드를 제공함으로써 동작합니다. 작업은 보통 관련 우선순위를 가지며, 이는 주로 스케줄러가
제어하는 다른 작업들과 비교했을 때 해당 작업이 언제 실행될지를 결정합니다. 스케줄러는 일반적으로 일정 시간(스케줄러 퀀텀) 동안 작업을 실행한 뒤 브라우저에 제어를 반환하는 방식으로
동작합니다. 스케줄러는 setTimeout()
또는 postMessage()
호출과 같은 방식으로 연속 작업을 스케줄하여 다시 실행을 재개합니다.
사용자 공간 스케줄러가 성공을 거두었지만, 중앙집중식 브라우저 스케줄러와 더 나은 스케줄링 원시 기능이 있으면 상황이 개선될 수 있습니다. 스케줄러의 우선순위 시스템은 스케줄러의 도달
범위까지만 확장됩니다. 사용자 공간 스케줄러에 대한 결과로서, UA는 일반적으로 사용자 공간 작업 우선순위를 알지 못합니다. 예외는 스케줄러가 일부 작업에 대해 requestIdleCallback()을
사용하는 경우인데, 이는 최저 우선순위 작업으로 한정됩니다. 동일한 문제가 페이지에 여러 스케줄러가 있는 경우에도 존재하는데, 이는 점점 더 흔해지고 있습니다. 예를
들어, 애플리케이션은 자체 스케줄러를 가진 프레임워크(예: React)로 빌드되어 일부 스케줄링을 자체적으로 수행하고, 심지어 스케줄러가 있는 기능(예: 임베디드 지도)을 포함할 수도
있습니다. 브라우저는 전역 정보를 가지고 있고 이벤트 루프가 작업 실행을 담당하므로 이상적인 조정 지점입니다.
우선순위를 제외하더라도, 사용자 공간 스케줄러가 의존하는 현재의 원시 기능들은 현대적인 사용 사례에 이상적이지 않습니다. setTimeout(0)은
지연 없는 작업을 스케줄링하는 전형적인 방법이지만, 중첩된 작업 등에 대해 종종 최소 지연 값이 있어 지연이 증가하여 성능이 저하될 수 있습니다. 널리 알려진 해결책은 postMessage() 또는 MessageChannel를
사용하는 것이지만, 이러한 API는 스케줄링 용도로 설계된 것이 아니므로 콜백을 큐잉할 수 없습니다. requestIdleCallback()은
일부 사용 사례에 효과적일 수 있지만, 이는 유휴 작업에만 적용되며 우선순위가 변경될 수 있는 작업(예: 스크롤과 같은 사용자 입력에 반응하여 오프스크린 콘텐츠의 우선순위를 재조정하는
경우)을 고려하지 않습니다.
이 명세는 개발자가 우선순위가 지정된 작업과 연속을 스케줄링하고 제어하기 위한 새로운 인터페이스를 도입합니다. 이 문맥에서 작업은 자체 이벤트 루프 이벤트 루프의 task로 비동기적으로 실행되는 JavaScript 콜백입니다. 연속은 제어를 브라우저에 양보한 후 새로운 이벤트 루프 이벤트 루프 task에서 JavaScript 코드가 재개되는 것을 의미합니다. Scheduler
인터페이스는 작업을 스케줄링하기 위한 postTask()
메서드와 연속을 스케줄링하기 위한 yield()
메서드를 노출합니다. 이 명세는 작업 및 연속 실행 순서를 제어하기 위한 여러 TaskPriorities를
정의합니다. 추가로, 예정된 작업을 중단하고 우선순위를 제어하는 데 사용할 수 있는 TaskController와
관련된 TaskSignal이
제공됩니다.
2. 작업 및 연속 스케줄링
2.1. 작업 및 연속 우선순위
이 명세는 작업 스케줄링을 지원하기 위해 세 가지 우선순위를 형식화합니다:
enum {TaskPriority "user-blocking" ,"user-visible" ,"background" };
user-blocking는 가장 높은 우선순위로, 낮은 우선순위로 실행할 경우 사용자
경험이 저하되는 작업을 위해 사용됩니다. 예를 들어, 사용자 입력에 직접적으로 반응하는 (조각화된) 작업이나 뷰포트 내 UI 상태를 업데이트하는 작업이 해당될 수 있습니다.
이러한 우선순위로 스케줄된 작업은 일반적으로 다른 작업에 비해 이벤트 루프 우선순위가 높지만 반드시 렌더 차단(render-blocking)인 것은 아닙니다. 즉시 중단 없이 수행되어야 하는
작업은 일반적으로 동기적으로 처리하는 것이 적절하지만, 작업이 너무 오래 걸리면 응답성이 떨어질 수 있습니다. 반면 "user-blocking"
작업은 작업을 분할하여 입력 및 렌더링에 여전히 응답하면서 가능한 한 빨리 작업이 완료되도록 할 가능성을 높이는 데 사용할 수 있습니다.
user-visible는 두 번째로 높은 우선순위로, 사용자에게 관찰 가능한 유용한 부수
효과를 갖는 작업을 위한 것입니다. 다만 즉시 관찰 가능하지 않거나 사용자 경험에 필수적이지 않은 작업들입니다. 이 우선순위를 가지는 작업은 "user-blocking"
작업보다 덜 중요하거나 덜 긴급합니다. 이 우선순위가 기본값입니다.
background는 가장 낮은 우선순위로, 백그라운드 로그 처리나 측정(metric) 처리,
일부 서드파티 라이브러리 초기화와 같이 시간에 민감하지 않은 작업에 사용됩니다.
Note: 주어진 Scheduler를 통해
스케줄된 작업들은 엄격한 우선순위 순서로 실행됩니다. 즉, 스케줄러는 항상 "user-blocking"
작업을 먼저 실행하고, 그 다음에 "user-visible"
작업을 실행하며, 마지막으로 "background"
작업을 실행합니다. 연속(continuations)은 동일한 TaskPriority를
가진 작업보다 더 높은 유효 우선순위(effective priority)를 가집니다.
2.2. Scheduler 인터페이스
dictionary {SchedulerPostTaskOptions AbortSignal ;signal TaskPriority ; [priority EnforceRange ]unsigned long long = 0; };delay callback =SchedulerPostTaskCallback any (); [Exposed =(Window ,Worker )]interface {Scheduler Promise <any >postTask (SchedulerPostTaskCallback ,callback optional SchedulerPostTaskOptions = {});options Promise <undefined >yield (); };
Note: signal
옵션은 AbortSignal이나
TaskSignal
중 하나일 수 있지만, AbortSignal이
TaskSignal의
상위 클래스이므로 해당 타입으로 정의되어 있습니다. 우선순위가 변경될 수 있는 경우에는 TaskSignal이
필요합니다. 반면 취소만 필요하다면 AbortSignal이면
충분하여 기존에 AbortSignals을
사용하는 코드에 API를 통합하기 더 쉬울 수 있습니다.
result = scheduler .postTask( callback, options )-
callback의 반환값으로 이행되거나, 태스크가 중단(abort)되는 경우
AbortSignal의 abort reason으로 거부되는 promise를 반환합니다. callback 실행 중 오류가 발생하면,postTask()가 반환하는 promise는 해당 오류로 거부(reject)됩니다.태스크의
priority는 option의priority와signal조합으로 결정됩니다:-
만약 option의
priority가 명시되었다면, 해당TaskPriority가 태스크 예약에 사용되며 태스크의 우선순위는 변경할 수 없습니다(immutable). -
그렇지 않고, option의
signal이 명시되었고TaskSignal객체라면, 태스크 우선순위는 option의signal의 priority에 의해 결정됩니다. 이 경우 태스크 우선순위는 동적이며, 연결된controller.setPriority()호출로 변경할 수 있습니다. -
그 밖의 경우, 태스크 우선순위는 기본값인 "
user-visible" 이 사용됩니다.
option의
signal이 명시된 경우,signal은 태스크가 중단(abort)되었는지Scheduler가 판단하는 데 사용됩니다. -
result = scheduler .yield()-
undefined로 이행되거나, 연속동작(continuation)이 중단(abort)된 경우AbortSignal의 abort reason으로 거부되는 promise를 반환합니다.연속동작(continuation)의 우선순위와 abort에 사용할 signal은 시작 태스크(originating task)에서 상속합니다. 시작 태스크가
postTask()와AbortSignal을 함께 사용해 예약되었다면, 해당 signal을 사용해 연속동작의 abort 여부가 결정됩니다. 시작 태스크의 우선순위(TaskSignal이나 고정 우선순위)가 연속동작의 우선순위에도 사용됩니다. 시작 태스크에 우선순위가 없었다면 "user-visible" 이 사용됩니다.
Scheduler 객체는
연관된 정적 우선순위 작업 큐 맵을 가지며,
이는 (TaskPriority,
boolean)에서 스케줄러 작업
큐로의 맵입니다. 이 맵은 새로운 빈 맵으로 초기화됩니다.
Scheduler 객체는
dynamic priority task queue map을 가진다.
이 맵은 (TaskSignal,
boolean)에서 scheduler
task queue로 매핑되는 map이다. 이 맵은 새로운 비어 있는 map으로 초기화된다.
참고: 우리는 동적 우선순위 지정을, 특정 TaskSignal이
연결된 태스크를 같은 스케줄러 태스크
큐에 넣고, prioritychange 이벤트에 따라 해당 큐의 우선순위를 변경함으로써 구현한다. dynamic priority task queue map은 우선순위가 변동 가능한
스케줄러 태스크 큐를
보관하며, 맵의 키는 큐 내 모든 태스크가 연결된 TaskSignal
이다.
static priority task queue map의 값은 우선순위가 변하지
않는 스케줄러 태스크 큐이다.
정적 우선순위 태스크 — 명시적으로 priority
옵션 혹은 signal
옵션이 null이거나 AbortSignal
인 경우 예약된 태스크 — 는 맵의 키 역할을 하는 TaskPriority에
따라 이 큐들에 들어간다.
논리적으로 동등한 또 다른 방식은,
각 TaskPriority
별로 하나의 스케줄러 태스크
큐만 유지하고, TaskSignal의
priority가 바뀔 때마다
태스크를 스케줄러 태스크 큐
사이에서 옮기며, enqueue order에 따라 삽입하는 것이다. 이 방식은 모든 스케줄러에서 다음 스케줄러 태스크 큐 선택을
단순하게 만들 수 있지만, 우선순위 변동이 더 복잡해진다.
postTask(callback, options)
메서드의 단계는 주어진 callback과 options에 대해 postTask 작업을 스케줄하는 것의 결과를
반환하는 것입니다.
yield() 메서드의 단계는 yield 연속을 스케줄하는 것의
결과를 반환하는 것입니다.
2.3. 정의
스케줄러 작업(scheduler task)은 추가적인 숫자형 enqueue order 항목을 가진 task이며, 이 값은 초기값이 0인 item입니다.
다음의 task sources들은 스케줄러 작업 소스(scheduler task sources)로 정의되며, 오직 스케줄러 작업에만 사용되어야 합니다.
- The posted task task source
-
이 task source는
postTask()또는yield()를 통해 스케줄된 작업에 사용됩니다.
스케줄러 작업 큐(scheduler task queue)는 다음 항목들을 가진 struct입니다:
- priority
-
TaskPriority입니다. - is continuation
-
불리언입니다.
- tasks
- removal steps
-
알고리즘입니다.
스케줄링 상태(scheduling state)는 다음 항목들을 가진 struct입니다:
- abort source
-
초기값이 null인
AbortSignal객체 또는 null입니다. - priority source
-
초기값이 null인
TaskSignal객체 또는 null입니다.
연속 상태(continuation state)는 다음 항목들을 가진 struct입니다:
- state map
-
초기에는 비어 있는 맵입니다.
Note: state map는 키가 가비지 컬렉션되는 객체로 구현된다면 약한 맵(weak map)으로 구현할 수 있습니다.
작업 핸들(task handle)은 다음 항목들을 가진 struct입니다:
- task
-
스케줄러 작업 또는 null입니다.
- queue
-
스케줄러 작업 큐 또는 null입니다.
- abort steps
-
알고리즘입니다.
- task complete steps
-
알고리즘입니다.
2.4. 처리 모델
TaskPriority
priority, 불리언 isContinuation, 알고리즘 removalSteps와 함께:
-
queue를 새로운 스케줄러 작업 큐로 만든다.
-
queue의 priority를 priority로 설정한다.
-
queue의 is continuation을 isContinuation으로 설정한다.
-
queue의 removal steps을 removalSteps로 설정한다.
-
queue를 반환한다.
AbortSignal
또는 null signal이 주어졌을 때:
-
handle을 새로운 작업 핸들로 만든다.
-
handle의 task를 null로 설정한다.
-
handle의 queue를 null로 설정한다.
-
handle의 abort steps를 아래 단계로 설정한다:
-
Reject result를 signal의 abort reason으로 거부한다.
-
task가 null이 아니라면,
-
Remove task를 queue에서 삭제.
-
queue가 empty 상태라면 queue의 removal steps을 실행.
-
-
-
handle의 task complete steps를 아래 단계로 설정한다:
-
signal이 null이 아니면 remove handle의 abort steps을 signal에서 제거한다.
-
queue가 empty라면 queue의 removal steps을 실행한다.
-
-
handle을 반환한다.
| Priority | Is Continuation | Effective Priority |
|---|---|---|
"background"
| false
| 0 |
"background"
| true
| 1 |
"user-visible"
| false
| 2 |
"user-visible"
| true
| 3 |
"user-blocking"
| false
| 4 |
"user-blocking"
| true
| 5 |
2.4.1. 스케줄러 작업 대기 및 제거
-
task를 새로운 스케줄러 작업으로 한다.
-
task의 enqueue order를 enqueue order로 설정한다.
-
task의 steps를 steps로 설정한다.
-
task의 source를 source로 설정한다.
-
task의 document를 document로 설정한다.
-
task의 스크립트 평가 환경 설정 객체 집합을 새 빈 set으로 설정한다.
-
task를 반환한다.
HTML 명세를 리팩토링해 task에 생성자를 추가하는 것을 고려해야 합니다. 문제는 새 작업이 일반 task가 아닌 scheduler task이어야 한다는 점입니다.
2.4.2. 작업 및 연속 스케줄링
Scheduler 타입)에
대해 state (스케줄링
상태 타입)로:
-
eventLoop를 scheduler의 연관 에이전트(relevant agent)의 이벤트 루프(event loop)로 둔다.
-
연속 상태 값(continuation state value)을 scheduler에 대해 eventLoop를 인자로 하여 state로 설정한다.
Scheduler 타입)에
대해 수행하려면:
-
eventLoop를 scheduler의 relevant agent의 event loop로 설정한다.
-
continuation 상태 값 가져오기 결과를 scheduler와 eventLoop를 인자로 하여 반환한다.
Scheduler
scheduler에 대해, SchedulerPostTaskCallback
callback과 SchedulerPostTaskOptions
options가 주어졌을 때:
-
result를 새로운 promise로 설정한다.
-
signal을 options["
signal"] 값이 존재하면 해당 값으로, 그렇지 않으면 null로 설정한다. -
signal이 null이 아니면서 aborted라면, result 를 signal의 abort reason으로 reject하고 result를 반환한다.
-
state를 새로운 스케줄링 상태로 설정한다.
-
state의 abort source를 signal로 설정한다.
-
만약 options["
priority"] 존재하면, state의 priority source를 고정 우선순위 비중단 태스크 시그널 생성 결과(options["priority"]로 생성)에 설정한다. -
그 외에 signal이 null이 아니고 TaskSignal 인터페이스를 구현하면, state의 priority source를 signal로 설정한다.
-
state의 priority source가 null이면, state의 priority source를 "
user-visible" 값으로 고정 우선순위 비중단 태스크 시그널 생성 결과로 설정한다. -
handle을 태스크 핸들 생성 결과(result 및 signal 인자)를 설정한다.
-
signal이 null이 아니면, add handle의 abort steps를 signal에 추가한다.
-
enqueueSteps를 아래 단계로 정의한다:
-
handle의 queue를 스케줄러 작업 큐 선택 결과(scheduler, state의 priority source, false 인자)에 설정한다.
-
알고리즘 호출용 태스크 예약을 scheduler, handle 및 다음 단계를 인자로 실행한다:
-
eventLoop를 scheduler의 relevant agent의 event loop로 설정한다.
-
현재 스케줄링 상태 설정을 scheduler와 state로 수행한다.
-
callbackResult를 callback 함수 호출(callback에 « » 및 "
rethrow" 전달) 결과로 설정한다. 예외가 발생했다면 result를 해당 예외로 reject한다. 그렇지 않으면 result를 callbackResult로 resolve한다. -
eventLoop의 current continuation state를 null로 설정한다.
-
-
-
delay를 options["
delay"] 값으로 설정한다. -
delay가 0보다 크면, 타임아웃 후 단계 실행을 scheduler의 relevant global object, "
scheduler-postTask", delay, 및 아래 단계로 실행한다:-
signal이 null이거나 signal이 aborted가 아니면 enqueueSteps 실행.
-
-
그 외에는 enqueueSteps를 실행한다.
-
result를 반환한다.
참고: 여기서 생성된 고정 우선순위 비중단 신호는 메모리 할당을 줄이기 위해 캐시하고 재사용할 수 있습니다.
타임아웃 후 단계 실행(run steps after a timeout)은 suspension을 반드시 고려하지는 않는다. 자세한 사항은 whatwg/html#5925를 참조.
Scheduler
scheduler에 대해:
-
result를 새로운 promise로 설정한다.
-
inheritedState를 현재 스케줄링 상태 가져오기 결과(scheduler 인자)를 설정한다.
-
abortSource를 inheritedState가 null이 아니면 inheritedState의 abort source로, null이면 null로 설정한다.
-
abortSource가 null이 아니고 abortSource가 aborted면, result 를 abortSource의 abort reason으로 reject하고 result를 반환한다.
-
prioritySource를 inheritedState가 null이 아니면 inheritedState의 priority source로, null이면 null로 설정한다.
-
prioritySource가 null이면, prioritySource를 "
user-visible"로 고정 우선순위 비중단 태스크 시그널 생성 결과로 설정한다. -
handle을 태스크 핸들 생성 결과(result와 abortSource 인자)를 설정한다.
-
abortSource가 null이 아니면, add handle의 abort steps를 abortSource에 추가한다.
-
handle의 queue를 스케줄러 태스크 큐 선택 결과(scheduler, prioritySource, true 인자)로 설정한다.
-
알고리즘 실행 태스크 예약을 scheduler와 handle, 그리고 아래 단계를 인자로 하여 실행한다:
-
result를 resolve한다.
-
-
result를 반환한다.
참고: 여기서 생성된 고정 우선순위 비중단 신호는 메모리 할당을 줄이기 위해 캐시하고 재사용할 수 있다.
Scheduler
scheduler에 대해 TaskSignal
객체 signal과 불린 값 isContinuation이 주어진 경우:
-
signal이 고정 우선순위가 없다면,
-
scheduler의 동적 우선순위 작업 큐 맵이 (signal, isContinuation)을 포함하지 않는다면
-
queue를 스케줄러 작업 큐 생성으로 만드는데, 인자는 signal의 priority, isContinuation, 그리고 아래 단계.
-
Remove 동적 우선순위 작업 큐 맵[(signal, isContinuation)]에서 제거.
-
-
동적 우선순위 작업 큐 맵[(signal, isContinuation)]에 queue를 설정.
-
우선순위 변경 알고리즘을 signal에 추가. 단계:
-
-
동적 우선순위 작업 큐 맵[(signal, isContinuation)]을 반환.
-
-
그렇지 않으면
-
priority를 signal의 priority로 설정.
-
scheduler의 정적 우선순위 작업 큐 맵이 (priority, isContinuation)를 포함하지 않으면
-
queue를 스케줄러 작업 큐 생성으로 만들며, 인자는 priority, isContinuation, 아래 단계.
-
Remove 정적 우선순위 작업 큐 맵[(priority, isContinuation)]에서 제거.
-
-
정적 우선순위 작업 큐 맵[(priority, isContinuation)]에 queue를 설정.
-
-
정적 우선순위 작업 큐 맵[(priority, isContinuation)]을 반환.
-
Scheduler
scheduler에 대해, 태스크 핸들
handle과 알고리즘 steps가 주어졌을 때:
-
global을 scheduler의 relevant global object로 설정한다.
-
document를 global의 associated
Document로 설정한다. 단, global이Window오브젝트일 때만 해당하며, 그 외에는 null로 한다. -
event loop를 scheduler의 relevant agent의 event loop로 설정한다.
-
enqueue order를 event loop의 next enqueue order로 설정한다.
-
event loop의 next enqueue order를 1 증가시킨다.
-
handle의 task를 스케줄러 태스크 큐잉의 결과로 설정한다. 큐잉 대상은 handle의 큐(queue)이고, 인자로는 enqueue order, posted task task source, document 및 아래 단계를 수행하는 태스크가 사용된다:
-
steps를 실행한다.
-
handle의 태스크 완료 단계(task complete steps)를 실행한다.
-
이 알고리즘은 병렬 단계에서 호출될 수 있기 때문에, 이와 다른 알고리즘의 일부가 경쟁 조건(racy condition)을 초래할 수 있습니다. 특히, next enqueue order는 원자적으로 업데이트되어야 하며, 스케줄러 태스크 큐에 접근도 원자적으로 진행되어야 합니다. 후자는 이벤트 루프의 태스크 큐에도 영향을 줍니다(자세한 내용은 이 이슈 참조).
2.4.3. 다음에 실행할 작업 선택
Scheduler
scheduler의 실행 가능한 작업 대기열 가져오기는 다음과 같다:
-
queues를 값 가져오기로 scheduler의 정적 우선순위 작업 대기열 맵에 대해 초기화한다.
-
확장 queues를 값 가져오기로 scheduler의 동적 우선순위 작업 대기열 맵에 대해 더한다.
-
제거 queues에서 queue의 tasks가 실행 가능한 스케줄러 작업을 포함하지 않는 queue를 모두 제거한다.
-
queues를 반환한다.
Scheduler가
event loop와 연관되어 실행 가능한 작업을 가지고 있지 않다면 null을 반환한다.
-
queues를 빈 집합으로 둔다.
-
schedulers를 집합으로,
Scheduler객체 중 관련 에이전트의 이벤트 루프가 event loop와 같고, 실행 가능한 작업이 있는 것들의 집합으로 설정한다. -
각 scheduler에 대해 확장 queues에, 실행 가능한 작업 대기열 가져오기의 결과를 추가한다.
-
queues가 비어 있다면 null을 반환한다.
-
제거 queues에서, queue의 유효 우선순위가 queues의 그 어떤 항목보다도 작은 queue를 모두 제거한다.
-
queue를, queues 중 첫 실행 가능한 작업이 가장 오래된 작업인 스케줄러 작업 대기열로 둔다.
두 작업은 큐잉 순서가 고유하므로 같은 나이를 가질 수 없다. -
queue를 반환한다.
참고: 다음에 실행할 작업은 모든 Scheduler와
연관된 이벤트 루프에서 가장 오래되고, 우선순위가 가장 높은 실행 가능한 스케줄러 작업이다.
2.5. 예시
TODO(shaseley): 예시 추가.
3. 작업 제어
Scheduler
인터페이스를 통해 예약된 작업은 TaskController를
사용해, TaskSignal을
controller.signal로
받아
option으로
postTask()를
호출할 때 전달하여 제어할 수 있다.
TaskController
인터페이스는 하나의 작업 또는 여러 작업의 중단(abort) 및 우선순위 변경을 지원한다.
3.1.
TaskPriorityChangeEvent 인터페이스
[Exposed =(Window ,Worker )]interface :TaskPriorityChangeEvent Event {(constructor DOMString ,type TaskPriorityChangeEventInit );priorityChangeEventInitDict readonly attribute TaskPriority previousPriority ; };dictionary :TaskPriorityChangeEventInit EventInit {required TaskPriority ; };previousPriority
event .previousPriority-
이
TaskSignal에 대해prioritychange이벤트가 발생하기 이전의TaskPriority를 반환한다.새
TaskPriority는event.target.priority로 읽을 수 있다.
previousPriority
getter의
단계는 해당 속성이 초기화된 값을 반환하도록 한다.
3.2. TaskController 인터페이스
dictionary {TaskControllerInit TaskPriority = "user-visible"; }; [priority Exposed =(Window ,Worker )]interface :TaskController AbortController {constructor (optional TaskControllerInit = {});init undefined setPriority (TaskPriority ); };priority
참고: TaskController의
signal
getter는 AbortController로부터
상속되어,
TaskSignal
객체를 반환한다.
controller = newTaskController( init )-
새로운
TaskController를 반환하며, 그signal은 init의priority로 초기화된 새로운TaskSignal로 설정되어 있다. controller .setPriority( priority )-
이 메소드를 호출하면, 관련
TaskSignal의 priority가 변경되며, 변경을 관찰 중인 모든 곳에 신호가 전달되고prioritychange이벤트가 발생한다.
new TaskController(init)
생성자 단계:
-
signal을 새로운
TaskSignal객체로 둔다.
setPriority(priority) 메소드 단계는, 우선순위 변경 신호 보내기를 this의 signal에 priority를 넘겨 호출하는 것이다.
3.3. TaskSignal 인터페이스
dictionary { (TaskSignalAnyInit TaskPriority or TaskSignal )= "user-visible"; }; [priority Exposed =(Window ,Worker )]interface :TaskSignal AbortSignal { [NewObject ]static TaskSignal _any (sequence <AbortSignal >,signals optional TaskSignalAnyInit = {});init readonly attribute TaskPriority priority ;attribute EventHandler onprioritychange ; };
참고: TaskSignal
은 AbortSignal
에서 상속되며, AbortSignal
을 받는 API에서 사용할 수 있다.
또한, postTask()
역시 AbortSignal
을 받을 수 있으며, 이는 동적 우선순위 변경이 필요하지 않은 경우에 유용하다.
TaskSignal . any(signals, init)TaskSignal인스턴스를 반환한다. signals 중 하나라도 abort되면 이 인스턴스도 abort 된다. 그 abort reason은 abort를 유발한 signals의 reason으로 설정된다. 새 signal의 priority는 init의priority값이 정해주는데, 이는 고정된TaskPriority값일 수도 있고,TaskSignal일 수도 있다. 후자의 경우 새 signal의 priority가 해당 signal과 함께 변경된다.signal .priority-
signal의
TaskPriority를 반환한다.
TaskSignal
객체는 연결된 priority 속성을 가진다 (TaskPriority
타입).
TaskSignal
객체는 연결된 priority changing (boolean, 기본값 false) 속성을 가진다.
TaskSignal
객체는 연결된 priority change algorithms
(priority changing 값이 true일 때 실행되는 알고리즘을 담은 set, 처음엔 비어 있음)을 가진다.
TaskSignal
객체는 연결된 source signal
(TaskSignal
객체에 대한 약한 참조로, priority를 결정함), 처음엔 null이다.
TaskSignal
객체는 연결된 dependent signals
(TaskSignal
들의 약한 set, 이 객체를 참조하여 priority가 결정됨, 처음엔 비어 있음)을 가진다.
TaskSignal
객체는 연결된 dependent (boolean, 기본값 false) 속성을 가진다.
priority getter의 동작은 this의 priority를 반환하는 것이다.
onprioritychange 속성은
이벤트 핸들러 IDL 속성이다. onprioritychange
이벤트 핸들러이고,
이벤트 타입은 prioritychange이다.
static any(signals, init) 메서드는
의존 TaskSignal 생성 알고리즘을 실행한다 (signals, init, 현재 realm
사용).
TaskSignal
고정된
priority가 있다
는 dependent
signal이면서 source signal이 null일 때이다.
priority change algorithm 추가에서는,
algorithm을 TaskSignal
객체 signal의 priority change algorithms에 추가한다.
-
resultSignal = 의존 신호 생성 호출 결과(TaskSignal 인터페이스/realm 사용)
-
resultSignal의 dependent를 true로 설정
-
만약 init["
priority"] 가TaskPriority라면: -
그 외:
-
sourceSignal = init["
priority"] -
sourceSignal이 고정 priority가 없으면:
-
sourceSignal의 dependent가 true라면 sourceSignal을 sourceSignal의 source signal로 바꾼다.
-
단언: sourceSignal은 dependent가 아니다.
-
resultSignal의 source signal을 sourceSignal의 weak ref로 설정
-
Append resultSignal을 sourceSignal의 dependent signals에 추가
-
-
-
resultSignal 반환
-
만약 signal의 priority changing이 true라면 "
NotAllowedError"DOMException을 throw한다. -
만약 signal의 priority가 priority와 같으면 종료
-
signal의 priority changing을 true로 설정
-
previousPriority = signal의 priority
-
signal의 priority를 priority로 설정
-
각 algorithm을 signal의 priority change algorithms마다 실행
-
이벤트를
prioritychange이름으로 signal에 fire (이벤트 객체는TaskPriorityChangeEvent이고,previousPriority속성을 previousPriority로 초기화) -
각 dependentSignal을 signal의 dependent signals마다 signal priority change를 priority와 함께 실행
-
signal의 priority changing을 false로 설정
-
init =
TaskSignalAnyInit새 인스턴스 -
init["
priority"] = priority으로 설정 -
의존 TaskSignal 생성 알고리즘을 « », init, realm 인자로 호출해 반환
3.3.1. 가비지 컬렉션
dependent TaskSignal
객체는 source
signal이 null이 아니고,
prioritychange
이벤트 리스너가 등록되어 있거나,
priority change algorithms이 비어 있지 않다면 가비지 컬렉션 되면
안된다.
3.4. 예시
TODO(shaseley): 예시를 추가하세요.
4. 타 표준에 대한 수정사항
4.1. HTML 표준
4.1.1. WindowOrWorkerGlobalScope
WindowOrWorkerGlobalScope
mixin을 구현한 각 객체는 대응하는 scheduler를 가진다.
이는 Scheduler
인스턴스로 초기화된다.
partial interface mixin WindowOrWorkerGlobalScope { [Replaceable ]readonly attribute Scheduler scheduler ; };
scheduler 속성의 getter 동작은
this의 scheduler를 반환함
4.1.2. 이벤트 루프: 정의
다음으로 교체: 각 event loop에 대해, 각 task source는 특정 task queue와 연관되어야 한다.
다음으로 대체: 각 event loop에 대해, task source가 scheduler task source가 아니라면, 그 source는 특정 task queue와 연관되어야 한다.
추가: 이벤트 루프는 숫자형 next enqueue order를 가진다. 초기값은 1이다.
참고: next enqueue order
값은
scheduler task
queue들의 실행 순서를
TaskPriority
별로 결정하는데 사용된다.
고유하고 증가하는 수로, timestamp도 쓸 수 있다(유일성과 단조성 보장 필요).
추가: 이벤트 루프는 현재 continuation state를 가진다 (처음엔 null).
아래 알고리즘들 추가:
-
eventLoop의 current continuation state가 null이면 새 continuation state로 초기화
-
continuationState = eventLoop의 current continuation state
-
continuationState의 state map[key] = value
-
continuationState = eventLoop의 current continuation state
-
continuationState가 null이 아니고, state map[key]가 존재하면 해당 값을 반환. 아니면 null 반환
4.1.3. 이벤트 루프: 처리 모델
아래 단계들을 이벤트 루프 처리 단계의 2번 앞에 추가:
-
queues = set (해당 이벤트 루프의 task queues 중 실행 가능한 runnable task을 포함하는 것들)
-
schedulerQueue = 모든 scheduler에서 다음 task queue 선택 수행 결과
2단계를 다음과 같이 변경:
-
schedulerQueue가 null이 아니거나, queues가 비어 있지 않다면:
2.1단계는 아래처럼 변경:
-
taskQueue는 다음 중 하나, 방법은 구현 정의:
-
queues가 비어 있지 않으면 task queues 중 하나, 구현 정의로 선택
-
schedulerQueue의 tasks, 만약 schedulerQueue가 null이 아니면
-
참고: HTML 명세에서는 이벤트 루프 처리 단계에서 다음 task
source별 우선순위화가 가능하다.
본 명세 역시 다음 Scheduler
task와
이벤트 루프의 task
queue 중에서 어떤 걸 처리할지 구현정의하게 만들어, User Agent에 스케줄링의 융통성을 준다.
다만, 이 명세의 의도는
TaskPriority
값에 따라 Scheduler
task의 이벤트 루프 우선순위에 영향을 주도록 하는 것이다.
예를 들어 "background"
작업·continuation은 대체로 중요도가 낮고,
"user-blocking"
task/continuation과
"user-visible"
continuation(단, task는 아님)은 더 중요도가 높음.
한 전략은 Scheduler
task의 effective priority가 3 이상이면 사용자 입력·렌더링·긴급작업보다
낮고
대부분의 queue보다 높게, 0~1이면 타 queue가 비었을 때만 실행, 2면 타 스케줄링 관련 task
source와 비슷하게(예: timer task source) 실행할 수 있다.
이 단계의 taskQueue 는 set of tasks 혹은 set of scheduler tasks임. 이후 단계들은 item 제거만 하므로 거의 호환됨. 이상적으로는 plain task를 반환하는 공용 task queue 인터페이스가 있어야 하지만 상당한 리팩터링이 필요함.
4.1.4. 이벤트 루프: 작업 큐잉
마이크로태스크 큐에 추가 알고리즘을 선택적 불리언 ignoreContinuationState(기본값 false)를 받도록 변경한다.
5단계를 다음과 같이 변경한다:
-
continuationState를 null로 둔다.
-
ignoreContinuationState가 false이고 eventLoop의 current continuation state가 null이 아니면, continuationState를 클론한 event loop의 current continuation state로 설정한다.
-
microtask의 steps를 다음과 같이 설정한다:
-
ignoreContinuationState가 false이면 eventLoop의 current continuation state를 continuationState로 설정한다.
-
steps를 실행한다.
-
ignoreContinuationState가 false이면 eventLoop의 current continuation state를 null로 설정한다.
-
4.1.5. HostMakeJobCallback(callable)
5단계 이전에 다음을 추가한다:
-
event loop를 incumbent settings의 realm의 agent의 event loop로 설정한다.
-
클론한 event loop의 current continuation state가 null이 아니라면 그 값으로, 그렇지 않으면 null로 state를 설정한다.
5단계를 다음과 같이 바꾼다:
-
JobCallback 레코드 { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[ContinuationState]]: state } }를 반환한다.
4.1.6. HostCallJobCallback(callback, V, argumentsList)
5단계 이전에 다음을 추가한다:
-
event loop를 incumbent settings의 realm의 agent의 event loop로 설정한다.
-
event loop의 current continuation state를 callback.[[HostDefined]].[[ContinuationState]]로 설정한다.
7단계 이후에 다음을 추가한다:
-
event loop의 current continuation state를 null로 설정한다.
4.1.7. HostEnqueuePromiseJob(job, realm)
2단계를 다음과 같이 변경한다:
-
다음 단계를 ignoreContinuationState를 true로 하여 마이크로태스크 큐에 추가한다:
4.2.
requestIdleCallback()
4.2.1. 휴지 콜백 호출 알고리즘
3.3단계 이전에 다음 단계를 추가한다:
-
realm을 window의 relevant realm으로 설정한다.
-
state를 새로운 scheduling state로 설정한다.
-
state의 priority source를 "
background" 및 realm을 인자로 하여 고정 우선순위 비중단 태스크 시그널 생성 호출 결과로 설정한다. -
scheduler를 realm을 relevant realm으로 갖는
Scheduler로 설정한다. -
Set the current scheduling state를 scheduler에 대해 state로 수행한다.
3.3단계 이후에 다음을 추가한다:
-
event loop의 current continuation state를 null로 설정한다.
5. 보안 고려 사항
이 명세에서 정의하는 API의 주요 보안 고려사항은, 타이밍 기반 부채널(side-channel) 공격으로 인한 출처(origin) 간 정보 유출 가능성 여부이다.
5.1.
postTask의 고해상도 타이밍 소스 사용 방지
setTimeout()의
timeout 값과 마찬가지로, postTask()의
delay
는 전체 밀리초 단위(최소 비0 지연은 1ms)로 표현되며 호출자는 1ms보다 정밀한 타이밍을 지정할 수 없다. 또한, 태스크는 지연이 만료될 때 큐에 들어갈 뿐 즉시 실행되는 것이 아니어서,
호출자가 가질 수 있는 정밀도가 더 낮아진다.
5.2. 다른 출처의 태스크 모니터링
postTask()
또는 yield()
가 다른 출처의 태스크에 대한 정보를 유출하는지 여부다. 우리는 한 출처에서 실행 중인 공격자가 같은 브라우저 내에서 다른 출처(즉, 별도의 이벤트 루프)에서 실행 중인 코드를(동일 스레드에서
스케줄된) 관찰하려는 상황을 가정한다.
UA 내의 한 스레드는 한 번에 한 이벤트 루프의 태스크만 실행할 수 있기 때문에, 공격자는 자신의 태스크가 실행되는 시점을 관찰하여 다른 이벤트 루프에서 실행된 태스크에 대한 정보를 획득할 수 있다. 예를 들어 공격자가 태스크로 시스템을 가득 채우고 이들이 연속으로 실행되기를 기대할 때 사이가 비면 다른 태스크가(아마 다른 이벤트 루프에서) 실행됐음을 추론할 수 있다. 이런 경우 유출되는 정보는 구현 세부사항에 따라 다르며, 아래 설명처럼 정보량을 줄이는 것도 가능하다.
공격자가 어떤 정보를 얻을 수 있는가?
구체적으로 공격자는 시스템을 태스크로 과부하시키거나 재귀적으로 태스크를 스케줄하여 브라우저가 다른 태스크를
실행하는 시점을 감지할 수 있다. 이는 이미
알려진 공격이며, postMessage()
등의 기존 API로도 실행 가능하다. 공격자의 태스크 대신 실행되는 것은 다른 이벤트 루프 또는 같은 이벤트 루프의 다른 태스크, UA 내부 태스크(예: GC 등)일 수 있다.
공격자가 높은 확률로 실행 중인 태스크가 다른 이벤트 루프에 속한다는 사실을 파악할 수 있으면, 추가로 어떤 정보를 알 수 있냐는 문제다. 이벤트 루프 간 태스크 선택은 명세화되어 있지 않으므로, 이 정보는 구현체가 이벤트 루프 간 태스크를 어떻게 정렬하냐에 따라 달라진다. 하지만, 스레드를 공유하는 이벤트 루프를 단일 이벤트 루프로 취급하는 우선순위 스케줄러를 사용할 경우 더 많은 정보 노출 위험이 있다.
UA가 공격자의 태스크 대신 선택할 수 있는 가능성 있는 태스크 집합을 생각해보는 것은 유용하다. 공격자가 시스템을 태스크로 채울 때, 가능한 태스크 집합은 UA가 그 시점에서 더 높은 우선순위라고 판단한 모든 태스크가 된다. 이는 입력이 항상 제일 높고, 그 다음이 네트워크 등인 정적 우선순위일 수도 있고, 얼마나 오랫동안 굶주렸는지에 따라 UA가 동적으로 실행 대상을 고르는 경우일 수도 있다. 동적 정책을 쓰면 정보 충실도가 줄어든다.
postTask()
및 yield()
는 스케줄된 태스크와 컨티뉴에이션의 우선순위를 지원한다. 이러한 태스크가 다른 태스크 소스와 어떻게 섞이는지는 구현체마다 다르겠지만, 공격자는 우선순위 활용으로 자신의 태스크 대신 실행될
가능성 있는 집합을 더 줄일 수도 있다. 예를 들어 한 스레드 내 모든 이벤트 루프에 대해 단순히 정적 우선순위만 쓰는 UA라면, "user-blocking"
postTask()
태스크 또는
"user-visible"
이상의 yield()
컨티뉴에이션(즉, 이벤트 루프 우선순위가 더 높은 것)을 postMessage()
태스크 대신 쓰면, 그 사이에 실행될 우선순위와 작업에 따라 집합이 줄어들 수 있다.
가능한 완화책은?
위 위험을 줄이기 위해 구현자가 고려할 수 있는 완화책들:
-
가능하다면, 서로 다른 출처의 이벤트 루프를 다른 스레드에서 분리하여 실행한다. 이 공격은 이벤트 루프가 스레드를 공유할 때만 성립한다.
-
이벤트 루프간 스케줄링 방침을 순수 우선순위 기반이 아니라 라운드로빈, 공정 스케줄링 등으로 하여 우선순위 정보 유출 가능성을 줄인다. 또는 낮은 우선순위 태스크가 주기적으로 실행되도록 강제하여 우선순위 역추정을 방지할 수도 있다.
6. 프라이버시 고려 사항
이 섹션은 비규범적입니다.
우리는 이 명세에서 정의된 API를 프라이버시 관점에서 평가하였으며, 별도의 프라이버시 고려 사항이 없다고 판단한다.