Skip to content
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

How much latitude should implementations have in looking up transformer.transform()? #691

Closed
ricea opened this issue Mar 9, 2017 · 14 comments

Comments

@ricea
Copy link
Collaborator

ricea commented Mar 9, 2017

Pull request #689 verifies that transformer.transform is looked up in exactly the same way as the reference implementation. But this puts pretty severe restrictions on possible optimisations. For example, new TransformStream() can be optimised to skip Javascript altogether, but new TransformStream({}) cannot. Here are some use cases for a TransformStream with no transform() method:

  1. A TransformStream which adds extra buffering to a pipe.
  2. A TransformStream which adds a header and/or footer to the stream but does nothing else.

I see two ways we could permit optimisation here:

  1. Make the standard ambiguous about when (and how?) the "transform" attribute of transformer is looked up.
  2. Change the reference implementation (and hence the standard) to look up the "transform" attribute in the constructor only.

I think for developers 2. is better than 1. because it will at least be consistent between implementations even if it's different from the way sources and sinks work.

The semantics we provide for sources and sinks are that we treat them exactly like an object, including expected Javascript behaviours like dynamically changing prototypes.

My feeling is that complying with this expectation is important, even though only a tiny proportion of developers may take advantage of the flexibility. On the other hand, falling off a performance cliff because you wrote new TransformStream({}) instead of new TransformStream() is suboptimal. Making identity transforms always be slower just to avoid surprising performance behaviour would also be bad.

Thoughts?

@tschneidereit
Copy link
Contributor

tschneidereit commented Mar 9, 2017 via email

@domenic
Copy link
Member

domenic commented Mar 9, 2017

I'm loathe to make the constructor design inconsistent between the three classes. On the other hand, it seems like a low-risk change to cache all the methods on all the constructors, so if this is important, I think we should do that.

It's a bit weird from a JS perspective IMO. If we're treating these things as methods, the usual syntax for method calling is foo.bar(), not previouslySavedBar.call(foo). But few people will notice.

We don't want to go back to just treating them as functions, as that breaks cases like https://streams.spec.whatwg.org/#example-both.

It would bloat the size of each object a decent bit, e.g. ReadableStreamDefaultController would end up with three more internal slots to store start, pull, and cancel.

BTW I think the way you've stated this as a performance cliff between new TransformStream() and new TransformStream({}) is not emphasizing the right thing. That's a very theoretical performance cliff. But the cliff is a real problem for the more realistic use cases you cite. Just a phrasing thing :).

@tschneidereit
Copy link
Contributor

tschneidereit commented Mar 10, 2017 via email

@domenic
Copy link
Member

domenic commented Mar 10, 2017

Good point about not needing to store start(); you're right.

@ricea
Copy link
Collaborator Author

ricea commented Mar 10, 2017

I feel conflicted about this. An important feature of Streams is being able to plug together standard-shaped pieces and have them work together in a predictable way. If the objects passed to Stream constructors behave differently from Javascript objects in other contexts then we're going against our own design philosophy.

Strawman scenario: some future framework uses bytecode for everything on first load to avoid JS compilation overhead. After first load it lazily loads the real Javascript and dynamically swaps all the bytecode functions for the optimizable Javascript versions. Eventually some users of this framework notice that Stream performance is bad and the fact that byte-coding needs to be disabled for sink/source/transformer methods is independently rediscovered several times before finally making it into the documentation.

My intuition tells me that optimising for undefined transform() is important, but my intuition about optimisation has been wrong before.

@ricea
Copy link
Collaborator Author

ricea commented Mar 10, 2017

Alternative proposal: scrap the transform() fallback and add an explicit identity: true attribute. Transformers can either define a transform() method or set identity: true, but cannot do both (or neither). Like the type attribute on a source, identity is only checked in the constructor and changing it afterwards does nothing.

@domenic
Copy link
Member

domenic commented Mar 10, 2017

If the objects passed to Stream constructors behave differently from Javascript objects in other contexts then we're going against our own design philosophy.

I guess I'm not as conflicted because even though they behave differently, it's really not a big deal in practice. I've never really seen JavaScript code that dynamically changes the methods of an object passed elsewhere and expects that to be respected---even though it's probably the case that doing so would work, since most object-accepting code will use foo.bar() instead of previouslySavedBar.call(foo).

Oh, and also, we use the same pattern for custom elements. Here was the thread with arguments for it at that time: WICG/webcomponents#417

@tschneidereit
Copy link
Contributor

Oh, and also, we use the same pattern for custom elements. Here was the thread with arguments for it at that time: WICG/webcomponents#417

That's a good discussion of the up- and downsides to this, thanks. I don't know if the same is true for custom elements, though I'd assume so, but as I said above the important part for me is that having this check be required would necessitate entering the JS engine in some fashion, even if it doesn't necessarily entail running JS code itself. (Though recommending developers freeze the object or make the transform property non-configurable would also work.)

