Action proxies

# David Bruant (13 years ago)

Action proxies were born as a fork to notification proxies [1]. Both were attempts to get rid of invariant checks which have some cost. It's probably too late to bring such a change in the proxy design, but I have given more thoughts to it, so I'll share it, in the hope it'll fuel people's thoughts on proxies. I had issues with potential dangers of action proxies, but they're isolated to the handler author and only in already complex cases. Since proxies are a expert feature, the additional complexity in already complex cases is probably acceptable. Tom mentioned a per-trap-invocation cost [2]. Some ideas can make this cost disappear or so small it becomes acceptable.

In my experience, a lot of traps end with the statement: return Reflect.trap(...trapArgs) It would be nice if this was made implicit. Notification proxies allow for this implicity. It would be nice if action proxies did too.

Proposal

The action is the equivalent of "Reflect.trap(...trapArgs)". It is optional to call it. There is one action function per trap (not per invocation, only per trap type). When called, the "action" performs "Reflect.trap(...trapArgs)", stores the value in a slot and returns the value or throws.

Answers to Mark's questions:

  1. what happens if the trap does not call action? => Exactly the same thing than with notification proxies: an implicit return Reflect.trap(...trapArgs)

  2. what happens if the trap calls action more than once? => The best thing I've come up with is to make the function stateful (keep in mind that it's a theorical model, I'll talk optimizations below) and have one slot per trap invocation. Calling action fills the slot with the return value, the end of trap invocation empties the slot to use it as termination value (return value or boolean). This slot semantics is necessary anyway for action proxies (to remember the values to return in case of nested proxies). So calling twice just changes the slot value. Calling the action outside of a related trap invocation timeframe throws (no slot to fill in)

  3. do actions ever return anything, in case the trap wants to pay attention to it? => yes, the return value of Reflect.trap(...trapArgs)

  4. are there any return values from the trap that the proxy would pay attention to? => No. The return value is ignored.

  5. if the original operation performed by action throws, should the action still just return to the trap normally? => No, forward the thrown exception.

  6. what happens if the trap throws rather that returning? => The error thrown is forwarded to the caller, regardless of whether

action has been called.

Stateful per-trap function and abusive authority

What if malicious code gets all action functions and call them maliciously? In order to be malicious, the code would have to call the function in the middle of a trap invocation. The effect of the trap is Reflect.trap(...trapArgs) (not even change the return value) which was planned to be done implicitly or explicitly anyway. The attack case is when action had been called, modification was performed on the target and action wasn't planned to be called after the modification (and the attacker does call it within the invocation timeframe). Arguably, this is so subtle, harmless and easy to protect against that stateful actions can't be considered as abusive authority.

Optimization opportunities

Since the calling the action is optional, before-traps won't even call it, won't even mention it, so the trap-invocation-slot semantics can be bypassed and the cost of this kind of action proxy is equivalent to notification proxies.

Conclusion

This type of action proxy is sort of the fusion between notification proxies and original action proxies. By design, they remove the need for invariant checks. Their cost is one function per trap and the slot-per-trap-invocation semantics which will be ignored if the action isn't called explicitly. For the handler author, there is an additional action argument for each trap which is a bit boilerplate-y, but you only need it if you call it.

I feel it could work. Too late?

David

[1] esdiscuss/2012-December/026774 [2] esdiscuss/2012-December/026779

# Mark S. Miller (13 years ago)

have you seen tvcutsem/harmony-reflect/tree/master/notification ? AFAICT, this provides the same flexibility as action proxies with hardly any more mechanism and overhead than bare notification proxies. The key is that, if the pre trap returns a callable, the proxy calls that callable after the action as a post trap. No need to reify an action thunk, ever, though in exchange the pre trap must often allocate the post trap it returns.

Is there any remaining advantage of action proxies over this?

# David Bruant (13 years ago)

Le 03/02/2013 01:59, Mark S. Miller a écrit :

