Skip to content

Commit eb9fff6

Browse files
committed
[JSC] Support Promise.race in async stack trace
https://bugs.webkit.org/show_bug.cgi?id=299675 Reviewed by Yusuke Suzuki. This patch changes async stack trace to support `Promise.race`. Test: JSTests/stress/async-stack-trace-promise-race-basic.js * JSTests/stress/async-stack-trace-promise-race-basic.js: Added. (nop): (shouldThrowAsync): (throw.new.Error.async fine): (throw.new.Error.async thrower): (throw.new.Error.async run): (async task1): (async task2): (async task3): (async run): (throw.new.Error.async drainMicrotasks): * Source/JavaScriptCore/builtins/PromiseConstructor.js: (race): * Source/JavaScriptCore/interpreter/Interpreter.cpp: (JSC::Interpreter::getAsyncStackTrace): Canonical link: https://commits.webkit.org/300795@main
1 parent 5e9a25f commit eb9fff6

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//@ requireOptions("--useAsyncStackTrace=1")
2+
3+
const source = "async-stack-trace-promise-race-basic.js";
4+
5+
function nop() {}
6+
7+
function shouldThrowAsync(run, errorType, message, stackFunctions) {
8+
let actual;
9+
var hadError = false;
10+
run().then(
11+
function (value) {
12+
actual = value;
13+
},
14+
function (error) {
15+
hadError = true;
16+
actual = error;
17+
},
18+
);
19+
drainMicrotasks();
20+
if (!hadError) {
21+
throw new Error("Expected " + run + "() to throw " + errorType.name + ", but did not throw.");
22+
}
23+
if (!(actual instanceof errorType)) {
24+
throw new Error("Expected " + run + "() to throw " + errorType.name + ", but threw '" + actual + "'");
25+
}
26+
if (message !== void 0 && actual.message !== message) {
27+
throw new Error("Expected " + run + "() to throw '" + message + "', but threw '" + actual.message + "'");
28+
}
29+
30+
const stackTrace = actual.stack;
31+
if (!stackTrace) {
32+
throw new Error("Expected error to have stack trace, but it was undefined");
33+
}
34+
35+
const stackLines = stackTrace.split('\n').filter(line => line.trim());
36+
37+
for (let i = 0; i < stackFunctions.length; i++) {
38+
const [expectedFunction, expectedLocation] = stackFunctions[i];
39+
const isNativeCode = expectedLocation === "[native code]"
40+
const stackLine = stackLines[i];
41+
42+
let found = false;
43+
44+
if (isNativeCode) {
45+
if (stackLine === `${expectedFunction}@[native code]`)
46+
found = true;
47+
} else {
48+
if (stackLine === `${expectedFunction}@${source}:${expectedLocation}`)
49+
found = true;
50+
if (stackLine === `${expectedFunction}@${source}`)
51+
found = true;
52+
}
53+
54+
if (!found) {
55+
throw new Error(
56+
`Expected stack trace to contain '${expectedFunction}' at '${expectedLocation}', but got '${stackLine}'` +
57+
`\nActual stack trace:\n${stackTrace}\n`
58+
);
59+
}
60+
}
61+
}
62+
63+
{
64+
async function fine() { }
65+
async function thrower() { await fine(); throw new Error('error'); }
66+
async function run() { await Promise.race([thrower()]); }
67+
68+
for (let i = 0; i < testLoopCount; i++) {
69+
shouldThrowAsync(
70+
async function test() {
71+
await run();
72+
}, Error, 'error', [
73+
["thrower", "65:59"],
74+
["async run", "66:44"],
75+
["async test", "71:18"],
76+
["drainMicrotasks", "[native code]"],
77+
["shouldThrowAsync", "19:20"],
78+
["global code", "69:21"]
79+
]
80+
);
81+
drainMicrotasks();
82+
}
83+
}
84+
85+
{
86+
async function task1() {
87+
88+
await nop();
89+
90+
throw new Error("error from task1");
91+
}
92+
async function task2() {
93+
await nop();
94+
95+
96+
97+
throw new Error('error from task2');
98+
}
99+
async function task3() { await 1; throw new Error("error from task3"); }
100+
async function run() { await Promise.race([task1(), task2(), task3()]); }
101+
102+
for (let i = 0; i < testLoopCount; i++) {
103+
shouldThrowAsync(
104+
async function test() {
105+
await run();
106+
}, Error, 'error from task1' , [
107+
["task1", "90:20"],
108+
["async run", "100:44"],
109+
["async test", "105:18"],
110+
["drainMicrotasks", "[native code]"],
111+
["shouldThrowAsync", "19:20"],
112+
["global code", "103:21"]
113+
]
114+
);
115+
drainMicrotasks();
116+
}
117+
}

Source/JavaScriptCore/builtins/PromiseConstructor.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,11 @@ function race(iterable)
330330

331331
for (var value of iterable) {
332332
var nextPromise = promiseResolve.@call(this, value);
333-
nextPromise.then(resolve, reject);
333+
var then = nextPromise.then;
334+
if (@isPromise(nextPromise) && then === @defaultPromiseThen)
335+
@performPromiseThen(nextPromise, resolve, reject, @undefined, /* context */ promise);
336+
else
337+
nextPromise.then(resolve, reject);
334338
}
335339
} catch (error) {
336340
reject.@call(@undefined, error);

Source/JavaScriptCore/interpreter/Interpreter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ void Interpreter::getAsyncStackTrace(JSCell* owner, Vector<StackFrame>& results,
497497
}
498498
}
499499

500-
// handle `Promise.any`
500+
// handle `Promise.any` and `Promise.race`
501501
if (auto* contextPromise = jsDynamicCast<JSPromise*>(promiseContext)) {
502502
if (JSValue parentContext = getContextValueFromPromise(contextPromise)) {
503503
if (auto* generator = jsDynamicCast<JSGenerator*>(parentContext))

0 commit comments

Comments
 (0)