@ricea
Copy link
Collaborator Author

ricea commented Mar 14, 2017

Oh, and also, we use the same pattern for custom elements. Here was the thread with arguments for it at that time: WICG/webcomponents#417

I didn't know that custom elements was doing this. That negates my concerns. Thank you.

Mark me in favour of "cache method pointers at construction time for ReadableStream, TransformStream and WritableStream". Because this makes no difference in the vast majority of cases, I have no interoperability concerns about changing existing behaviour.

@ricea
Copy link
Collaborator Author

ricea commented Nov 7, 2017

I think we should go directly to #813 (algorithm steps in slots) without doing the method-copying thing as an intermediate step.

@domenic
Copy link
Member

domenic commented Nov 7, 2017

Oh, wow, I didn't realize they solved the same problem. Good point. I'll set aside my half-done branch that does method copying and switch to working on algorithms in slots.

@domenic
Copy link
Member

domenic commented Nov 30, 2017

So, with #857 merged, where does this leave us? Do we still want to pull off the properties at creation time? It seems with the current spec text we still have the problem where new TransformStream({}) is not optimizable, right? So we still want to make this change, although it'll be somewhat easier now.

@ricea
Copy link
Collaborator Author

ricea commented Nov 30, 2017

Yes, I plan to do this soon, probably tomorrow. Aside from providing optimisation opportunities I expect it will be a good simplification for TransformStream.

ricea added a commit to ricea/web-platform-tests that referenced this issue Nov 30, 2017
Stream classes are changing to get methods from the underlying object
during construction rather than when the methods are called.

Change expectations of when method lookup happens.

See whatwg/streams#691 for background.
ricea added a commit to ricea/streams that referenced this issue Nov 30, 2017
Lookup methods on underlyingSource, underlyingSink and transformer in
the constructor. The main benefit is that identity transforms can be
simply implemented without touching Javascript. Other less critical
optimisation opportunities are unlocked for other stream types.

The algorithm construction for used-supplied methods is changed so that
a different algorithm is constructed depending on whether or not the
method was supplied. Two new abstract operations are added:

* GetMethod works like GetV but throws if the value is neither undefined
  nor a function.
* PromiseInvoke works like PromiseInvokeOrNoop but doesn't check for
  undefined.

PromiseInvokeOrNoop is no longer used and has been removed.

Constructors now throw for invalid method parameters, which results in
some test expectation changes. Changing the methods or prototype on the
underlying object after construction no longer does anything. Other than
these edge cases there are no web-developer visible changes.

Fixes whatwg#691.
@ricea
Copy link
Collaborator Author

ricea commented Nov 30, 2017

WIP at #860. The test changes web-platform-tests/wpt#8519 are unexpectedly nice. There are less edge cases. Even though the new semantics are more unusual than the old ones, subjectively they seem less weird.

Sadly no TransformStream tests failed due to the change. I have a separate work in progress to fix that :-)

ricea added a commit to web-platform-tests/wpt that referenced this issue Dec 6, 2017
Stream classes are changing to get methods from the underlying object
during construction rather than when the methods are called.

Change expectations of when method lookup happens.

Includes new tests to verify that TransformStream and
TransformStreamDefaultController have the correct properties, and that
those properties have the correct properties, and that transformer
objects are called in the correct ways, and not called in unexpected ways.

See whatwg/streams#691 for background.
@ricea ricea closed this as completed in #860 Dec 6, 2017
ricea added a commit that referenced this issue Dec 6, 2017
Lookup methods on underlyingSource, underlyingSink and transformer in
the constructor. The main benefit is that identity transforms can be
simply implemented without touching Javascript. Other less critical
optimisation opportunities are unlocked for other stream types.

The algorithm construction for used-supplied methods is changed so that
a different algorithm is constructed depending on whether or not the
method was supplied. PromiseCall() replaces PromiseInvokeOrNoop(); it is
similar but accepts a function argument rather than a string, and
doesn't permit it to be undefined.

CreateAlgorithmFromUnderlyingMethod() is a new operation that
encapsulates the logic to create an algorithm that calls the method or
does nothing depending on whether or not the method is defined or
not. It is used for all user-supplied methods except
transformer.transform(), which has custom fallback behaviour.

In many cases the controller argument must be included in the arguments
to CreateAlgorithmFromUnderlyingMethod(). In order to permit algorithms
to be created before the controller is fully initialised, the creation
of controller objects has moved into the caller of the
SetUpFooStreamBarController().

Constructors now throw for invalid method parameters, which results in
some test expectation changes. Changing the methods or prototype on the
underlying object after construction no longer does anything. Other than
these edge cases there are no web-developer visible changes.

Some SetUpFooController operations were erroneously marked as nothrow in
the standard text and have been fixed, along with callers correctly
annotated with "?".

Fixes #691.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

3 participants