Hi David, have you seen tvcutsem/harmony-reflect/tree/master/notification ?

I remember seeing the announcement, but I must have forgotten about it. My bad :-s

AFAICT, this provides the same flexibility as action proxies with hardly any more mechanism and overhead than bare notification proxies. The key is that, if the pre trap returns a callable, the proxy calls that callable after the action as a post trap. No need to reify an action thunk, ever, though in exchange the pre trap must often allocate the post trap it returns.

Either allocate or keep the post-trap around for reuse, but yes. On the post trap of getOwnPropertyDescriptor and keys, I would pass an iterator as argument, because it is not sure the post-trap will actually look at the result, so no absolute need to re-allocate the array. Or maybe wrap the array in a (built-in) readonly proxy (throws on writing traps). Speaking of iterator, if the enumerate pretrap returns an iterator with an editable "next" method, can the post-trap modify the next method? Maybe there is some necessary wrapping in that case too.

Is there any remaining advantage of action proxies over this?

Trap names don't start with "on"? :-) I don't think the "on" is absolutely necessary, but that's more of a style issue. Otherwise, I don't think so. Unless I'm overlooking something, I think there is the following equivalence:

 // action proxy:
 trap: function(){
     // pre-trap code
     action();
     // post-trap code
 }

 // notification proxy:
 trap: function(){
     // pre-trap code
     return () =>{
         // post-trap code
     }
 }

The post-trap code is optional in the former part equivalently to the return statement in the latter.

This does indeed get rid of invariant checks while guaranteeing the invariants anyway and apparently not losing expressiveness. Wow.

Was this discussed during the January TC39 meeting? Do notification proxies have a chance to replace direct proxies or is it too late? In the case it would be too late, could "throw ForwardToTarget" be considered?

# Mark S. Miller (13 years ago)

On Sun, Feb 3, 2013 at 7:22 AM, David Bruant <bruant.d at gmail.com> wrote:

[...] This does indeed get rid of invariant checks while guaranteeing the invariants anyway and apparently not losing expressiveness. Wow.

;)

Was this discussed during the January TC39 meeting? Do notification proxies have a chance to replace direct proxies or is it too late? In the case it would be too late, could "throw ForwardToTarget" be considered?

I mentioned at the January meeting that we'll be experimenting with these new notification proxies, to see if they cover all the motivating use cases adequately. I'm increasingly hopeful, but have nothing to report yet. If they do, then at the March meeting I will propose that we do not include direct proxies in ES6. Since it is too late to introduce as radical a change as notification proxies into ES6, I would propose that proxies as a whole get postponed till ES7.

We'll all be sad to see proxies wait. But given how much better notification proxies seem to be, if they work out, it would be a terrible shame to standardize the wrong proxies in ES6 just because they're ready and sorely needed. Of course, as with Object.observe, implementors are free to ship things ahead of formal standardization. And notification proxies are vastly simpler to implement correctly than direct proxies.

# Tom Van Cutsem (13 years ago)

In short, I don't think action proxies bring anything more to the table than notification proxies and are not worth pursuing separately. Replies to more detailed points below:

2013/2/3 David Bruant <bruant.d at gmail.com>

Le 03/02/2013 01:59, Mark S. Miller a écrit :

