Skip to content

Commit a0f2978

Browse files
committed
Add 'Readiness' section to Async.md
1 parent 3870cbe commit a0f2978

File tree

2 files changed

+63
-11
lines changed

2 files changed

+63
-11
lines changed

design/mvp/Async.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ summary of the motivation and animated sketch of the design in action.
1919
* [Structured concurrency](#structured-concurrency)
2020
* [Streams and Futures](#streams-and-futures)
2121
* [Waiting](#waiting)
22+
* [Readiness](#readiness)
2223
* [Backpressure](#backpressure)
2324
* [Returning](#returning)
2425
* [Borrows](#borrows)
@@ -459,6 +460,60 @@ tasks to be scheduled before continuing execution of the current task. A task
459460
can yield by either calling [`yield`] or, when using a `callback`, by returning
460461
the Canonical-ABI-defined "yield" code to the event loop.
461462

463+
### Readiness
464+
465+
When passed a non-zero-length buffer, the `stream.read` and `stream.write`
466+
built-ins are "completion-based" (in the style of, e.g., [Overlapped I/O] or
467+
[`io_uring`]) in that they complete only once one or more values have been
468+
copied to or from the memory buffer passed into the operation. Completion-based
469+
I/O avoids intermediate copies and enables a greater degree of concurrency in a
470+
number of cases and thus language producer toolchains should attempt to pass
471+
non-zero-length buffers whenever possible.
472+
473+
Given completion-based `stream.{read,write}` built-ins, "readiness-based" APIs
474+
(e.g., [`select`] or [`epoll`]) can be implemented (e.g., by [wasi-libc]) by
475+
passing an intermedaite non-zero-length memory buffer to `stream.{read,write}`
476+
and signalling readiness once the operation completes. However, this approach
477+
incurs extra copying overhead. To avoid this overhead in a best-effort mannner,
478+
`stream.{read,write}` additionally allow the given buffer length to be zero in
479+
which case "completion" of the `stream.{read,write}` is allowed (but not
480+
required) to wait to complete until the other end is "ready". As the "but not
481+
required" caveat suggests, after a zero-length read or write succeeds, there is
482+
no guarantee that the next non-zero-length `stream.{read,write}` will complete
483+
without blocking (due to any number of practical externalities or because
484+
readiness was simply not possible to implement given an underlying host API).
485+
Thus, a robust implementation of a guest readiness-based API using zero-length
486+
`stream.{read,write}` must fall back to non-zero-length buffering.
487+
488+
TODO...
489+
490+
As an example, `select()` could be implemented in wasi-libc with the following
491+
rough implementation strategy
492+
* When `select()`ing a file descriptor, a zero-length read or write is started
493+
(if one is not already pending) and the stream is added to the waitable set
494+
waited on by `select()`.
495+
* When the zero-length read or write completes, `select()` marks the stream's
496+
file descriptor as "ready" and remembers that zero-length read/write said so.
497+
498+
Based on this rule, to implement a traditional
499+
`O_NONBLOCK` `write()` or `sendmsg()` API, a writer can use a buffering scheme
500+
in which, after `select()` (or a similar API) signals a file descriptor is
501+
ready to write, the next `O_NONBLOCK` `write()`/`sendmsg()` on that file
502+
descriptor copies to an internal buffer and suceeds, issuing an `async`
503+
`stream.write` in the background and waiting for completion before signalling
504+
readiness again. Note that buffering only occurs when streaming between two
505+
components using non-blocking I/O; if either side is the host or a component
506+
using blocking or completion-based I/O, no buffering is necessary. This
507+
buffering is analogous to the buffering performed in kernel memory by a
508+
`pipe()`.
509+
510+
511+
* why not *require* readiness... constraints host, necessary for c2c
512+
* specifically c2c both-readiness case
513+
* trouble, but mostly only wasi-libc
514+
* only overhead in some cases
515+
516+
462517
### Backpressure
463518

464519
Once a component exports functions using the async ABI, multiple concurrent
@@ -1134,6 +1189,10 @@ comes after:
11341189
[FS or GS Segment Base Address]: https://docs.kernel.org/arch/x86/x86_64/fsgs.html
11351190
[Cooperative]: https://en.wikipedia.org/wiki/Cooperative_multitasking
11361191
[Multithreading]: https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)
1192+
[Overlapped I/O]: https://en.wikipedia.org/wiki/Overlapped_I/O
1193+
[`io_uring`]: https://en.wikipedia.org/wiki/Io_uring
1194+
[`select`]: https://pubs.opengroup.org/onlinepubs/007908799/xsh/select.html
1195+
[`epoll`]: https://en.wikipedia.org/wiki/Epoll
11371196

11381197
[AST Explainer]: Explainer.md
11391198
[Lift and Lower Definitions]: Explainer.md#canonical-definitions
@@ -1190,6 +1249,7 @@ comes after:
11901249
[shared-everything-threads]: https://github.com/webAssembly/shared-everything-threads
11911250
[memory64]: https://github.com/webAssembly/memory64
11921251
[wasm-gc]: https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP.md
1252+
[wasi-libc]: https://github.com/WebAssembly/wasi-libc
11931253

11941254
[WASI Preview 3]: https://github.com/WebAssembly/WASI/tree/main/wasip2#looking-forward-to-preview-3
11951255
[`wasi:http/handler.handle`]: https://github.com/WebAssembly/wasi-http/blob/main/wit-0.3.0-draft/handler.wit

design/mvp/CanonicalABI.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,17 +1411,8 @@ was first, the zero-length `write` always completes, leaving the zero-length
14111411
*must* (eventually) follow a completed zero-length `write` with a
14121412
non-zero-length `write` that is allowed to block. This will break the loop,
14131413
notifying the reader end and allowing it to rendezvous with a non-zero-length
1414-
`read` and make progress. Based on this rule, to implement a traditional
1415-
`O_NONBLOCK` `write()` or `sendmsg()` API, a writer can use a buffering scheme
1416-
in which, after `select()` (or a similar API) signals a file descriptor is
1417-
ready to write, the next `O_NONBLOCK` `write()`/`sendmsg()` on that file
1418-
descriptor copies to an internal buffer and suceeds, issuing an `async`
1419-
`stream.write` in the background and waiting for completion before signalling
1420-
readiness again. Note that buffering only occurs when streaming between two
1421-
components using non-blocking I/O; if either side is the host or a component
1422-
using blocking or completion-based I/O, no buffering is necessary. This
1423-
buffering is analogous to the buffering performed in kernel memory by a
1424-
`pipe()`.
1414+
`read` and make progress. See the [Readiness] section in the async explainer
1415+
for more background on purpose of zero-length reads and writes.
14251416

14261417
The two ends of a stream are stored as separate elements in the component
14271418
instance's table and each end has a separate `CopyState` that reflects what
@@ -4486,6 +4477,7 @@ def canon_thread_available_parallelism():
44864477
[Readable or Writable End]: Async.md#streams-and-futures
44874478
[Context-Local Storage]: Async.md#context-local-storage
44884479
[Subtask State Machine]: Async.md#cancellation
4480+
[Readiness]: Async.md#readiness
44894481
[Lazy Lowering]: https://github.com/WebAssembly/component-model/issues/383
44904482

44914483
[Core WebAssembly Embedding]: https://webassembly.github.io/spec/core/appendix/embedding.html

0 commit comments

Comments
 (0)