Bug 205654 - Make _callAsyncFunction:withArguments: work with promises
Summary: Make _callAsyncFunction:withArguments: work with promises
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: WebKit Misc. (show other bugs)
Version: Other
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Brady Eidson
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2019-12-30 21:56 PST by Brady Eidson
Modified: 2020-01-17 09:28 PST (History)
10 users (show)

See Also:


Attachments
Patch (13.35 KB, patch)
2020-01-06 14:30 PST, Brady Eidson
no flags Details | Formatted Diff | Diff
Patch (9.30 KB, patch)
2020-01-06 16:03 PST, Brady Eidson
no flags Details | Formatted Diff | Diff
Patch (9.35 KB, patch)
2020-01-07 16:07 PST, Brady Eidson
no flags Details | Formatted Diff | Diff
Patch (9.31 KB, patch)
2020-01-07 22:38 PST, Brady Eidson
no flags Details | Formatted Diff | Diff
Patch (9.39 KB, patch)
2020-01-08 11:21 PST, Brady Eidson
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Brady Eidson 2019-12-30 21:56:54 PST
Make _callAsyncFunction:withArguments: work with promises
Comment 1 Brady Eidson 2020-01-06 14:30:25 PST
Created attachment 386888 [details]
Patch
Comment 2 Saam Barati 2020-01-06 14:57:20 PST
Comment on attachment 386888 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=386888&action=review

r=me