Hi David, have you seen tvcutsem/**

harmony-reflect/tree/master/**notificationtvcutsem/harmony-reflect/tree/master/notification?

I remember seeing the announcement, but I must have forgotten about it. My bad :-s

AFAICT, this provides the same flexibility as action proxies with hardly

any more mechanism and overhead than bare notification proxies. The key is that, if the pre trap returns a callable, the proxy calls that callable after the action as a post trap. No need to reify an action thunk, ever, though in exchange the pre trap must often allocate the post trap it returns.

Either allocate or keep the post-trap around for reuse, but yes. On the post trap of getOwnPropertyDescriptor and keys, I would pass an iterator as argument, because it is not sure the post-trap will actually look at the result, so no absolute need to re-allocate the array. Or maybe wrap the array in a (built-in) readonly proxy (throws on writing traps).

I think these are implementation-level optimizations that shouldn't necessarily be exposed in the API.

Also, the above API is not yet up-to-date w.r.t. the latest agreement on how to deal with traps that return multiple property names. Allen has specced an "ownKeys" trap that would return an iterator. The getOwnPropertyNames trap, on the other hand, can continue to return an array since Object.getOwnPropertyNames must still return an array anyway.

Speaking of iterator, if the enumerate pretrap returns an iterator with an editable "next" method, can the post-trap modify the next method? Maybe there is some necessary wrapping in that case too.

The enumerate pre-trap cannot return an iterator (its return value is ignored). Presumably you meant to say: if the result of forwarding enumerate() is an iterator, and we pass that iterator to the post-trap, can the post-trap mess around with the iterator? The answer is yes: the post-trap could e.g. fully exhaust the iterator, such that the proxy's client actually sees 0 keys. I think this is fine for property enumeration. getOwnPropertyNames would be the reliable way to query an object for its own property names.

Is there any remaining advantage of action proxies over this?

Trap names don't start with "on"? :-) I don't think the "on" is absolutely necessary, but that's more of a style issue.

I think the "on"-prefix is actually pretty important. It signals to the proxy writer that the trap is a callback whose return value will be ignored.

Otherwise, I don't think so. Unless I'm overlooking something, I think there is the following equivalence:

// action proxy:
trap: function(){
    // pre-trap code
    action();
    // post-trap code
}

// notification proxy:
trap: function(){
    // pre-trap code
    return () =>{
        // post-trap code
    }
}

Correct. Note that if no post-trap is needed, there is no allocation overhead in the case of notification proxies. That's a net win compared to action proxies. Action proxies would require sophisticated optimizations to obtain the same benefits. It's better not to count on that.

The post-trap code is optional in the former part equivalently to the return statement in the latter.

This does indeed get rid of invariant checks while guaranteeing the invariants anyway and apparently not losing expressiveness. Wow.

Was this discussed during the January TC39 meeting? Do notification proxies have a chance to replace direct proxies or is it too late? In the case it would be too late, could "throw ForwardToTarget" be considered?

I wasn't present but I think notification proxies were discussed briefly. We need to work on an implementation of membranes for notification proxies to see if they can fully supplant proxies.

# Brendan Eich (13 years ago)

If notification proxies require allocation per trap activation, that's a fatal flaw in my view.

# Tom Van Cutsem (13 years ago)

2013/2/4 Brendan Eich <brendan at mozilla.com>

If notification proxies require allocation per trap activation, that's a fatal flaw in my view.

Did you mean to say action proxies? Action proxies do require allocation per trap activation, and I also considered this a fatal flaw.

Notification proxies require allocation of a post-trap when they need to do something after the operation was performed on the target. The post-trap could be cached and reused, but only if the post-processing is independent of the specific arguments passed to the intercepted operation.

# David Bruant (13 years ago)

Le 04/02/2013 18:51, Brendan Eich a écrit :

If notification proxies require allocation per trap activation, that's a fatal flaw in my view.

I assume you mean allocation of trap return values and will discuss that, if you mean something else, please expand.

Before stating anything, let's compare notification proxies with direct proxies.

No post-trap case:

 // direct proxies
 trap(...args){
     // pretrap code
     return Reflect.trap(...args)
 }

 //notif proxies
 trap(...args){
     // pretrap code
 }

In both case, the same return result needs to be allocated and passed to whoever the trap caller was.

With post-trap case:

 // direct proxies
 trap(...args){
     // pretrap code
     var ret = Reflect.trap(...args)
     // post-trap code
     return ret;
 }

 // notif proxies
 trap(...args){
     // pretrap code
     return () => {
         // post-trap code
     }
 }

In the last snippet, the engine has to store the result of Reflect.trap(...args) internally, but that's not worse than storing it in a variable as it would currently be the case. I think that having this storage internal may even open the door to optimizations that would be harder if not impossible to achieve with current proxies. Thinking about it more, since the posttrap can't modify the return value, it can be seen like a "finally" block. So return-value related allocation characteristics of a notif proxy with a post trap are comparable to the allocation characteristics of:

 function f(){
     try{
         return 'https://www.youtube.com/watch?v=08WeoqWilRQ'
     }
     finally{
         console.log('finally')
     }
 }
# Brendan Eich (13 years ago)

Tom Van Cutsem wrote:

2013/2/4 Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>>

If notification proxies require allocation per trap activation,
that's a fatal flaw in my view.

Did you mean to say action proxies? Action proxies do require allocation per trap activation, and I also considered this a fatal flaw.

I was replying to Mark, so meant notification proxies.

Notification proxies require allocation of a post-trap when they need to do something after the operation was performed on the target.

Yes, and (just to respond to Mark's somewhat premature "maybe notification proxies will defer proxies from ES6") that seems like a fatal flaw, in spite of the post-trap condition.

The post-trap could be cached and reused, but only if the post-processing is independent of the specific arguments passed to the intercepted operation.

Yup.

# Brendan Eich (13 years ago)

David Bruant wrote:

// notif proxies
trap(...args){
    // pretrap code
    return () => {
        // post-trap code
    }
}

In the last snippet, the engine has to store the result of Reflect.trap(...args) internally, but that's not worse than storing it in a variable as it would currently be the case. I think that having this storage internal may even open the door to optimizations that would be harder if not impossible to achieve with current proxies.

Maybe, but this is irrelevant to the allocation of the arrow-function closure.

Thinking about it more, since the posttrap can't modify the return value, it can be seen like a "finally" block. So return-value related allocation characteristics of a notif proxy with a post trap are comparable to the allocation characteristics of:

function f(){
    try{
        return 'https://www.youtube.com/watch?v=08WeoqWilRQ'
    }
    finally{
        console.log('finally')
    }
}

No, that's not a closure. It's not free either, but there's no closure allocation.

# David Bruant (13 years ago)

Le 04/02/2013 19:57, Tom Van Cutsem a écrit :

The post-trap could be cached and reused, but only if the post-processing is independent of the specific arguments passed to the intercepted operation.

Is there any harm in passing the trap arguments to the post-trap function additionally to the result?

# Mark S. Miller (13 years ago)

On Mon, Feb 4, 2013 at 1:36 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tom Van Cutsem wrote:

2013/2/4 Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>>

If notification proxies require allocation per trap activation,
that's a fatal flaw in my view.

Did you mean to say action proxies? Action proxies do require allocation per trap activation, and I also considered this a fatal flaw.

I was replying to Mark, so meant notification proxies.

Notification proxies require allocation of a post-trap when they need to

do something after the operation was performed on the target.

Yes, and (just to respond to Mark's somewhat premature "maybe notification proxies will defer proxies from ES6") that seems like a fatal flaw, in spite of the post-trap condition.

Saying that a maybe is premature seems a bit much.

In any case, you may be right that this is a fatal flaw. You're making a performance-based argument, and it is certainly premature one way or the other to predict how these relative costs will balance out. Let's wait till we have more data.

# David Bruant (13 years ago)

Le 04/02/2013 22:41, David Bruant a écrit :

Le 04/02/2013 19:57, Tom Van Cutsem a écrit :

The post-trap could be cached and reused, but only if the post-processing is independent of the specific arguments passed to the intercepted operation. Is there any harm in passing the trap arguments to the post-trap function additionally to the result?

I've played with post-traps a bit. A place I would naturally store the post-trap to cache it is the handler.

Assuming trap arguments are passed to the post-trap, another idea is to have pre and post traps. It duplicates the number of elements in a handler, but not the size of the code (or marginally assuming pre/post traps are longer than the boilerplate). Having a post-trap would still be an opt-in, but the protocol to get it would be "callable handler.[[Get]]" instead of the current "callable pretrap-return". One allocation per handler would become the natural default (while the current natural default is a function literal as return value).

# Brendan Eich (13 years ago)

Mark S. Miller wrote:

On Mon, Feb 4, 2013 at 1:36 PM, Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>> wrote:

Tom Van Cutsem wrote:

    2013/2/4 Brendan Eich <brendan at mozilla.com
    <mailto:brendan at mozilla.com> <mailto:brendan at mozilla.com
    <mailto:brendan at mozilla.com>>>


        If notification proxies require allocation per trap
    activation,
        that's a fatal flaw in my view.


    Did you mean to say action proxies? Action proxies do require
    allocation per trap activation, and I also considered this a
    fatal flaw.


I was replying to Mark, so meant notification proxies.


    Notification proxies require allocation of a post-trap when
    they need to do something after the operation was performed on
    the target.


Yes, and (just to respond to Mark's somewhat premature "maybe
notification proxies will defer proxies from ES6") that seems like
a fatal flaw, in spite of the post-trap condition.

Saying that a maybe is premature seems a bit much.

I think you're getting ahead of consensus in a "what if?" way, which is fine (like the Marvel Comics "What If? Dr. Doom became Spider-Man!" ;-).

But fair's fair, so I thought it worth giving some countervailing contingent feedback.

In any case, you may be right that this is a fatal flaw. You're making a performance-based argument, and it is certainly premature one way or the other to predict how these relative costs will balance out. Let's wait till we have more data.

We are not going to defer proxies from ES6 in order to implement notification proxies and find they cost too much. We know enough about allocation dwarfing other costs (direct + GC indirect), I bet. Happy to find out more from experiments but since we are just saying "what if?" I will talk back -- and make a wager on the side if you like.

IOW, I argue that while it's ok to speculate, doing so in one direction (in favor of notification proxies) does not mean data must gathered to prove a cost is "too much" in order to not defer proxies from ES6. For some applications, any cost is too much, so no data is needed.

# Tom Van Cutsem (13 years ago)

2013/2/4 David Bruant <bruant.d at gmail.com>

Le 04/02/2013 22:41, David Bruant a écrit :

Le 04/02/2013 19:57, Tom Van Cutsem a écrit :

The post-trap could be cached and reused, but only if the post-processing is independent of the specific arguments passed to the intercepted operation.

Is there any harm in passing the trap arguments to the post-trap function additionally to the result?

I've played with post-traps a bit. A place I would naturally store the post-trap to cache it is the handler.

Assuming trap arguments are passed to the post-trap, another idea is to have pre and post traps. It duplicates the number of elements in a handler, but not the size of the code (or marginally assuming pre/post traps are longer than the boilerplate). Having a post-trap would still be an opt-in, but the protocol to get it would be "callable handler.[[Get]]" instead of the current "callable pretrap-return". One allocation per handler would become the natural default (while the current natural default is a function literal as return value).

I guess this could work. Borrowing naming conventions from Cocoa, you could have an API along the lines of:

willGetOwnPropertyDescriptor(target, ...args) // pre-notification didGetOwnPropertyDescriptor(target, result, ...args) // post-notification etc.

I like the current API better because it allows for a cleaner pairing of pre and post-traps, including the ability to share private intermediate state through closure capture. However, if the allocation cost of the closure is a deal-breaker, the above API would be one way to avoid that cost.

# David Bruant (13 years ago)

Le 05/02/2013 12:20, Tom Van Cutsem a écrit :

2013/2/4 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>

Le 04/02/2013 22:41, David Bruant a écrit :

    Le 04/02/2013 19:57, Tom Van Cutsem a écrit :

        The post-trap could be cached and reused, but only if the
        post-processing is independent of the specific arguments
        passed to the intercepted operation.

    Is there any harm in passing the trap arguments to the
    post-trap function additionally to the result?

I've played with post-traps a bit. A place I would naturally store
the post-trap to cache it is the handler.

Assuming trap arguments are passed to the post-trap, another idea
is to have pre and post traps. It duplicates the number of
elements in a handler, but not the size of the code (or marginally
assuming pre/post traps are longer than the boilerplate). Having a
post-trap would still be an opt-in, but the protocol to get it
would be "callable handler.[[Get]]" instead of the current
"callable pretrap-return". One allocation per handler would become
the natural default (while the current natural default is a
function literal as return value).

I guess this could work. Borrowing naming conventions from Cocoa, you could have an API along the lines of:

willGetOwnPropertyDescriptor(target, ...args) // pre-notification didGetOwnPropertyDescriptor(target, result, ...args) // post-notification etc. <bikeshed>

In most cases, posttraps aren't necessary. It may be a good idea to reflect this asymmetry in the handler API: getOwnPropertyDescriptor(target, ...args) // pre-notification didGetOwnPropertyDescriptor(target, result, ...args) // post-notification

Another idea: var handler = { getOwnPropertyDescriptor: { pre(target, ...args){ // ... }, post(target, ...args){ // ... } }, get(target, ...args){ // ... } }

The "trap" (like getOwnPropertyDescriptor here) can expose 2 parts pre/post (which could as well be will/did). If the trap is callable (like get here), it's only the pretrap.

In an earlier message, I hadn't answered one of your points:

I think the "on"-prefix is actually pretty important. It signals to the proxy writer that the trap is a callback whose return value will be ignored.

That's a distinction we're aware of as people who've followed the evolution of the API, but people discovering proxies will have to read the doc anyway to understand what they can do and how it works. I'm not sure the "on" (or any other) prefix will really help that much. </bikeshed>

That just was my bikeshed-ish opinion. I won't fight if there is disagreement.

I like the current API better because it allows for a cleaner pairing of pre and post-traps, including the ability to share private intermediate state through closure capture.

I have to admit, I'm a bit sad to loose that too. But that's the price to pay to get rid of invariant checks I think. It remains possible for pre/post trap to share info through the handler.

# David Bruant (13 years ago)

Le 04/02/2013 23:11, Brendan Eich a écrit :

Mark S. Miller wrote:

In any case, you may be right that this is a fatal flaw. You're making a performance-based argument, and it is certainly premature one way or the other to predict how these relative costs will balance out. Let's wait till we have more data.

We are not going to defer proxies from ES6 in order to implement notification proxies and find they cost too much. We know enough about allocation dwarfing other costs (direct + GC indirect), I bet. Happy to find out more from experiments but since we are just saying "what if?" I will talk back -- and make a wager on the side if you like.

IOW, I argue that while it's ok to speculate, doing so in one direction (in favor of notification proxies) does not mean data must gathered to prove a cost is "too much" in order to not defer proxies from ES6. For some applications, any cost is too much, so no data is needed.

About the performance argument, I think a performance argument can only be made in comparison with what we have and not in absolute terms. What's at stake with notification proxies is getting rid of invariant checks [1]. For some applications, the cost of invariant check is too much too. The right question for performance isn't "do notification proxies cost?" but "do they cost more than direct proxies? for the main use cases? on average? worst cast?"

Anyway, there are ideas to get rid of the per-invocation allocations on the table, so let's explore them. If they fail, time will come to compare posttrap allocations and invariant checks.

David

[1] harmony:direct_proxies#invariant_enforcement

# Brendan Eich (13 years ago)

David Bruant wrote:

About the performance argument, I think a performance argument can only be made in comparison with what we have and not in absolute terms.

Agreed, and I wrote taking that into account.

What's at stake with notification proxies is getting rid of invariant checks [1]. For some applications, the cost of invariant check is too much too.

Too much for what applications, based on what measure?

See Sam's followup with paper reference. Allocation is always costlier than JIT-optimized hidden-class-indexed fast paths, summing direct and indirect effects.