File: chunk_header.md

package info (click to toggle)
iceoryx 2.0.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 11,260 kB
  • sloc: cpp: 94,127; ansic: 1,443; sh: 1,436; python: 1,377; xml: 80; makefile: 61
file content (287 lines) | stat: -rw-r--r-- 16,683 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Chunk-Header

## Summary

Chunks are the transport capsules in iceoryx. They store data from a publisher as payload and are sent to one or more subscriber. Furthermore, there is some meta information which is stored alongside the payload, e.g. the size and the origin of the chunk. This data is composed in the `ChunkHeader` and located at the front of the chunk. Custom meta information can be added to extend the data from the `ChunkHeader` and tailor the chunk to specific use cases. While this makes the chunk layout more complex, this complexity would otherwise be distributed at different locations all over the code base, e.g. in the request/response feature. Additionally, the adjustments for the user-header makes arbitrary alignments of the user-payload trivial to implement.

## Terminology

| Name              | Description                                              |
| :---------------- | :------------------------------------------------------- |
| Chunk             | a piece of memory                                        |
| Chunk-Header      | contains meta information related to the chunk           |
| Chunk-Payload     | the part of the chunk for the payload which is split into user-header and user-payload |
| User-Header       | contains custom meta information, e.g. timestamps        |
| User-Payload      | the user data with custom alignment                      |
| Back-Offset       | offset stored in front of the user-payload to calculate back to the chunk-header (for the most simple case it will overlap with the user-payload offset stored in the Chunk-Header) |

Framing with terminology
```
+=============================================================================+
|                                 Chunk                                       |
+===================+=========================================================+
|                   |                    Chunk-Payload                        |
|   Chunk-Header    +===============+=======+====================+============+
|                   |  User-Header  |   ¦ ᶺ |    User-Payload    |  Padding   |
+===================+===============+=====|=+====================+============+
                                          └ Back-Offset
```

## Design

### Considerations

- it's not uncommon to record chunks for a later replay -> detect incompatibilities on replay
- iceoryx runs on multiple platforms -> endianness of recorded chunks might differ
- for tracing, a chunk should be uniquely identifiable -> store origin and sequence number
- the chunk is located in the shared memory, which will be mapped to arbitrary positions in the address space of various processes -> no absolute pointer are allowed
- in order to reduce complexity, the alignment of the user-header must not exceed the alignment of the `ChunkHeader`

### Solution

#### ChunkHeader Definition
```
class ChunkHeader
{
    uint32_t chunkSize;
    uint8_t chunkHeaderVersion;
    uint8_t reserved{0};
    uint16_t userHeaderId;
    popo::UniquePortId originId; // underlying type = uint64_t
    uint64_t sequenceNumber;
    uint32_t userHeaderSize{0U};
    uint32_t userPayloadSize{0U};
    uint32_t userPayloadAlignment{1U};
    UserPayloadOffset_t userPayloadOffset; // alias to uint32_t
};
```

- **chunkSize** is the size of the whole chunk
- **chunkHeaderVersion** is used to detect incompatibilities for record&replay functionality
- **reserved** is currently not used and set to `0`
- **userHeaderId** is currently not used and set to `NO_USER_HEADER`
- **originId** is the unique identifier of the publisher the chunk was sent from
- **sequenceNumber** is a serial number for the sent chunks
- **userPayloadSize** is the size of the chunk occupied by the user-header
- **userPayloadSize** is the size of the chunk occupied by the user-payload
- **userPayloadAlignment** is the alignment of the chunk occupied by the user-payload
- **userPayloadOffset** is the offset of the user-payload relative to the begin of the chunk

#### Framing

For back calculation from the user-payload pointer to the `ChunkHeader` pointer, the user-payload offset must be accessible from a defined position relative to the user-payload. Lets call this `back-offset`. This is solved by storing the offset in the 4 bytes in front of the user-payload. In case of a simple layout where the `ChunkHeader` is adjacent to the user-payload, this nicely overlaps with the position of `userPayloadOffset` and no memory is wasted. In more complex cases, the offset has to be stored a second time. If the user-payload alignment requires some padding from the header extension, this memory is used to store the offset.

1. No user-header and user-payload alignment doesn't exceed the `ChunkHeader` alignment

```
 sizeof(ChunkHeader)    userPayloadSize
|------------------>|--------------------->|
|                   |                      |
+===================+======================+==================================+
| Chunk-Header  ¦ * |     User-Payload     |  Padding                         |
+===================+======================+==================================+
|                   |                                                         |
| userPayloadOffset |                                                         |
|------------------>|                                                         |
|                                 chunkSize                                   |
|---------------------------------------------------------------------------->|

*) userPayloadOffset from ChunkHeader and back-offset are overlapping
```

2. No user-header and user-payload alignment exceeds the `ChunkHeader` alignment

```
 sizeof(ChunkHeader)             back-offset   userPayloadSize
|------------------>|                  |<---|------------------->|
|                   |                  |    |                    |
+===================+=======================+====================+============+
|   Chunk-Header    |                  ¦    |    User-Payload    |  Padding   |
+===================+=======================+====================+============+
|                                           |                                 |
|          userPayloadOffset                |                                 |
|------------------------------------------>|                                 |
|                                                                             |
|                                 chunkSize                                   |
|---------------------------------------------------------------------------->|
```

Depending on the address of the chunk there is the chance that `ChunkHeader` is the still adjacent to the user-payload. In this case, the framing looks exactly like in case 1.

3. User-Header is used

```
 sizeof(ChunkHeader)             back-offset   userPayloadSize
|------------------>|                  |<---|------------------->|
|                   |                  |    |                    |
+===================+===============+=======+====================+============+
|   Chunk-Header    |  User-Header  |  ¦    |    User-Payload    |  Padding   |
+===================+===============+=======+====================+============+
|                                           |                                 |
|          userPayloadOffset                |                                 |
|------------------------------------------>|                                 |
|                                                                             |
|                                 chunkSize                                   |
|---------------------------------------------------------------------------->|
```

#### User-Payload Offset Calculation

1. No user-header and user-payload alignment doesn't exceed the `ChunkHeader` alignment

```
userPayloadOffset = sizeof(ChunkHeader);
```

2. No user-header and user-payload alignment exceeds the `ChunkHeader` alignment, which means the user-payload is either adjacent to the `ChunkHeader` or there is a padding with at least the size of `ChunkHeader` alignment in front of the user-payload and therefore enough space to store the `back-offset`

```
chunkHeaderEndAddress = addressof(chunkHeader) + sizeof(chunkHeader);
alignedUserPayloadAddress = align(chunkHeaderEndAddress, userPayloadAlignment);
userPayloadOffset = alignedUserPayloadAddress - addressof(chunkHeader);
```

3. User-Header is used

```
chunkHeaderEndAddress = addressof(chunkHeader) + sizeof(chunkHeader) + sizeof(userHeader);
anticipatedBackOffsetAddress = align(chunkHeaderEndAddress, alignof(userPayloadOffset));
unalignedUserPayloadAddress = anticipatedBackOffsetAddress + sizeof(userPayloadOffset);
alignedUserPayloadAddress = align(unalignedUserPayloadAddress, userPayloadAlignment);
userPayloadOffset = alignedUserPayloadAddress - addressof(chunkHeader);
```

#### Required Chunk Size Calculation

In order to fit the user-header and the user-payload into the chunk, a worst case calculation has to be done. We can assume that a chunk is sufficiently aligned for the `ChunkHeader`.

1. No user-header and user-payload alignment doesn't exceed the `ChunkHeader` alignment

```
chunkSize = sizeof(chunkHeader) + userPayloadSize;
```

2. No user-header and user-payload alignment exceeds the `ChunkHeader` alignment

Worst case scenario is when a part of the `ChunkHeader` crosses the user-payload alignment boundary, so that the user-payload must be aligned to the next boundary. The following drawing demonstrates this scenario.

```
                               ┌ back-offset
    +===============+==========|+===============================+
    |  ChunkHeader  |         ¦ᵛ|        User-Payload           |
    +===============+===========+===============================+

⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥ <- ChunkHeader alignment boundaries
⊥               ⊥               ⊥               ⊥ <- user-payload alignment boundaries
    <-----------|--------------->
        pre       user-payload  |------------------------------->
    user-payload   alignment           user-payload size
     alignment
      overhang
```

The following formula is used to calculate the required chunk size.

```
preUserPayloadAlignmentOverhang = sizeof(chunkHeader) - alignof(chunkHeader);
chunkSize = preUserPayloadAlignmentOverhang + userPayloadAlignment + userPayloadSize;
```

3. User-Header is used

Similar to case 2, but in this case it is the `back-offset` which might cross the user-payload alignment boundary.

```
                                               ┌ back-offset with same alignment
                                               | as userPayloadOffset
    +===============+===========+==============|+===============================+
    |  ChunkHeader  | UserHeader|             ¦ᵛ|         User-Payload          |
    +===============+===========+===============+===============================+

                            ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ <- userPayloadOffset alignment boundaries
⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥   ⊥ <- ChunkHeader alignment boundaries
⊥               ⊥               ⊥               ⊥ <- userPayload alignment boundaries
    <---------------------------|--------------->
     pre user-payload alignment   user-payload  |------------------------------->
            overhang               alignment            user-payload size
```

The following formula is used to calculate the required chunk size.