> Source/WTF/wtf/SharedFunction.h:36
> +class SharedFunction<Out(In...)> : public RefCounted<SharedFunction<Out(In...)>> {

I think instead of this we can just use SharedTask

> Source/WebCore/bindings/js/ScriptController.cpp:715
> +    auto thenIdentifier = world.vm().propertyNames->builtinNames().thenPrivateName();

this should be: "world.vm().propertyNames->then".

A test that would catch this is using your own "thenable"

> Source/WebCore/bindings/js/ScriptController.cpp:719
> +    if (!thenFunction.isFunction(world.vm())) {

The right check here is:

CallData ignoredCallData;
if (thenFunction.isObject && asObject(thenFunction)->methodTable(vm)->getCallData(asObject(thenFunction), ignoredCallData) != CallType::None)

I think a test for a callable which does not pass the isFunction() query would be something like this with a Proxy as the "thenFunction":
let p = new Proxy(function() { 
    print("this should be called");
}, { });
let thenable = {then: p};

> Source/WebCore/bindings/js/ScriptController.cpp:740
> +    auto finalizeCount = makeUniqueWithoutFastMallocCheck<unsigned>(0);

nit: can you change makeUnique to just check if T is a primitive?

> Source/WebCore/bindings/js/ScriptController.cpp:747
> +    if (heap) {

these branches are not needed.

> Source/WebCore/bindings/js/ScriptController.cpp:754
> +    if (heap) {

ditto.

> Source/WebCore/bindings/js/ScriptController.cpp:756
> +            finalizeGuard.get()();

instead of getting the heap pointer here, you can just do:

world.vm().heap
Comment 3 Brady Eidson 2020-01-06 16:03:03 PST
Created attachment 386907 [details]
Patch
Comment 4 Brady Eidson 2020-01-07 08:51:03 PST
The problem with these test runs is spamming to the console from the AsyncFunction.Promise test affecting later runs.

Damn.

Fixing.
Comment 5 Brady Eidson 2020-01-07 16:07:05 PST
Created attachment 387049 [details]
Patch
Comment 6 Brady Eidson 2020-01-07 16:07:36 PST
(In reply to Brady Eidson from comment #4)
> The problem with these test runs is spamming to the console from the
> AsyncFunction.Promise test affecting later runs.
> 
> Damn.
> 
> Fixing.

Nope that wasn't it.

Accidentally applying the new async behavior to everybody.

Fixed in most recent upload.
Comment 7 Brady Eidson 2020-01-07 21:58:39 PST
That fixed the collateral damage to other API tests but revealed that the original AsyncFunction tests are now busted.

I can't quite figure out why, other than verify locally before this patch they work, and with it applied they don't.

It's a problem with serialization.

Before my patch, a JSC::JSValue representing "undefined" successfully serializes. Same for  "null"

With my patch, those exact same JSValues do not serialize.
Comment 8 Brady Eidson 2020-01-07 22:04:08 PST
For the "null" and "undefined" jsvalues,

SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception) succeeds before my patch

It fails after my patch because it has an exception.
Comment 9 Brady Eidson 2020-01-07 22:12:09 PST
Here's the body of SerializedScriptValue::create.

With my patch, an exception occurs in here.
Without my patch, it doesn't.

RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception)
{
    JSGlobalObject* lexicalGlobalObject = toJS(originContext);
    VM& vm = lexicalGlobalObject->vm();
    JSLockHolder locker(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);

    JSValue value = toJS(lexicalGlobalObject, apiValue);

------> Exception happens in the above line

    auto serializedValue = SerializedScriptValue::create(*lexicalGlobalObject, value);
    if (UNLIKELY(scope.exception())) {
        if (exception)
            *exception = toRef(lexicalGlobalObject, scope.exception()->value());
        scope.clearException();
        return nullptr;
    }
    ASSERT(serializedValue);
    return serializedValue;
}
Comment 10 Brady Eidson 2020-01-07 22:18:39 PST
(In reply to Brady Eidson from comment #9)
> Here's the body of SerializedScriptValue::create.
> 
> With my patch, an exception occurs in here.
> Without my patch, it doesn't.
> 
> RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef
> originContext, JSValueRef apiValue, JSValueRef* exception)
> {
>     JSGlobalObject* lexicalGlobalObject = toJS(originContext);
>     VM& vm = lexicalGlobalObject->vm();
>     JSLockHolder locker(vm);
>     auto scope = DECLARE_CATCH_SCOPE(vm);
> 
>     JSValue value = toJS(lexicalGlobalObject, apiValue);
> 
> ------> Exception happens in the above line
> 
>     auto serializedValue =
> SerializedScriptValue::create(*lexicalGlobalObject, value);
>     if (UNLIKELY(scope.exception())) {
>         if (exception)
>             *exception = toRef(lexicalGlobalObject,
> scope.exception()->value());
>         scope.clearException();
>         return nullptr;
>     }
>     ASSERT(serializedValue);
>     return serializedValue;
> }

Nope, I was wrong - the exception has already occurred the moment you make the DECLARE_CATCH_SCOPE.

This must be because of my patch's attempt to pull the "then" property from the result.
Comment 11 Brady Eidson 2020-01-07 22:24:56 PST
(In reply to Brady Eidson from comment #10)
> (In reply to Brady Eidson from comment #9
> 
> Nope, I was wrong - the exception has already occurred the moment you make
> the DECLARE_CATCH_SCOPE.
> 
> This must be because of my patch's attempt to pull the "then" property from
> the result.

Confirmed. By smattering exception scope checks throughout ScriptController::executeAsynchronousUserAgentScriptInWorld I found it occurs on this line:

    auto thenFunction = result.value().get(&globalObject, thenIdentifier);
Comment 12 Brady Eidson 2020-01-07 22:26:04 PST
(In reply to Brady Eidson from comment #11)
> (In reply to Brady Eidson from comment #10)
> > (In reply to Brady Eidson from comment #9
> > 
> > Nope, I was wrong - the exception has already occurred the moment you make
> > the DECLARE_CATCH_SCOPE.
> > 
> > This must be because of my patch's attempt to pull the "then" property from
> > the result.
> 
> Confirmed. By smattering exception scope checks throughout
> ScriptController::executeAsynchronousUserAgentScriptInWorld I found it
> occurs on this line:
> 
>     auto thenFunction = result.value().get(&globalObject, thenIdentifier);

It appears doing that on a JSNull or JSUndefined throws an exception.

Will try my own fix and make sure Saam takes another look.
Comment 13 Brady Eidson 2020-01-07 22:38:09 PST
Created attachment 387079 [details]
Patch
Comment 14 Brady Eidson 2020-01-07 22:39:12 PST
(In reply to Brady Eidson from comment #13)
> Created attachment 387079 [details]
> Patch

Basically added an "isNullOrUndefined()" check on the value before we try to extract "then" from it.
Comment 15 Saam Barati 2020-01-07 22:53:54 PST
Comment on attachment 387079 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=387079&action=review

> Source/WebCore/bindings/js/ScriptController.cpp:718
> +    auto thenFunction = result.value().get(&globalObject, thenIdentifier);

I think the appropriate check is just to check for exceptions after this (and maybe omit the isUndefinedOrNull, since that'll just fall out from this).
(Even if result.value() isn't null or undefined, this "get" may run arbitrary JS code, and can throw. For. example, when it's a getter).

I think you want to declare a catch scope, and return early (and maybe reject?) if an exception is thrown.

The only other place where an exception might get thrown is your JS function call at the bottom. However, since that's the final thing in this function, you can call "release" on your exception scope before doing the JS call.
Comment 16 Brady Eidson 2020-01-07 23:12:15 PST
(In reply to Saam Barati from comment #15)
> Comment on attachment 387079 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=387079&action=review
> 
> > Source/WebCore/bindings/js/ScriptController.cpp:718
> > +    auto thenFunction = result.value().get(&globalObject, thenIdentifier);
> 
> I think the appropriate check is just to check for exceptions after this
> (and maybe omit the isUndefinedOrNull, since that'll just fall out from
> this).
> (Even if result.value() isn't null or undefined, this "get" may run
> arbitrary JS code, and can throw. For. example, when it's a getter).

Okay, but...

> I think you want to declare a catch scope, and return early (and maybe
> reject?) if an exception is thrown.

No, because it's 100% valid to return null or undefined and pass them back to the UI process.

So if looking up "then" on a real object can run arbitrary JS code and throw an exception, we definitely should pass that back to the UI process as an error.

But looking up "then" on null or undefined should silently fail, and we need to pass back null or undefined to the UI process.

I'm guessing that it's impossible to detect *why* an exception was thrown in the get() call, and therefore I need to prequalify the value beforehand, like in this patch?

Or is there something else I'm missing?
Comment 17 Brady Eidson 2020-01-08 07:29:40 PST
Note: The api-ios failures here are not due to this patch.
Comment 18 Brady Eidson 2020-01-08 11:12:25 PST
Pow-wow'ed with Keith - Should do an isObject check before doing the get (This is what some of the related ES spec algorithms do)
Comment 19 Brady Eidson 2020-01-08 11:21:48 PST
Created attachment 387116 [details]
Patch
Comment 20 Saam Barati 2020-01-08 11:41:02 PST
(In reply to Brady Eidson from comment #16)
> (In reply to Saam Barati from comment #15)
> > Comment on attachment 387079 [details]
> > Patch
> > 
> > View in context:
> > https://bugs.webkit.org/attachment.cgi?id=387079&action=review
> > 
> > > Source/WebCore/bindings/js/ScriptController.cpp:718
> > > +    auto thenFunction = result.value().get(&globalObject, thenIdentifier);
> > 
> > I think the appropriate check is just to check for exceptions after this
> > (and maybe omit the isUndefinedOrNull, since that'll just fall out from
> > this).
> > (Even if result.value() isn't null or undefined, this "get" may run
> > arbitrary JS code, and can throw. For. example, when it's a getter).
> 
> Okay, but...
> 
> > I think you want to declare a catch scope, and return early (and maybe
> > reject?) if an exception is thrown.
> 
> No, because it's 100% valid to return null or undefined and pass them back
> to the UI process.
I see. I didn't realize that.

What Keith suggested to you seems reasonable to me too.

> 
> So if looking up "then" on a real object can run arbitrary JS code and throw
> an exception, we definitely should pass that back to the UI process as an
> error.
> 
> But looking up "then" on null or undefined should silently fail, and we need
> to pass back null or undefined to the UI process.
> 
> I'm guessing that it's impossible to detect *why* an exception was thrown in
> the get() call, and therefore I need to prequalify the value beforehand,
> like in this patch?
> 
> Or is there something else I'm missing?
Comment 21 Saam Barati 2020-01-08 11:41:41 PST
Comment on attachment 387116 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=387116&action=review

> Source/WebCore/bindings/js/ScriptController.cpp:718
> +    auto thenFunction = result.value().get(&globalObject, thenIdentifier);

I think you still want an exception check after this.
Comment 22 Brady Eidson 2020-01-08 12:27:19 PST
(In reply to Saam Barati from comment #21)
> Comment on attachment 387116 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=387116&action=review
> 
> > Source/WebCore/bindings/js/ScriptController.cpp:718
> > +    auto thenFunction = result.value().get(&globalObject, thenIdentifier);
> 
> I think you still want an exception check after this.

As mentioned, the SerializedScriptValue creator handles this.

It would be nicer to catch the native exception, and I'll do it in a followup
Comment 23 WebKit Commit Bot 2020-01-08 13:47:25 PST
Comment on attachment 387116 [details]
Patch

Clearing flags on attachment: 387116

Committed r254222: <https://trac.webkit.org/changeset/254222>
Comment 24 WebKit Commit Bot 2020-01-08 13:47:27 PST
All reviewed patches have been landed.  Closing bug.
Comment 25 Radar WebKit Bug Importer 2020-01-08 13:48:16 PST
<rdar://problem/58420346>
Comment 26 Truitt Savell 2020-01-09 09:01:02 PST
It looks like the changes in https://trac.webkit.org/changeset/254222/webkit

introduced a Timing out API test TestWebKitAPI.AsyncFunction.Promise

https://results.webkit.org/?suite=api-tests&test=TestWebKitAPI.AsyncFunction.Promise

tracked in https://bugs.webkit.org/show_bug.cgi?id=206012
Comment 27 Brady Eidson 2020-01-13 13:26:42 PST
(In reply to Truitt Savell from comment #26)
> It looks like the changes in https://trac.webkit.org/changeset/254222/webkit
> 
> introduced a Timing out API test TestWebKitAPI.AsyncFunction.Promise
> 
> https://results.webkit.org/?suite=api-tests&test=TestWebKitAPI.AsyncFunction.
> Promise
> 
> tracked in https://bugs.webkit.org/show_bug.cgi?id=206012

Yet Another Case™ of EWS not agreeing with the bots.

Will take a look in that other bug.
Comment 28 Jonathan Bedard 2020-01-17 09:28:44 PST
(In reply to Brady Eidson from comment #27)
> (In reply to Truitt Savell from comment #26)
> > It looks like the changes in https://trac.webkit.org/changeset/254222/webkit
> > 
> > introduced a Timing out API test TestWebKitAPI.AsyncFunction.Promise
> > 
> > https://results.webkit.org/?suite=api-tests&test=TestWebKitAPI.AsyncFunction.
> > Promise
> > 
> > tracked in https://bugs.webkit.org/show_bug.cgi?id=206012
> 
> Yet Another Case™ of EWS not agreeing with the bots.
> 
> Will take a look in that other bug.

It looks like it's only on Debug builds, EWS tests Release.