← All issues

JSPromise: inline reaction storage for single-handler .then(f)

3f9955f

JSTests/stress/promise-inline-child-reaction.js

+// PATH 1 — inline-child: single .then(f), no allocation
+{
+ let d = defer();
+ let q = d.promise.then(v => { counter++; return v + 1; });
+ d.resolve(10);
+ shouldBe(await q, 11);
+}
+// PATH 2 — spill: second .then() converts inline storage to reaction list
+{
+ let d = defer();
+ let a = d.promise.then(v => v * 2);
+ let b = d.promise.then(v => v * 3); // triggers spillInlineReaction
+ let c = d.promise.then(v => v * 4);
+}
+// PATH 3 — two-arg .then(f,g): falls through to JSFullPromiseReaction, no inline

JSPromise previously subclassed JSInternalFieldObjectImpl, storing state and reactions as JSValues in a fixed internal-fields array shared with WeakRef, FinalizationRegistry, and similar objects. Every .then() call allocated a separate JSPromiseReaction cell holding the callback and child-promise pointer, even when only one handler ever fires. This commit reworks JSPromise into a plain JSObject with a CompactPointerTuple member packing the first reaction's handler (or microtask context) alongside flag bits in one machine word; a second .then() triggers spillInlineReaction which promotes the inline cell to a JSSlimPromiseReaction linked list. New DFG nodes NewPromise and PhantomNewPromise mirror how JSFunction participates in allocation sinking, with PhantomNewPromise reconstructing the promise on OSR exit.

Before — every .then():
  JSPromise [JSInternalFieldObjectImpl]
    internalFields[1]: reactions ─► JSPromiseReaction (heap cell)

After — first .then(f) [no allocation]:
  JSPromise [JSObject]
    packed: CompactPointerTuple
              cell ─► fulfillHandler (JSFunction*)
              bits: InlineHandler flag

After — second .then() spills:
  JSPromise [JSObject]
    packed: CompactPointerTuple
              cell ─► JSSlimPromiseReaction ─► JSSlimPromiseReaction

Eliminating the per-.then() JSPromiseReaction allocation removes heap pressure across every Promise-heavy workload and rewires the entire JSPromise object model in the process. Switching from JSInternalFieldObjectImpl to plain JSObject means the GC is no longer auto-visiting an internal-fields array; the CompactPointerTuple's cell pointer must now be visited by an explicit visitChildrenImpl. New IR nodes (NewPromise, PhantomNewPromise) participate in scalar replacement and OSR-exit materialization.

🔒

New inline reaction storage, spill path, and PhantomNewPromise materialization each introduce edge cases across GC, JIT, and settle dispatch worth close inspection.

Subscribe to read more