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

StructuredClone allocation failure #936

Closed
annevk opened this issue Mar 24, 2016 · 20 comments
Closed

StructuredClone allocation failure #936

annevk opened this issue Mar 24, 2016 · 20 comments
Assignees
Labels

Comments

@annevk
Copy link
Member

annevk commented Mar 24, 2016

According to @bakulf Mozilla will not deliver a message when an object cannot be deserialized in a particular realm, e.g., ArrayBuffer's allocation exception. Instead, for workers anyway, the exception will go to onerror.

Perhaps we should specify, maybe as part of #935, that some special error message gets delivered instead. E.g., we could set data to the exception thrown or set data to null and expose the exception separately. That way you don't get a global exception you don't know the origin of but instead a message you were expecting, just without the data to a problem.

This is also important for SharedArrayBuffer as I understand it.

@lars-t-hansen, @pizlonator, and @ajklein might have thoughts.

@lars-t-hansen
Copy link

In general reliable detection of errors is an important feature... I don't think SharedArrayBuffer changes much here, actually, even if agents sharing memory can be more closely coupled and hence more vulnerable. In any event, IMO it might be most reasonable for no message to be delivered on the data channel -- "placeholder" data are not great -- but for a message to be delivered on the error channel instead. I suppose that message could contain whatever we serialized, but that seems less important than distinguishing between what was properly transmitted and what was not.

All that said I don't actually know what will be most reasonable for web devs, of which I am not one.

@annevk
Copy link
Member Author

annevk commented Feb 16, 2017

I'm still a little unclear what's best here. Per @domenic implementations end up creating an empty SharedArrayBuffer buffer object when something fails. Is that preferable over data containing a TypeError or some such? Should we also create an empty ArrayBuffer object when we fail to allocate it? (Any other scenarios to think about?)

@domenic
Copy link
Member

domenic commented Feb 16, 2017

/cc @binji

A bit of context at #2361 (comment)

@domenic
Copy link
Member

domenic commented Feb 16, 2017

So it appears SharedArrayBuffer is not yet being shipped so we might have more wiggle room here than I thought.

I see one outstanding question:

  • Should we try to align failure to clone a SharedArrayBuffer due to illegal destination, with failure to allocate a non-shared ArrayBuffer due to insufficient memory, and give them both the same error handling behavior?

It's not clear to me that alignment here is necessary, as they are rather different failures. (In particular, the SAB case is a programming error---you shouldn't have wrote code to send it there---whereas the non-S AB case is an unpredictable runtime error.)

Leaving non-shared ArrayBuffers alone for now, I think we have a few options for SharedArrayBuffers:

  1. Deliver a zero-length SAB to the destination (optionally with an expando property, .notReceived = true?)
    • Pro: any other data still gets cloned, instead of disappearing (permanently, for transfer cases)
    • Pro: there's a least a chance the recipient's SAB-processing code will work fine with zero-length SABs
    • Con: treating errors as zero-length successes might give the wrong semantics
    • Con: this is kind of a strange way to signal errors
  2. Deliver a special error sentinel to the destination (maybe just null)
    • Pro: any other data still gets cloned, instead of disappearing (permanently, for transfer cases)
    • Con: maybe the recipient's code would have worked fine with a zero-length SAB, but this sentinel causes null property access errors and such.
  3. Deliver a zero-length SAB or a special error sentinel, and then also deliver a new "messageerror" event (or just global error event)
    • Same pros/cons as above
    • Pro: allows easier diagnostics and analytics
    • Con: if you want to react at runtime to error conditions, you need to correlate message and messageerror/error events.
    • Con: I kind of doubt people would actually remember to use this?
  4. Don't deliver any event to the destination at all.
    • Pro: no unexpected data to go down weird code paths; destination code might just keep working
    • Con: all cloned and transferred data is lost (permanently for the transfers)
    • Con: hard to debug without developer tools open
  5. Don't deliver a message event to the destination at all, but deliver a new messageerror event (or just global error event).
    • Pro: no unexpected data to go down weird code paths; destination code might just keep working
    • Con: all cloned and transferred data is lost (permanently for the transfers).
    • Pro: allows debugging and analytics
    • Pro: telling people they will always get one of two events makes them more likely to set up both listeners, I think.

I guess I end up being a fan of (1), (2), or (5). In the end I think this is likely a programming error, so maybe fail-hard like (5) is the way to go?

I'd love to get implementer (and ideally user!) feedback on what they think is best.

@annevk
Copy link
Member Author

annevk commented Feb 17, 2017

(Per @lukewagner this will also affect Module objects as they cannot always be deserialized either.)

@AVGP
Copy link

AVGP commented Feb 17, 2017

I am not sure if I understand the concept of an "error sentinel", but I am in favor of (2) or (5).

The way I use postMessage I expect errors. What I would not like is to have to check for zero-length as in most cases I assume a non-zero or even a fixed length, so I'd have to double check.

