← All reports

[JSC] Optimize initial `then` call

Component: JSC | c3276f4

JSC의 DFG IR은 node마다 operation과 proven type constraint를 함께 인코딩합니다. PerformPromiseThen은 promise, fulfill handler, reject handler, result capability 네 개의 child를 갖는 범용 node입니다. 각 handler가 런타임에 callable일 수도, null/undefined일 수도 있기 때문에 conservative하게 설계되어 있습니다. Abstract Interpreter는 SpeculatedType bit set을 이용해 type prediction을 전파하는데, callable에는 SpecFunction을, null/undefined에는 SpecOther를 사용합니다.

JSTests/stress/perform-promise-then-one-handler.js

+// Conversion happens in DFGConstantFoldingPhase only when the abstract interpreter
+// has *proven* that one handler is SpecFunction and the other is SpecOther
+// (undefined or null). Otherwise we keep the generic 4-child PerformPromiseThen
+// node, whose runtime semantics this test also verifies.
+
+// --- Converted FulfillHandler kind: .then(fn) — onRejected is implicit undefined ---
+function fulfillOnly(p) {
+ return p.then(v => v + 1);
+}
+noInline(fulfillOnly);
+
+// --- Converted RejectHandler kind: .then(undefined, fn) ---
+function rejectOnlyUndefined(p) {
+ return p.then(undefined, e => 'caught:' + e);
+}
+noInline(rejectOnlyUndefined);
+
+// --- Non-converted: two callable handlers — neither side is SpecOther ---
+function bothHandlers(p) {
+ return p.then(v => 'ok:' + v, e => 'err:' + e);
+}
+
+// --- Non-converted: AI proves SpecInt32, neither SpecFunction nor SpecOther ---
+function intHandlerWithReject(p) {
+ return p.then(1, e => 'int-rej:' + e);
+}

AI가 두 handler 중 정확히 하나만 SpecOther임을 증명한 경우, 새로 추가된 PerformPromiseThenOneHandler node가 fast path를 inline합니다. 이 변환은 DFGConstantFoldingPhase에서 수행되며, handler 종류(fulfill 또는 reject)를 node flag에 인코딩하고 reaction cell을 할당하는 대신 flag를 직접 쓰고 value를 저장하는 방식으로 처리합니다. 이는 초기 .then() 호출에서 reaction cell을 제거한 313220번 커밋을 기반으로 한 확장입니다.

Promise.then(fn, undefined)  — hot loop
        │
        ▼
  DFGFixupPhase
  PerformPromiseThen [promise, fn, undefined, resultCap]  (4 children)
        │
        ▼
  AbstractInterpreter
  handler[1] proven SpecOther? ──yes──► DFGConstantFoldingPhase
        │                                converts to:
        │                                PerformPromiseThenOneHandler
        │                                  kind=FulfillHandler
       no                                  (2 effective children)
        │                                      │
        ▼                                      ▼
  keep PerformPromiseThen            DFG/FTL SpeculativeJIT
  (generic slow-path operation)      inline: write flags + store handler slot
                                     slow branch → operationPerformPromiseThenOneHandler
                                     (when promise already settled at JIT time)

실제 프로덕션 JS 코드에서 흔히 쓰이는 .then(fn).then(undefined, fn) 패턴이, 이제 reaction cell 할당으로 빠지는 대신 촘촘한 inline memory operation으로 컴파일됩니다. 다만 그 대가로, security-sensitive한 영역인 promise handler dispatch에 JIT가 내부 object layout을 직접 기록하는 코드를 생성하게 되었습니다.

Narrow: classifyPerformPromiseThen의 classification gate는 두 handler 중 하나가 SpecOther임을 증명하는 AI type-proof에 의존합니다. 만약 AI가 과도하게 widen되거나 오분류되도록 만들 수 있다면 문제가 됩니다. 예를 들어 처음에는 null만 관측되다가 나중에 callable이 나타나는 polymorphic call site가 그런 경우입니다. 이 경우 specialized node가 실행되면서 실제로는 callable인 handler slot을 다루게 되고, 결과적으로 handler가 조용히 누락되거나 잘못 라우팅될 가능성이 있습니다. 눈여겨봐야 할 지점은, 이 node 변환이 런타임 체크가 아니라 call-site history로부터 추론된 "부재 증명"에 게이팅되어 있다는 점입니다.

Wider: store barrier 문제입니다. 새 node는 JSObject(handler)를 promise의 internal slot에 저장합니다. 만약 DFGStoreBarrierInsertionPhase.cpp가 이 새 node type에 대해 write barrier를 삽입하지 않는다면, GC가 root를 놓치고 살아있는 handler function을 수거해버릴 수 있습니다. 이는 managed-heap 관점에서 use-after-free에 해당합니다. cell을 기존 object에 저장하는 모든 신규 DFG node는 이 의무를 지므로, barrier phase의 node switch 문을 훑어 해당 node보다 뒤늦게 추가된 case가 있는지 점검할 필요가 있습니다. 이와 함께, inline flag encoding과 slot offset이 slow path 및 GC가 기대하는 값과 일치하는지도 확인해야 합니다. 이 값들이 서로 어긋나면 promise state가 손상되고, 이는 await chain 전반에 걸쳐 type confusion 형태로 드러날 수 있습니다.

Widest: fast/slow boundary contract입니다. JIT는 promise가 pending 상태일 때만 inline path를 태우고, 이미 settled된 promise는 operationPerformPromiseThenOneHandler로 떨어뜨립니다. 이 C++ operation이, JIT가 실제로는 보장하지 않는 precondition을 가정하고 있지는 않은지 점검할 필요가 있습니다. JIT fast path가 검사하는 조건과 그 runtime fallback이 가정하는 조건 사이의 이런 비대칭은 반복적으로 나타나는 패턴이므로, inline fast path와 짝을 이루는 모든 operation* entry point를 훑어볼 가치가 있습니다. FTLLowerDFGToB3.cpp의 FTL lowering에 대해서도 같은 질문이 적용됩니다. B3가 생성한 코드가 DFG path의 invariant를 준수하는지, 그리고 어떤 exit도 InlineReactionKind flag를 잘못 처리하지 않는지 확인해야 합니다.