@samitouri / QOS-React / commits / 033edca721

[Flight] Yolo Retention of Promises (#33737)

Follow up to #33736. If we need to save on CPU/memory pressure, we can instead just pray and hope that a Promise doesn't get garbage collected before we need to read it. This can cause fragile access to the Promise value in devtools especially if it's a slow and pressured render. Basically, you'd have to hope that GC doesn't run after the inner await finishes its microtask callback and before the resolution of the component being rendered is invoked.

Sebastian Markbåge committed Jul 9, 2025 at 10:39 UTC 033edca72199f5b0dac87e4662b6a4d32a9610ce
1 file changed +1 -45
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+1 -45
@@ -37,19 +37,6 @@ const getAsyncId = AsyncResource.prototype.asyncId;
37 const pendingOperations: Map<number, AsyncSequence> =
38 __DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
39
40 -// This is a weird one. This map, keeps a dependent Promise alive if the child Promise is still alive.
41 -// A PromiseNode/AwaitNode cannot hold a strong reference to its own Promise because then it'll never get
42 -// GC:ed. We only need it if a dependent AwaitNode points to it. We could put a reference in the Node
43 -// but that would require a GC pass between every Node that gets destroyed. I.e. the root gets destroy()
44 -// called on it and then that release it from the pendingOperations map which allows the next one to GC
45 -// and so on. By putting this relationship in a WeakMap this could be done as a single pass in the VM.
46 -// We don't actually ever have to read from this map since we have WeakRef reference to these Promises
47 -// if they're still alive. It's also optional information so we could just expose only if GC didn't run.
48 -const awaitedPromise: WeakMap<
49 - Promise<any>,
50 - Promise<any> | [Promise<any>, Promise<any>],
51 -> = __DEV__ && enableAsyncDebugInfo ? new WeakMap() : (null: any);
52 -
40 // Keep the last resolved await as a workaround for async functions missing data.
41 let lastRanAwait: null | AwaitNode = null;
42
@@ -92,38 +79,6 @@ export function initAsyncDebugInfo(): void {
79 // We don't track awaits on things that started outside our tracked scope.
80 return;
81 }
95 - let retain: null | Promise<any> | [Promise<any>, Promise<any>] =
96 - null;
97 - const triggerPromiseRef = trigger.promise;
98 - if (triggerPromiseRef !== null) {
99 - const triggerPromise = triggerPromiseRef.deref();
100 - if (triggerPromise !== undefined) {
101 - // Keep the awaited Promise alive as long as the child is alive so we can
102 - // trace its value at the end.
103 - retain = triggerPromise;
104 - }
105 - }
106 -
107 - const current = pendingOperations.get(currentAsyncId);
108 - if (current !== undefined) {
109 - const currentPromiseRef = current.promise;
110 - if (currentPromiseRef !== null) {
111 - const currentPromise = currentPromiseRef.deref();
112 - if (currentPromise !== undefined) {
113 - // Keep the previous Promise alive as long as the child is alive so we can
114 - // trace its value at the end.
115 - if (retain === null) {
116 - retain = currentPromise;
117 - } else {
118 - retain = [(retain: any), currentPromise];
119 - }
120 - }
121 - }
122 - }
123 -
124 - if (retain !== null) {
125 - awaitedPromise.set(resource, retain);
126 - }
82 // If the thing we're waiting on is another Await we still track that sequence
83 // so that we can later pick the best stack trace in user space.
84 let stack = null;
@@ -158,6 +113,7 @@ export function initAsyncDebugInfo(): void {
113 }
114 }
115 }
116 + const current = pendingOperations.get(currentAsyncId);
117 node = ({
118 tag: UNRESOLVED_AWAIT_NODE,
119 owner: resolveOwner(),