Also if my receiving code silently "works" the user probably would have expected a result different from what might turn out as a no-op. I'd like to get an error or something I can work with, not a "silent" failure.

Now, optimally it would give me an error, but losing all data in the process isn't the greatest thing to work around, so intuitively I'd err on (2) but (5) is possible to live with and likely considerably cleaner.

@davidflanagan
Copy link

IIRC this was a problem I actually had when working on FirefoxOS (in the context of transferring photos between apps on a low-end mobile device): there was no way to tell when the transfer failed because of memory allocation failure. I don't have a strong opinion on how it is signaled, but I would have liked to have some kind of error signal two or three years ago.

@lukewagner
Copy link

For "deliver a new messageerror event", is this messageerror event delivered to the sender or receiver? The sender is what I would expect and matches viewing this as a programmer error, but I wasn't sure this is what you meant.

@domenic
Copy link
Member

domenic commented Feb 17, 2017

Oh, I meant to the receiver (so that, in (5) at least, the receiver receives exactly one of message or messageerror), although indeed maybe the sender should get one too... hmm.

@davidflanagan
Copy link

I think maybe the issue I'm remembering from my FirefoxOS days was the desire to get an exception when a memory allocation (typically for a large image transfer) failed. It is an issue that basically never effects desktop browsers, but was happening for me on mobile. I wanted an exception (on the sending side) but was getting a silent failure instead.

Note, however, that if we only signal the error to the sender and not the receiver, then to write robust code, every postMessage() will need to be wrapped in try/catch

try { worker.postMessage(m) }
catch(e) { worker.postMessage(OOMSentinel) }

That kind of boilerplate on the sending side will get old really fast. So the idea of having both message and messageerror events sounds great to me. In particular, Domenic's option #5 sounds like it preserves compatibility with existing code that does not listen for messageerror events, and it sounds like it could be made to work for any postMessage(), not just SharedArrayBuffers.

@lars-t-hansen
Copy link

FWIW, I have the impression that with message channels, it's possible to post on a channel before a receiver is attached, and I presume that it is possible for the sender to disappear from the sending end before a receiver is attached (as if the channel itself has been posted as a message on some channel but not yet been delivered - not sure what's possible). If so, it may not be possible to deliver an error to the sender. Even if the sender is still attached the postmessage may have returned long before the message is delivered, so there would need to be some kind of callback to deliver the message.

@annevk
Copy link
Member Author

annevk commented Feb 18, 2017

I was mostly concerned with deserialization failure here, which is on the receiver side. Propagating that back would indeed require sending a message of sorts, which itself could "fail" if the sender got GC'd.

Serialization (which happens on the sender side) can fail too due to OOM. That is already required to simply throw, just like new ArrayBuffer(...) can throw for that reason. It's a little vague at the moment unfortunately due to the standard not clearly separating serialization and deserialization.

@domenic domenic self-assigned this Mar 7, 2017
@domenic
Copy link
Member

domenic commented Mar 8, 2017

It appears our understanding of this issue is not quite correct. We, or at least I, were assuming that UAs allocated the memory for the new ArrayBuffer only when the message was delivered. However, that does not appear to be what happens. Instead, UAs create a copy of the original memory at deserialization time. I assume they then branch on whether the destination is same-process or cross-process:

  • If same-process, they will just use the memory of the copy that was created at serialization time
  • If cross-process, they will need to do a second copy, cross-process

So allocation failure at deserialization time is only an issue cross-process, which is worth realizing.

Importantly, this means that the serialization steps for ArrayBuffers should include making a copy of their bytes right away. You can't just store a pointer.

https://jsbin.com/kohusax/edit?html,console,output

@annevk
Copy link
Member Author

annevk commented Mar 9, 2017

You don't need to copy ArrayBuffer when transfering within a process (agent cluster). And if you transfer with MessageChannel and eventually end up in a different process a copy can be done late.

(I was going to make an argument that we should consider not exposing the process boundaries, but SharedArrayBuffer already does that, so we might as well expose it for ArrayBuffer too.)

@annevk
Copy link
Member Author

annevk commented Mar 9, 2017

Actually, if we want to allow for copy-on-write-or-allocation-of-new-ArrayBuffer, always exposing the failure late might be beneficial.

@domenic
Copy link
Member

domenic commented Mar 9, 2017

I don't think we should force implementations to catch and store allocation errors, only to shuttle them over to the destination, just to support a hypothetical copy-on-write future. It's simpler to allow allocation errors to bubble up where they occur, IMO. If someone does a copy-on-write version then they'll need to throw the error at write time, which could be arbitrarily later (not just at receive time).

My point above was that I at least didn't realize when allocation happened for the simple case like cloning (not tranferring) an array buffer.

