-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proxy should get the handler's property descriptors during construction. #21
Comments
I agree with your analysis and proposal. I think it's worth going this route to allow proxies to remain transparent for inheritance cycle checks, but I don't think it's worth the hassle purely for the gains in performance. ErgonomicsWe should discuss the ergonomics of how hard it is for proxy authors to define handlers that satisfy the above properties. In most proxy definitions that I have written or come across, an object literal is used to define the handler in-line. IIUC, with this proposal, the easiest way to satisfy the properties of a stable trap is to wrap the handler in a call to Overheads?The checking mechanism proposed here does introduce a fair bit of runtime overhead to proxy construction: the constructor now needs to perform all these tests for all traps (which last I checked was about ~12 methods), although some of the tests may be shared among all traps. There's also a non-negligible change in the size in memory of a proxy object: today a proxy needs to store only a pointer to its target and its handler. With this proposal, the proxy may need up to ~12 more pointers to store the cached traps, correct? Coarse-grained vs Fine-grained stability checkI wonder if we can somehow mitigate the two identified overheads by drastically simplifying the construction-time check: instead of trying to check whether each individual trap is stable, we may also test whether the handler and all of its ancestors are frozen. This reduces the checking overhead for stable traps to a (proto-chain-walking) isFrozen test, and reduces the size overhead to a single flag (indicating whether the handler and all of its ancestors are frozen). Given that defining frozen handlers is way more ergonomic than calling |
Ok, on the checking, I see three options:
First, I agree that both of the other options beats the first bullet. If the isFrozen check passes, the others would be guaranteed to pass, so we should allow ourselves to skip them. If there is no interesting use case that would benefit from the third option (do both) then I agree the second (only isFrozen up the chain) is simpler. Oops. Except that isFrozen by itself is not adequate. We also need to ensure that the relevant properties are data properties. On the memory overhead issue, there is an optimization opportunity that is ironically recursive. If an implementation would rather not pay the memory overhead, and it can tell up front that these This double proxy case sounds esoteric, but it is the essence of the double lifting trick. It would be nice if we could have double lifting and this optimization at the same time. |
Indeed, isFrozen is not sufficient, so it seems there is little simplification in trying to leverage 'frozenness'. I still think there is value in trying to limit the amount of checking a proxy needs to do on the handler during proxy construction. In particular, the problem with checking that a property is absent from the handler is that it requires a proto-chain walk, which is not O(1). On the other hand, just testing whether any of the handler trap properties is an own, non-configurable, non-writable data property bound to So, a simpler rule would be as follows: if a handler wants to let a proxy know it need not trap a certain operation, it can do so by explicitly defining the trap to be an own frozen data property set to IMHO this provides a clearer signal, one that will always be visibly present in the code. Especially for |
I probably agree, but uncomfortably. The full optimization is so safe and so close to transparent that it would be nice to allow it without requiring it. Unfortunately, because it is observable, we should not. The discomfort is that normal ways of writing proxy handler will simply omit traps it does not need. It is a shame that we must forgo this significant optimization opportunity for these. |
In my experience, I usually write the handler as an in-line object literal, like so So if I want to benefit from optimized trapping, I already need to change the common code pattern in one way or another. |
Good and valid point! I now agree comfortably ;) |
my two cents:
|
I'm going to go out on a limb and claim that any JS developer that doesn't know what those things mean isn't going to be using Proxy, which is a much more complex API than property descriptors. |
Agree with @ljharb that anyone who does not understand property descriptors is unlikely to directly use proxies successfully anyway. They are likely to make good use of libraries that use proxies internally in order to implement a simpler API. These library authors are the proxy users we should be concerned with. @caridy the optimization we have in mind is similar to your (2). In comparison, your's has interesting pros and cons:
In any case, both our proposal and your alternative (2) have the feature that no new API surface is needed. Good use of descriptors can already provide implementations what they need to optimize. |
Something to be aware of although I'm not really sure of there's any practical use cases is that there was an article that showed that you could even use a The article is located here, hopefully no one has done this in practice, but it's likely at least someone has done it for logging reasons. |
Proxy as handler for another proxy is indeed an important use case. This is the double lifting technique. We carefully constrained the proxy design so this would work well. |
I want to note that potentially it's not only "a bit faster". It can be a lot faster. Consider this case: let p = Object.freeze(Object.create(null));
for (let i = 0; i < n; ++i) {
p = new Proxy(p, p);
} Then, something simple like |
It seems that most of this is concerned with making the |
@bmeurer would your technique be applicable to all VMs, or just v8? If it's not applicable to all, then I'm not sure it addresses the problem in a generic way. |
@ljharb I can only speak for V8. But past experience tells us that important optimizations tend to spread into other JSVMs. For example inline caching (IC) is now in every JSVM. And to optimize proxies you just need to extend the IC machinery a bit and not only check the hidden class of the proxy object it self, but also that of the handler. There are nuances, but in general I'd say this should be applicable everywhere. |
preserve-proxy-transparency.pdf Attached is the slideshow I was preparing in order to present this as a proposal to tc39. It stops at the point where I realized this proposal cannot possibly work, so it is now withdrawn. The attached slideshow is for historical interest only. The sense in which it cannot work is that a transparent membrane cannot avoid trapping on |
Why must the proxies in a membrane trap on
In the initial conditions,
In order for these initial conditions to be a correct membrane configuration, a [[GetPrototypeOf]] on xp in these conditions must return yp. In the scenarios considered by this proposal, xp's handler would not have a In the first step, the current execution sets x's [[Prototype]] to z, such as by In the next step, the code with access to d calls In order to write a correct membrane, xp's handler must have a However, if a [[GetPrototypeOf]] on xp returns an answer atomically, based on the membrane's knowledge at the time (as would happen in the absence of a |
@erights Summarizing your argument for my own understanding:
|
@tvcutsem Yes. Well put. One additional part of my thinking: The main use case for proxies, and the use case for which we invented proxies and weakmaps, is membranes. Only the membrane use of proxies can approximate transparency anyway. So if proxies cannot opt-out, then as a transparency repair proposal, this proposal is rather useless. The arguments about possible efficiency payoffs are not affected by this new observation. |
Historical mistake
We made a mistake in the Proxy design (@tvcutsem agrees): The proxy does a
[[Get]]
on the handler's trap properties each time it wants to trap. Rather, the proxy should have treated the handler object much like an options object, doing all those[[Get]]
s up front and remembering these methods internally.Either way, any user can of course create a taxonomy of partial customizations, using inheritance or copying between partially or fully populated options/handler objects as they wish. This shift would have both pros and cons:
Cons:
this
-argument?Pros:
[[Get]]
potentially being a bit faster.Cycle checking
Let us say that a non-proxy inherits simply if its
[[GetPrototype]]
behavior is known to always immediately return its[[Prototype]]
or nothing, and do nothing else. An object whose[[GetPrototype]]
behavior is not known to do so inherits exotically. All non-exotic objects inherit simply. Only exotic objects can inherits exotically, but most exotic objects inherit simply. These definitions do not care about the behavior of an object's[[SetPrototype]]
trap.A proxy inherits simply iff its target inherits simply and its handler can never provide a
getPrototype
trap. If the traps were gotten from the handler only once at proxy construction time, this would be a useful distinction. Since both handler and target may be proxies, this definition is recursive on the target. If a proxy inherits simply, then its[[GetPrototype]]
behavior either returns the leaf target's[[Prototype]]
, or nothing if any proxy in the target chain has been revoked. If a proxy inherits simply, then its[[GetPrototype]]
behavior does nothing else.With these definitions, we can change the cycle check to reliably prohibit an inheritance cycle among objects that inherit simply. The cycle check would operate atomically and with no side effects. It would cause no traps. A proxy that wishes to be truly transparent would need to inherit simply, which would not be a burden for almost all uses of proxies. A proxy that inherits exotically thereby opts out of full transparency. The cycle check can sometimes be used to reveal the presence of a proxy that inherits exotically, but it cannot reveal the presence of a proxy that inherits simply.
All Pros and no Cons
Fortunately, because of the invariants, a different change to the spec will give us all the pros, none of the cons, and will likely not break any existing code. We propose to have the proxy inspect the handler at proxy construction time. For a given handler and a given trap name, if any of the following conditions hold:
[[GetOwnPropertyDescriptor]]
on a that trap name of that handler reveals a non-configurable non-writable data property whose value isundefined
.[[GetOwnPropertyDescriptor]]
reveals that the property is absent.[[IsExtensible]]
reveals that the handler may not vary in is answers to[[GetPrototype]]
.[[GetPrototype]]
on the handler reveals that it inherits either from nothing or from an object that satisfies these same constraints,then we know that a
[[Get]]
of that trap name on that handler will never return a trapping function. Under these circumstances, the proxy should remember that this trap is absent and not do those[[Get]]
s at trapping time. In the case where the handler is a proxy (or possible other exotic objects), this has an observable difference: Doing the[[Get]]
could cause other side effects which skipping the[[Get]]
does not have. But in neither case could these side effects have caused the[[Get]]
to return a trapping function. The absence of these effects under these circumstances likely does not break any existing code.Cycle checking again
When the trap name in question is
getPrototype
then the above change becomes more than just a slightly non-transparent optimization. It allows us to distinguish proxies that inherit simply from those that inherit exotically, in order to make exactly the change to cycle detection explained above.Possible remaining optimizations.
For properties that satisfy the relaxed requirement
[[GetOwnPropertyDescriptor]]
on a that trap name of that handler reveals a non-configurable non-writable data property.then we know that whatever the current value reported for that trap name, it will always report the same value or nothing. If there are no revocable proxies in the handler chain, then we even know that it will always report the same value, period. As part of this overall change, we might want to allow the proxy to cache these trap functions as well. This isn't much of an optimization since these traps will still happen. But it would allow the proxy to avoid these
[[Get]]
s at trap time. For high speed operations through simple membranes, even this may be significant.The text was updated successfully, but these errors were encountered: