Advanced Sequence Size In F´
An advanced sequence is a compiled program, so it can be bigger than the standard sequence binary that encodes the same list of commands. This page gives measured sizes for both, the marginal cost of adding one more command to each, and the two places where a Wasm sequence gets expensive far out of proportion to what it appears to do.
Important
String formatting with format!("{my_variable}") anywhere in a sequence costs roughly 3.4 KB,
which is more than every other sizing decision on this page put together.
See String formatting.
Two budgets
File size is the uplink cost, and it is the number this page compares. It is not the number that decides whether a
sequence loads. On board, a module is sized against the memory the Svc.WasmSequencer instance was configured with,
and fprime-wasm verify reports all of it:
Limits (defaults): memory 8192 B, heap 8 pages, code 256 pages, operand stack 1024 words, page 8192 B
Module Bytes Memory Heap Code Fits
----------------------- ----- ------ ---- ---- ----
baseline 50 512 2 1 ok
cmd_no_args 122 516 2 1 ok
...
example 641 941 2 2 ok
mixed_max 1956 1004 2 6 ok
- Bytes is the uplinked file.
- Memory is declared guest linear memory, which starts at the reserved guest stack (512 B by default) and grows with the sequence's data.
- Heap and Code are interpreter-side pages: the store and the compiled intermediate representation.
A module can be small on the wire and still fail to load if it overruns one of the others, so gate on verify
(--json for CI) rather than on file size alone.
Baseline size
Standard (.bin) |
Wasm (.wasm) |
|
|---|---|---|
| Empty sequence | 15 B | 50 B |
A standard sequence's 15 bytes are an 11-byte header and a 4-byte CRC. A Wasm sequence's 50 bytes are the module scaffolding and an entry point that does nothing. Everything else on this page reads as a marginal cost over these.
Command sizing
One command with constant arguments, nothing else, for each argument shape the dictionary offers. The Wasm column is split into the module's code and data sections, because which one grows is the whole story:
| Arguments | Example command | Standard (.bin) |
Wasm (.wasm) |
Code | Data |
|---|---|---|---|---|---|
| None (opcode only) | CMD_NO_OP |
34 B | 122 B | 19 B | 8 B |
One bool |
SEND_BOOL |
35 B | 126 B | 19 B | 12 B |
| One enum | CHOICE |
38 B | 129 B | 19 B | 15 B |
| One aliased scalar | SEND_ALIAS |
38 B | 129 B | 19 B | 15 B |
| One 64-bit-ranged enum | SEND_WIDE_CHOICE |
42 B | 129 B | 19 B | 15 B |
F32 + F64 |
SEND_FLOATS |
46 B | 131 B | 19 B | 17 B |
| Three mixed scalars | CMD_TEST_CMD_1 |
43 B | 134 B | 19 B | 20 B |
Eight integers, I8 through U64 |
SEND_INTS |
64 B | 155 B | 19 B | 41 B |
Array of 2 bool |
SEND_BOOL_ARRAY |
36 B | 126 B | 19 B | 12 B |
| Array of 2 enums | CHOICES |
42 B | 133 B | 19 B | 19 B |
Array of 3 F32 |
SEND_FLOAT_ARRAY |
46 B | 135 B | 19 B | 21 B |
Array of 5 U32 |
SEND_U32_ARRAY |
54 B | 145 B | 19 B | 31 B |
| 2×2 array of enums | SEND_2D |
50 B | 137 B | 19 B | 23 B |
| Struct of 2 enums | CHOICE_PAIR |
42 B | 133 B | 19 B | 19 B |
| Scalar + struct + scalar | CHOICE_PAIR_WITH_FRIENDS |
44 B | 135 B | 19 B | 21 B |
| Struct of nested arrays and a struct | GLUTTON_OF_CHOICE |
64 B | 155 B | 19 B | 41 B |
| Struct of 10 scalars | SEND_SCALARS |
76 B | 161 B | 19 B | 47 B |
4-character literal, string size 8 |
SEND_TINY_STRING |
40 B | 131 B | 19 B | 17 B |
23-character literal, string size 40 |
CMD_NO_OP_STRING |
59 B | 150 B | 19 B | 36 B |
23-character literal, string size 240 |
LOAD |
59 B | 150 B | 19 B | 36 B |
Size Breakdown
A standard sequence stores one record per command. Every record carries the same fixed 19 bytes, then the serialized arguments on top:
- 1-byte record descriptor
- 8-byte time tag
- 4-byte record size
- 2-byte packet descriptor
- 4-byte opcode (really it's an
FwOpcodeTypewhich could be any type but it's typically 4 bytes)
A Wasm sequence does not encode with fixed records like the standard sequencer does. It has sections with different purposes. The "code" section is the executable instructions that tell the interpreter what to do in order. The "data" section includes bytes to include in the interpreter's memory that the code can point to.
- Compile-time serialization When every argument to a command is a compile-time constant, the whole command is encoded into a fixed byte buffer in the module's data section. In standard sequencing, all commands have constant arguments.
- Code cost does not vary. Every row above costs the same 19 bytes of code, from a bare opcode to a struct of ten scalars. Only the data section grows with the serialized bytes.
This is the property to design around: constant arguments are nearly free. What costs code is computing them at run time. See Runtime arguments.
Incremental sizing
The marginal cost of one more command:
| Added command | Standard (.bin) |
Wasm (.wasm) |
|---|---|---|
| A repeat of a command already in the sequence | +19 B ... +28 B | ~ +3 B |
A distinct Dp (enum), U32 (9 B of arguments) |
+28 B | ~ +22 B |
| A distinct string command (~11-character string) | +32 B | ~ +25 B |
A standard sequence has no notion of a repeat: the tenth identical command costs another full record. A Wasm sequence merges identical constant buffers to one address, so a repeat costs only the instructions to call it again.
Measured totals behind those margins:
| Sequence | Standard (.bin) |
Wasm (.wasm) |
|---|---|---|
8x identical Dp |
239 B | 145 B |
8x distinct Dp |
239 B | 283 B |
16x distinct Dp |
463 B | 463 B |
| 8x distinct string commands | 271 B | 325 B |
Because the Wasm marginal cost per command is lower than the standard one, the two curves cross:
- Around 5 commands if the sequence repeats itself.
- Around 16 commands if every command is distinct.
Past that point the Wasm module is the smaller artifact, and it stays smaller as the sequence grows. For a handful of one-shot commands, a standard sequence is smaller.
For the size of every benchmark sequence, see the size comment CI posts on every fprime-wasm pull
request. It carries the whole table, each row linked to the sequence's source, measured the same way as the
figures on this page.
Runtime arguments
Note
The rest of this document does not compare standard with advanced sequencing as these features are only supported in advanced sequencing.
An argument that is not a compile-time constant cannot be encoded ahead of time. The module links the runtime serializer for that argument's type instead, and pays for it in code:
| Benchmark | Total | Code | What makes it a runtime argument |
|---|---|---|---|
cmd_no_args |
122 B | 19 B | (constant, for reference) |
fallback_scalar |
244 B | 125 B | a scalar read from telemetry |
fallback_string |
266 B | 146 B | a string chosen between two literals |
fallback_struct |
293 B | 173 B | a structure built at run time |
fallback_struct_string |
1094 B | 975 B | a structure with a string member |
fallback_nested_string |
1294 B | 1163 B | an array of strings |
Two things to take from this table. A scalar or a whole structure computed at run time costs a hundred bytes or two. A string inside a struct costs a kilobyte.
Strings
A string argument written as a literal is constant-encoded like any other argument: it costs its own characters and
nothing more, whatever capacity the argument declares (see Command sizing). A literal longer than
its capacity is even truncated at compile time, so it costs only the bytes that will actually fly
(cmd_string_truncated, 135 B).
What is not free is a string the compiler cannot see:
| Benchmark | Total | Why |
|---|---|---|
fallback_string |
266 B | chosen at run time, so the string serializer is linked |
serial_struct |
857 B | a structure sent out a serial port |
fallback_struct_string |
1094 B | a string member inside a structure |
fallback_nested_string |
1294 B | an array of strings |
serial_string |
1614 B | a string serialized out a serial port |
A string nested in a structure or array is a fixed-capacity buffer rather than a &str, so that shape can never be
constant-encoded. If a command or a serial message with a string member is on a sequence's hot path for size,
consider whether the string can be an argument in its own right instead of a struct member, or whether the value can be an enum.
String formatting
Be wary, here be dragons! Using Rust's formatting machinery like print_message!,
the format! macro, or any {} placeholder links core::fmt, which costs about 3.4 KB.
Reading a channel and reporting its value in an event is the pattern that walks into it:
let voltage = Ref.power.BatteryVoltage().0;
// That `{voltage}` is the 3.4 KB
print_message!(EventSeverity::WarningHi, "battery at {voltage} V");
| Sequence | Total |
|---|---|
message(severity, "a literal event message") |
118 B |
print_message!(severity, "dropped {dropped}") |
3518 B |
format!(64, "dropped {dropped}"), then message |
3516 B |
| One float placeholder | 3429 B |
| One integer and one float in the same message | 3904 B |
| Four separate formatted messages | 3759 B |
Subsequent uses of format strings will not require an additional 3.4 KB.
Cheaper alternatives, best first:
- Emit one literal per branch, reporting which case it is rather than the number behind it:
let voltage = Ref.power.BatteryVoltage().0;
if voltage < 21.5 {
message(EventSeverity::WarningHi, "battery low");
} else {
message(EventSeverity::ActivityLo, "battery nominal");
}
- Send the number as a command argument, so the ground decodes and displays it from the dictionary and it is never rendered on board. The argument is not a constant so it costs more than the above (see Runtime arguments):
- Split off string formatting into a separate module. Wasm modules can call each other as long as they are
LOAD-ed in order. Certain pre-uplinked modules may include large expensive abstractions which other sequences can utilize.
Note
A guest event message is truncated to Wasm.GUEST_EVENT_MESSAGE_SIZE (128 B by default), so a long formatted
message pays the code cost and then loses its tail.
Rules of thumb
- Keep command arguments compile-time constants where the logic allows.
- Repeat commands freely. Duplicate commands are reused.
- Prefer literal event messages. Treat the first
{}as a 3.4 KB decision. - Keep strings out of structures and arrays.
- A handful of unconditional commands suits a standard sequence. Reach for an advanced sequence for logic, or past roughly a dozen distinct commands.
- Gate on
fprime-wasm verify, not on file size.