xref: /aosp_15_r20/external/pigweed/pw_channel/design.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _module-pw_channel-design:
2
3======
4Design
5======
6.. pigweed-module-subpage::
7   :name: pw_channel
8
9.. _module-pw_channel-design-why:
10
11--------------
12Why pw_channel
13--------------
14
15Flow control
16============
17Flow control ensures that:
18
19- Channels do not send more data than the receiver is prepared to handle.
20- Channels do not request more data than they are prepared to handle.
21
22If one node sends more data than the other node can receive, network performance
23degrades. The effects vary, but could include data loss, throughput drops, or
24even crashes in one or both nodes. The ``Channel`` API avoids these issues by
25providing backpressure.
26
27What is backpressure?
28---------------------
29In networking, backpressure is when a node, overwhelmed by inbound traffic,
30exterts pressure on upstream nodes. Communications APIs have to provide ways to
31release the pressure, allowing higher layers to can reduce data rates or drop
32stale or unnecessary packets.
33
34Pitfalls of simplistic backpressure APIs
35----------------------------------------
36Expressing backpressure in an API might seem simple. You could just return a
37status code that indicates that the link isn't ready, and retry when it is.
38
39..  code-block:: cpp
40
41   // Returns `UNAVAILABLE` if the link isn't ready for data yet; retry later.
42   Status Write(std::span<const std::byte> packet);
43
44In practice, this is very difficult to work with:
45
46..  code-block:: cpp
47
48    std::span packet = ExpensiveOperationToPreprarePacket();
49    if (Write(packet).IsUnavailable()) {
50      // ... then what?
51    }
52
53Now what do you do? You did work to prepare the packet, but you can't send it.
54Do you store the packet somewhere and retry? Or, wait a bit and recreate the
55packet, then try to send again? How long do you wait before sending?
56
57The result is inefficient code that is difficult to write correctly.
58
59There are other options: you can add an ``IsReadyToWrite()`` function, and only
60call ``Write`` when that is true. But what if ``IsReadyToWrite()`` becomes false
61while you're preparing the packet? Then, you're back in the same situation.
62Another approach: block until the link is ready for a write. But this means an
63entire thread and its resources are locked up for an arbitrary amount of time.
64
65How pw_channel addresses write-side backpressure
66------------------------------------------------
67When writing into a ``Channel`` instance, the ``Channel`` may provide
68backpressure in several locations:
69
70- :cpp:func:`PendReadyToWrite <pw::channel::AnyChannel::PendReadyToWrite>` --
71  Before writing to a channel, users must check that it is ready to receive
72  writes. If the channel is not ready, the channel will wake up the async task
73  when it becomes ready to accept outbound data.
74- :cpp:func:`GetWriteAllocator <pw::channel::AnyChannel::GetWriteAllocator>` --
75  Once the channel becomes ready to receive writes, the writer must ensure that
76  there is space in an outgoing write buffer for the message they wish to send.
77  If there is not yet enough space, the channel will wake up the async task
78  once there is space again in the future.
79
80Only once these two operations have completed can the writing task may place its
81data into the outgoing buffer and send it into the channel.
82
83How pw_channel addresses read-side backpressure
84-----------------------------------------------
85When reading from a ``Channel`` instance, the consumer of the ``Channel`` data
86exerts backpressure by *not* invoking :cpp:func:`PendRead <pw::channel::AnyChannel::PendRead>`.
87The buffers returned by ``PendRead`` are allocated by the ``Channel`` itself.
88
89Zero-copy
90=========
91It's common to see async IO APIs like this:
92
93..  code-block:: cpp
94
95   Status Read(pw::Function<void(pw::Result<std::span<const std::byte>)> callback);
96
97These APIs suffer from an obvious problem: what is the lifetime of the span
98passed into the callback? Usually, it only lasts for the duration of the
99callback. Users must therefore copy the data into a separate buffer if
100they need it to persist.
101
102Another common structure uses user-provided buffers:
103
104..  code-block:: cpp
105
106   Status ReadIntoProvidedBuffer(std::span<const std::byte> buffer, pw::Function<...> callback);
107
108But this a similar problem: the low-level implementor of the read interface
109must copy data from its source (usually a lower-level protocol buffer or
110a peripheral-associated DMA buffer) into the user-provided buffer. This copy
111is also required when passing between layers of the stack that need to e.g.
112erase headers, perform defragmentation, or otherwise modify the structure
113of the incoming data.
114
115This process requires both runtime overhead due to copying and memory overhead
116due to the need for multiple buffers to hold every message.
117
118``Channel`` avoids this problem by using
119:cpp:class:`MultiBuf <pw::multibuf::MultiBuf>`. The lower layers of the stack
120are responsible for allocating peripheral-compatible buffers that are then
121passed up the stack for the application code to read from or write into.
122``MultiBuf`` allows for fragementation, coalescing, insertion of headers,
123footers etc. without the need for a copy.
124
125Composable
126==========
127Many traditional communications code hard-codes its lower layers, making it
128difficult or impossible to reused application code between e.g. a UART-based
129protocol and an IP-based one. By providing a single standard interface for byte
130and packet streams, ``Channel`` allows communications stacks to be layered on
131top of one another in various fashions without need rewrites or intermediate
132buffering of data.
133
134Asynchronous
135============
136``Channel`` uses ``pw_async2`` to allow an unlimited number of channel IO
137operations without the need for dedicated threads. ``pw_async2``'s
138dispatcher-based structure ensures that work is only done as-needed,
139cancellation and timeouts are built-in and composable, and there is no
140need for deeply-nested callbacks or careful consideration of what
141context a particular callback may be invoked from.
142
143------------------
144Channel attributes
145------------------
146Channels may be reliable, readable, writable, or seekable. A channel may be
147substituted for another as long as it provides at least the same set of
148capabilities; additional capabilities are okay. The channel's data type
149(datagram or byte) implies different read/write semantics, so datagram/byte
150channels cannot be used interchangeably in general.
151
152Using datagram channels as byte channels
153========================================
154For datagram channels, the exact bytes provided to a write call will appear in a
155read call on the other end. A zero-byte datagram write results in a zero-byte
156datagram read, so empty datagrams may convey information.
157
158For byte channels, bytes written may be grouped differently when read. A
159zero-length byte write is meaningless and will not result in a zero-length byte
160read. If a zero-length byte read occurs, it is ignored.
161
162To facilitate simple code reuse, datagram-oriented channels may used as
163byte-oriented channels when appropriate. Calling
164:cpp:func:`Channel::IgnoreDatagramBoundaries` on a datagram channel returns a
165byte channel reference to it. The byte view of the channel is simply the
166concatenation of the contents of the datagrams.
167
168This is only valid if, for the datagram channel:
169
170- datagram boundaries have no significance or meaning,
171- zero-length datagrams are not used to convey information, since they are
172  meaningless for byte channels,
173- short or zero-length writes through the byte API will not result in
174  unacceptable overhead.
175