|
29 | 29 | URIError, |
30 | 30 | } = window.__bootstrap.primordials; |
31 | 31 |
|
| 32 | + // Promise ids must be non-negative and fit in an i32 (Rust's `PromiseId`); |
| 33 | + // the increment in the async op stubs wraps back to 0 past 2^31 - 1. |
32 | 34 | let nextPromiseId = 0; |
33 | 35 | const promiseMap = new SafeMap(); |
34 | 36 | const RING_SIZE = 4 * 1024; |
|
41 | 43 | let isLeakTracingEnabled = false; |
42 | 44 | let submitLeakTrace; |
43 | 45 |
|
| 46 | + // Exposed for testing promise id wraparound behavior. |
| 47 | + function __setNextPromiseId(promiseId) { |
| 48 | + nextPromiseId = promiseId; |
| 49 | + } |
| 50 | + |
44 | 51 | function __setLeakTracingEnabled(enabled) { |
45 | 52 | isLeakTracingEnabled = enabled; |
46 | 53 | } |
|
140 | 147 | // Move old promise from ring to map |
141 | 148 | const oldPromise = promiseRing[idx]; |
142 | 149 | if (oldPromise !== NO_PROMISE) { |
143 | | - const oldPromiseId = promiseId - RING_SIZE; |
144 | | - MapPrototypeSet(promiseMap, oldPromiseId, oldPromise); |
| 150 | + // Keyed on the entry's own id rather than `promiseId - RING_SIZE` so it |
| 151 | + // stays correct across the id counter wrapping back to 0. The one |
| 152 | + // unavoidable limit: if a single promise stays pending while 2^31 more |
| 153 | + // ids are dispatched its id is reused, and this set silently overwrites |
| 154 | + // (loses) the older map entry. Not a regression: the old code broke far |
| 155 | + // sooner, and no real workload keeps an op pending across 2^31 dispatches. |
| 156 | + MapPrototypeSet(promiseMap, oldPromise[2], oldPromise); |
145 | 157 | } |
146 | 158 |
|
147 | 159 | const promise = new Promise((resolve, reject) => { |
148 | | - promiseRing[idx] = [resolve, reject]; |
| 160 | + promiseRing[idx] = [resolve, reject, promiseId]; |
149 | 161 | }); |
150 | 162 | const wrappedPromise = PromisePrototypeCatch( |
151 | 163 | promise, |
|
160 | 172 | } |
161 | 173 |
|
162 | 174 | function __resolvePromise(promiseId, res, isOk) { |
163 | | - // Check if out of ring bounds, fallback to map |
164 | | - const outOfBounds = promiseId < nextPromiseId - RING_SIZE; |
165 | | - if (outOfBounds) { |
166 | | - const promise = MapPrototypeGet(promiseMap, promiseId); |
| 175 | + // A promise stays in the ring until its slot is reclaimed by a newer |
| 176 | + // promise, at which point it moves to the map. The entry stores its own |
| 177 | + // promise id, so a slot reused after the id counter wrapped around is |
| 178 | + // never mistaken for an older promise that is now in the map. |
| 179 | + const idx = promiseId % RING_SIZE; |
| 180 | + let promise = promiseRing[idx]; |
| 181 | + if (promise !== NO_PROMISE && promise[2] === promiseId) { |
| 182 | + promiseRing[idx] = NO_PROMISE; |
| 183 | + } else { |
| 184 | + promise = MapPrototypeGet(promiseMap, promiseId); |
167 | 185 | if (!promise) { |
168 | | - throw "Missing promise in map @ " + promiseId; |
| 186 | + throw "Missing promise @ " + promiseId; |
169 | 187 | } |
170 | 188 | MapPrototypeDelete(promiseMap, promiseId); |
171 | | - if (isOk) { |
172 | | - promise[0](res); |
173 | | - } else { |
174 | | - promise[1](res); |
175 | | - } |
| 189 | + } |
| 190 | + if (isOk) { |
| 191 | + promise[0](res); |
176 | 192 | } else { |
177 | | - // Otherwise take from ring |
178 | | - const idx = promiseId % RING_SIZE; |
179 | | - const promise = promiseRing[idx]; |
180 | | - if (!promise) { |
181 | | - throw "Missing promise in ring @ " + promiseId; |
182 | | - } |
183 | | - promiseRing[idx] = NO_PROMISE; |
184 | | - if (isOk) { |
185 | | - promise[0](res); |
186 | | - } else { |
187 | | - promise[1](res); |
188 | | - } |
| 193 | + promise[1](res); |
189 | 194 | } |
190 | 195 | } |
191 | 196 |
|
192 | 197 | function hasPromise(promiseId) { |
193 | | - // Check if out of ring bounds, fallback to map |
194 | | - const outOfBounds = promiseId < nextPromiseId - RING_SIZE; |
195 | | - if (outOfBounds) { |
196 | | - return MapPrototypeHas(promiseMap, promiseId); |
197 | | - } |
198 | | - // Otherwise check it in ring |
199 | 198 | const idx = promiseId % RING_SIZE; |
200 | | - return promiseRing[idx] != NO_PROMISE; |
| 199 | + // Loose comparison on purpose: `promiseId` can be `undefined` (e.g. |
| 200 | + // `unrefOpPromise()` with a promise from an op that completed eagerly and |
| 201 | + // has no promise id attached), making `idx` NaN and the ring lookup |
| 202 | + // return `undefined` instead of the `NO_PROMISE` (null) sentinel. |
| 203 | + const promise = promiseRing[idx]; |
| 204 | + if (promise != NO_PROMISE && promise[2] === promiseId) { |
| 205 | + return true; |
| 206 | + } |
| 207 | + return MapPrototypeHas(promiseMap, promiseId); |
201 | 208 | } |
202 | 209 |
|
203 | 210 | function setUpAsyncStub(opName, originalOp, maybeProto) { |
|
223 | 230 | if (isLeakTracingEnabled) { |
224 | 231 | submitLeakTrace(id); |
225 | 232 | } |
226 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 233 | + nextPromiseId = (id + 1) & 0x7fffffff; |
227 | 234 | return setPromise(id); |
228 | 235 | }; |
229 | 236 | break; |
|
243 | 250 | if (isLeakTracingEnabled) { |
244 | 251 | submitLeakTrace(id); |
245 | 252 | } |
246 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 253 | + nextPromiseId = (id + 1) & 0x7fffffff; |
247 | 254 | return setPromise(id); |
248 | 255 | }; |
249 | 256 | break; |
|
263 | 270 | if (isLeakTracingEnabled) { |
264 | 271 | submitLeakTrace(id); |
265 | 272 | } |
266 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 273 | + nextPromiseId = (id + 1) & 0x7fffffff; |
267 | 274 | return setPromise(id); |
268 | 275 | }; |
269 | 276 | break; |
|
283 | 290 | if (isLeakTracingEnabled) { |
284 | 291 | submitLeakTrace(id); |
285 | 292 | } |
286 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 293 | + nextPromiseId = (id + 1) & 0x7fffffff; |
287 | 294 | return setPromise(id); |
288 | 295 | }; |
289 | 296 | break; |
|
303 | 310 | if (isLeakTracingEnabled) { |
304 | 311 | submitLeakTrace(id); |
305 | 312 | } |
306 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 313 | + nextPromiseId = (id + 1) & 0x7fffffff; |
307 | 314 | return setPromise(id); |
308 | 315 | }; |
309 | 316 | break; |
|
323 | 330 | if (isLeakTracingEnabled) { |
324 | 331 | submitLeakTrace(id); |
325 | 332 | } |
326 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 333 | + nextPromiseId = (id + 1) & 0x7fffffff; |
327 | 334 | return setPromise(id); |
328 | 335 | }; |
329 | 336 | break; |
|
343 | 350 | if (isLeakTracingEnabled) { |
344 | 351 | submitLeakTrace(id); |
345 | 352 | } |
346 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 353 | + nextPromiseId = (id + 1) & 0x7fffffff; |
347 | 354 | return setPromise(id); |
348 | 355 | }; |
349 | 356 | break; |
|
363 | 370 | if (isLeakTracingEnabled) { |
364 | 371 | submitLeakTrace(id); |
365 | 372 | } |
366 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 373 | + nextPromiseId = (id + 1) & 0x7fffffff; |
367 | 374 | return setPromise(id); |
368 | 375 | }; |
369 | 376 | break; |
|
383 | 390 | if (isLeakTracingEnabled) { |
384 | 391 | submitLeakTrace(id); |
385 | 392 | } |
386 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 393 | + nextPromiseId = (id + 1) & 0x7fffffff; |
387 | 394 | return setPromise(id); |
388 | 395 | }; |
389 | 396 | break; |
|
403 | 410 | if (isLeakTracingEnabled) { |
404 | 411 | submitLeakTrace(id); |
405 | 412 | } |
406 | | - nextPromiseId = (id + 1) & 0xffffffff; |
| 413 | + nextPromiseId = (id + 1) & 0x7fffffff; |
407 | 414 | return setPromise(id); |
408 | 415 | }; |
409 | 416 | break; |
|
524 | 531 | setUpAsyncStub, |
525 | 532 | hasPromise, |
526 | 533 | promiseIdSymbol, |
| 534 | + __setNextPromiseId, |
527 | 535 | }); |
528 | 536 |
|
529 | 537 | const infra = { |
|
0 commit comments