domenic added a commit that referenced this issue Mar 20, 2017
This rewrites most of the cloneable and transferable object
infrastructure to better reflect the reality that structured cloning
requires separate serialization and deserialization steps, instead of a
single operation that creates a new object in the target Realm. This is
most evident in the case of MessagePorts, as noted in #2277. It also
allows us to avoid awkward double-cloning with an intermediate
"user-agent defined Realm", as seen in e.g. history.state or IndexedB;
instead we can simply store the serialized form and later deserialize.

Concretely, this:

* Replaces the concept of cloneable objects with serializable objects.
  For platform objects, instead of defining a [[Clone]]() internal
  method, serializable platform objects are annotated with the new
  [Serializable] IDL attribute, and include serialization and
  deserialization steps in their definition.
* Updates the concept of transferable objects. For platform objects,
  instead of defining a [[Transfer]]() internal method, transferable
  platform objects are annotated with the new [Transferable] IDL
  attribute, and include transfer and transfer-receiving steps.
  Additionally, the [[Detached]] internal slot for such objects is now
  managed more automatically.
* Removes the StructuredClone() abstract operation in favor of separate
  StructuredSerialize() and StructuredDeserialize() abstract operations.
  In practice we found that performing a structured clone alone is never
  necessary in specs. It is always either coupled with a transfer list,
  for which StructuredCloneWithTransfer() can be used, or it is best
  expressed as separate serialization and deserialization steps.
* Removes IsTransferable() and Transfer() abstract operations. When
  defined more properly, these became less useful by themselves, so they
  were inlined into the rest of the machinery.
* Introduces StructuredSerialzieWithTransfer() and
  StructuredDeserializeWithTransfer(), which can be used by other
  specifications which need to define their own postMessage()-style
  algorithm but for which StructuredCloneWithTransfer() is not
  sufficient.

Closes #785. Closes #935. Closes #2277. Closes #1162. Sets the stage for
#936 and #2260/#2361.
@annevk
Copy link
Member Author

annevk commented Apr 10, 2017

While working on this in #2518 I realized that if we want to do this properly we should do away with StructuredCloneWithTransfer so we can distinctly notify the sender (for serialization) and receiver (for deserialization) errors. The tight coupling would present a different API otherwise. This impacts window.postMessage(). Does everyone agree with that?

@domenic
Copy link
Member

domenic commented Apr 10, 2017

That makes sense, although it'll have ripple effects on at least service worker; we should check the list of PRs I made.

@annevk
Copy link
Member Author

annevk commented Apr 10, 2017

#2421 (comment) is the list. Only service workers and HTML itself will be affected. (Worklets don't seem to plan to allow transfers, but if they do we'll catch that on time.)

@annevk
Copy link
Member Author

annevk commented Apr 26, 2017

This was meant to be closed by 25a94f6 but unfortunately the commit message mentioned an issue that was off-by-one.

@annevk annevk closed this as completed Apr 26, 2017
alice pushed a commit to alice/html that referenced this issue Jan 8, 2019
This rewrites most of the cloneable and transferable object
infrastructure to better reflect the reality that structured cloning
requires separate serialization and deserialization steps, instead of a
single operation that creates a new object in the target Realm. This is
most evident in the case of MessagePorts, as noted in whatwg#2277. It also
allows us to avoid awkward double-cloning with an intermediate
"user-agent defined Realm", as seen in e.g. history.state or IndexedB;
instead we can simply store the serialized form and later deserialize.

Concretely, this:

* Replaces the concept of cloneable objects with serializable objects.
  For platform objects, instead of defining a [[Clone]]() internal
  method, serializable platform objects are annotated with the new
  [Serializable] IDL attribute, and include serialization and
  deserialization steps in their definition.
* Updates the concept of transferable objects. For platform objects,
  instead of defining a [[Transfer]]() internal method, transferable
  platform objects are annotated with the new [Transferable] IDL
  attribute, and include transfer and transfer-receiving steps.
  Additionally, the [[Detached]] internal slot for such objects is now
  managed more automatically.
* Removes the StructuredClone() abstract operation in favor of separate
  StructuredSerialize() and StructuredDeserialize() abstract operations.
  In practice we found that performing a structured clone alone is never
  necessary in specs. It is always either coupled with a transfer list,
  for which StructuredCloneWithTransfer() can be used, or it is best
  expressed as separate serialization and deserialization steps.
* Removes IsTransferable() and Transfer() abstract operations. When
  defined more properly, these became less useful by themselves, so they
  were inlined into the rest of the machinery.
* Introduces StructuredSerialzieWithTransfer() and
  StructuredDeserializeWithTransfer(), which can be used by other
  specifications which need to define their own postMessage()-style
  algorithm but for which StructuredCloneWithTransfer() is not
  sufficient.

Closes whatwg#785. Closes whatwg#935. Closes whatwg#2277. Closes whatwg#1162. Sets the stage for
whatwg#936 and whatwg#2260/whatwg#2361.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

6 participants