```
chunkHeaderSize = sizeof(chunkHeader) + sizeof(userHeader)
preUserPayloadAlignmentOverhang = align(chunkHeaderSize, alignof(userPayloadOffset));
maxPadding = max(sizeof(userPayloadOffset), userPayloadAlignment);
chunkSize = preUserPayloadAlignmentOverhang + maxPadding + userPayloadSize;
```

#### Accessing Chunk-Header Extension

The `ChunkHeader` has a template method to get user-header. There is a risk to use this wrong by accident, but there is currently no better solution for this.

Since the user-header is always adjacent to the `ChunkHeader`, the formula to get the user-header is:
```
userHeader = addressOf(chunkHeader) + sizeof(chunkHeader);
```

#### ChunkHeader Methods

- `void* userPayload()` returns a pointer to the user-payload
- `template <typename T> T* userHeader()` returns a pointer to the user-header
- `static ChunkHeader* fromUserPayload(const void* const userPayload)` returns a pointer to the `ChunkHeader` associated to the user-payload

#### Integration Into Publisher/Subscriber API

The `Publisher` has additional template parameters for the user-header and user-payload alignment. Alternatively this could also be done with the `allocate` method, but that increases the risk of using it wrong by accident.

Additionally, to fully integrate the user-header into the regular control flow, a pointer to a `ChunkHeaderHook` can be passed to the `Publisher` c'tor.

The `ChunkHeaderHook` is not in the shared memory and has the following virtual methods:
- `virtual void allocateHook(ChunkHeader& chunkHeader)` can be used to initialize the user-header
- `virtual void deliveryHook(ChunkHeader& chunkHeader)` can be used to set members of the user-header, e.g. a timestamp

Furthermore, the `Publisher` and `Subscriber` have access to the `ChunkHeader` and can use the `userHeader()` method to gain access to the user-header.

#### Pitfalls & Testing

- when the user-payload is adjacent to the `ChunkHeader`, it must be ensured that `userPayloadOffset` overlaps with the `back-offset`, which is `sizeof(userPayloadOffset)` in front of the user-payload
- to simplify calculation, it is assumed that the alignment of the user-header doesn't exceed the alignment of the `ChunkHeader`. This has to be enforced with an `assert`

## Open Issues

- a `ChunkHeaderHook` could be used on the publisher side
```
template <typename UserHeader>
class MyChunkHeaderHook : public ChunkHeaderHook
{
  public:
    void deliveryHook(ChunkHeader& chunkHeader) override
    {
        chunkHeader.userHeader<UserHeader>().timestamp = myTimeProvider::now();
    }
};

auto userHeaderHook = MyChunkHeaderHook<MyUserHeader>();
auto pub = iox::popo::Publisher<MyPayload>(serviceDescription, userHeaderHook);
```
        - alternatively, instead of the ChunkHeaderHook class, the publisher could have a method `registerDeliveryHook(std::function<void(ChunkHeader&)>)`
        - allow the user only read access to the `ChunkHeader` and write access to the `UserHeader`
- user defined sequence number
    - this can probably be done by a `ChunkHeaderHook`
    - alternatively, a flag could be introduce into the `ChunkHeader`
- user defined timestamp
    - this should probably be in a user-header
- mempool configuration
    - currently we specify the chunk-payload size and the size of the `ChunkHeader` is added automatically
    - with the new approach, part of the specified chunk-payload size might be used for the user-header
    - part of the specified chunk-payload might also be used as padding for the user-payload alignment
    - the user will continue to specify the chunk-payload; if a user-header or custom user-payload alignment is used, the user needs to take this into account
- is it necessary to store a flag in `ChunkHeader` if a user extension is used?
    - we could maintain a list of known user-header IDs or ranges of IDs similar to `IANA` https://tools.ietf.org/id/draft-cotton-tsvwg-iana-ports-00.html#privateports
    - the IDs could be stored in the `ChunkHeader` and everything with an ID larger than `0xC000` is free to use
    - to make this somewhat save, the `ChunkHeaderHook` must be mandatory with e.g. a `virtual uint16_t getId() = 0;` method which will be called in the `ChunkSender`
        - alternatively, the user-header struct must have a `constexpr uint16_t USER_HEADER_ID`; if it's not present, we could set the ID to `0xC000` which is the first ID free to use
- do we want to store the version of the user extension in the `ChunkHeader`, similarly to `chunkHeaderVersion`
- for record&replay the user-header is totally opaque, which might lead to problems if e.g. a timestamp is stored in the user-header and needs to be updated for the replay
    - if we maintain a list of known user-header IDs and also store the user-header version, a record&replay framework could implement the required conversions
- for record&replay it is necessary to store the alignment of the user-payload; decide if a uint16_t should be used of if just the alignment power should be stored in a uint8_t