---
date: "2023-02-12T09:51:23Z"
title: 'Review: Compulab Fitlet2'
aliases:
- /s/articles/2023/02/12/fitlet2.html
---

{{< image width="400px" float="right" src="/assets/fitlet2/Fitlet2-stock.png" alt="Fitlet" >}}

A while ago, in June 2021, we were discussing home routers that can keep up with 1G+ internet
connections in the [CommunityRack](https://www.communityrack.org) telegram channel. Of course
at IPng Networks we are fond of the Supermicro Xeon D1518 [[ref]({{< ref "2021-09-21-vpp-7" >}})],
which has a bunch of 10Gbit X522 and 1Gbit i350 and i210 intel NICs, but it does come at a certain
price.

For smaller applications, PC Engines APU6 [[ref]({{< ref "2021-07-19-pcengines-apu6" >}})] is
kind of cool and definitely more affordable. But, in this chat, Patrick offered an alternative,
the [[Fitlet2](https://fit-iot.com/web/products/fitlet2/)] which is a small, passively cooled,
and expandable IoT-esque machine.

Fast forward 18 months, and Patrick decided to sell off his units, so I bought one off of him,
and decided to loadtest it. Considering the pricetag (the unit I will be testing will ship for
around $400), and has the ability to use (1G/SFP) fiber optics, it may be a pretty cool one!

# Executive Summary

**TL/DR: Definitely a cool VPP router, 3x 1Gbit line rate, A- would buy again**

With some care on the VPP configuration (notably RX/TX descriptors), this unit can handle L2XC at
(almost) line rate in both directions (2.94Mpps out a theoretical 2.97Mpps), with one VPP worker
thread, which it not just good, it's _Good Enough&trade;_, at which time there is still plenty of
headroom on the CPU, as the Atom E3950 has 4 cores.

In IPv4 routing, using two VPP worker threads, and 2 RX/TX queues on each NIC, the machine keeps up
with 64 byte traffic in both directions (ie 2.97Mpps), again with compute power to spare, and while
using only two out of four CPU cores on the Atom E3950.

For a $400,- machine that draws close to 11 Watts fully loaded, and sporting 8GB (at a max of 16GB)
this Fitlet2 is a gem: it will easily keep up 3x 1Gbit in a production environment, while carrying
multiple full BGP tables (900K IPv4 and 170K IPv6), with room to spare. _It's a classy little
machine!_

## Detailed findings

{{< image width="250px" float="right" src="/assets/fitlet2/Fitlet2-BottomOpen.png" alt="Fitlet2 Open" >}}

The first thing that I noticed when it arrived is how small it is! The design of the Fitlet2 has a
motherboard with a non-removable Atom E3950 CPU running at 1.6GHz, from the _Goldmont_ series. This
is a notoriously slow/budget CPU, and it comes with 4C/4T, each CPU thread comes with 24kB of L1
and 1MB of L2 cache, and there is no L3 cache on this CPU at all. That would mean performance in
applications like VPP (which try to leverage these caches) will be poorer -- the main question on
my mind is: does the CPU have enough __oompff__ to keep up with the 1G network cards? I'll want this
CPU to be able to handle roughly 4.5Mpps in total, in order for Fitlet2 to count itself amongst the
_wirespeed_ routers.

Looking further, Fitlet2 has one HDMI and one MiniDP port, two USB2 and two USB3 ports, two Intel
i211 NICs with RJ45 port (these are 1Gbit).  There's a helpful MicroSD slot, two LEDs and an audio
in- and output 3.5mm jack. The power button does worry me a little bit, I feel like just brushing
against it may turn the machine off. I do appreciate the cooling situation - the top finned plate
mates with the CPU on the top of the motherboard, and the bottom bracket holds a sizable aluminium
cooling block which further helps dissipate heat, without needing any active cooling. The Fitlet
folks claim this machine can run in environments anywhere between -50C and +112C, which I won't be
doing :)

{{< image width="400px" float="right" src="/assets/fitlet2/Fitlet2+FACET.png" alt="Fitlet2" >}}

Inside, there's a single DDR3 SODIMM slot for memory (the one I have came with 8GB at 1600MT/s) and
a custom, ableit open specification expansion board called a __FACET-Card__ which stands for
**F**unction **A**nd **C**onnectivity **E**xtension **T**-Card, well okay then!  The __FACET__ card
in this little machine sports one extra Intel i210-IS NIC, an M2 for an SSD, and an M2E for a WiFi
port. The NIC is a 1Gbit SFP capable device. You can see its optic cage on the _FACET_ card above,
next to the yellow CMOS / Clock battery.

The whole thing is fed with 12V powerbrick delivering 2A, and a nice touch is that the barrel
connector has a plastic bracket that locks it into the chassis by turning it 90degrees, so it won't
flap around in the breeze and detach. I wish other embedded PCs would ship with those, as I've been
fumbling around in 19" racks that are, let me say, less tightly cable organized, and may or may not
have disconnected the CHIX routeserver at some point in the past. Sorry, Max :)

For the curious, here's a list of interesting details: [[lspci](/assets/fitlet2/lspci.txt)] -
[[dmidecode](/assets/fitlet2/dmidecode.txt)] -
[[likwid-topology](/assets/fitlet2/likwid-topology.txt)] - [[dmesg](/assets/fitlet2/dmesg.txt)].

## Preparing the Fitlet2

First, I grab a USB key and install Debian _Bullseye_ (11.5) on it, using the UEFI installer. After
booting, I carry through the instructions on my [[VPP Production]({{< ref "2021-09-21-vpp-7" >}})]
post. Notably, I create the `dataplane` namespace, run an SSH and SNMP agent there, run
`isolcpus=1-3` so that I can give three worker threads to VPP, but I start off giving it only one (1)
worker thread, because this way I can take a look at what the performance is of a single CPU, before
scaling out to the three (3) threads that this CPU can offer. I also take the defaults for DPDK,
notably allowing the DPDK poll-mode-drivers to take their proposed defaults:

*   **GigabitEthernet1/0/0**: Intel Corporation I211 Gigabit Network Connection (rev 03)
    > rx: queues 1 (max 2), desc 512 (min 32 max 4096 align 8) <br />
    > tx: queues 2 (max 2), desc 512 (min 32 max 4096 align 8)
*   **GigabitEthernet3/0/0**: Intel Corporation I210 Gigabit Fiber Network Connection (rev 03)
    > rx: queues 1 (max 4), desc 512 (min 32 max 4096 align 8) <br />
    > tx: queues 2 (max 4), desc 512 (min 32 max 4096 align 8)

I observe that the i211 NIC allows for a maximum of two (2) RX/TX queues, while the (older!) i210
will allow for four (4) of them. And another thing that I see here is that there are two (2) TX
queues active, but I only have one worker thread, so what gives? This is because there is always a
main thread and a worker thread, and it could be that the main thread needs to / wants to send
traffic out on an interface, so it always attaches to a queue in addition to the worker thread(s).
When exploring new hardware, I find it useful to take a look at the output of a few tactical `show`
commands on the CLI, such as:

**1. What CPU is in this machine?**

```
vpp# show cpu
Model name:               Intel(R) Atom(TM) Processor E3950 @ 1.60GHz
Microarch model (family): [0x6] Goldmont ([0x5c] Apollo Lake) stepping 0x9
Flags:                    sse3 pclmulqdq ssse3 sse41 sse42 rdrand pqe rdseed aes sha invariant_tsc
Base frequency:           1.59 GHz
```

**2. Which devices on the PCI bus, PCIe speed details, and driver?**

```
vpp# show pci
Address      Sock VID:PID     Link Speed    Driver          Product Name   Vital Product Data
0000:01:00.0   0  8086:1539   2.5 GT/s x1   uio_pci_generic
0000:02:00.0   0  8086:1539   2.5 GT/s x1   igb
0000:03:00.0   0  8086:1536   2.5 GT/s x1   uio_pci_generic
```

__Note__: This device at slot `02:00.0` is the second onboard RJ45 i211 NIC. I have used this one
to log in to the Fitlet2 and more easily kill/restart VPP and so on, but I could of course just as
well give it to VPP, in which case I'd have three gigabit interfaces to play with!

**3. What details are known for the physical NICs?**

```
vpp# show hardware GigabitEthernet1/0/0
GigabitEthernet1/0/0               1     up   GigabitEthernet1/0/0
  Link speed: 1 Gbps
  RX Queues:
    queue thread         mode
    0     vpp_wk_0 (1)   polling
  TX Queues:
    TX Hash: [name: hash-eth-l34 priority: 50 description: Hash ethernet L34 headers]
    queue shared thread(s)
    0     no     0
    1     no     1
  Ethernet address 00:01:c0:2a:eb:a8
  Intel e1000
    carrier up full duplex max-frame-size 2048
    flags: admin-up maybe-multiseg tx-offload intel-phdr-cksum rx-ip4-cksum int-supported
    rx: queues 1 (max 2), desc 512 (min 32 max 4096 align 8)
    tx: queues 2 (max 2), desc 512 (min 32 max 4096 align 8)
    pci: device 8086:1539 subsystem 8086:0000 address 0000:01:00.00 numa 0
    max rx packet len: 16383
    promiscuous: unicast off all-multicast on
    vlan offload: strip off filter off qinq off
    rx offload avail:  vlan-strip ipv4-cksum udp-cksum tcp-cksum vlan-filter
                       vlan-extend scatter keep-crc rss-hash
    rx offload active: ipv4-cksum scatter
    tx offload avail:  vlan-insert ipv4-cksum udp-cksum tcp-cksum sctp-cksum
                       tcp-tso multi-segs
    tx offload active: ipv4-cksum udp-cksum tcp-cksum multi-segs
    rss avail:         ipv4-tcp ipv4-udp ipv4 ipv6-tcp-ex ipv6-udp-ex ipv6-tcp
                       ipv6-udp ipv6-ex ipv6
    rss active:        none
    tx burst function: (not available)
    rx burst function: (not available)
```

### Configuring VPP

After this exploratory exercise, I have learned enough about the hardware to be able to take the
Fitlet2 out for a spin.  To configure the VPP instance, I turn to
[[vppcfg](https://github.com/pimvanpelt/vppcfg)], which can take a YAML configuration file
describing the desired VPP configuration, and apply it safely to the running dataplane using the VPP
API. I've written a few more posts on how it does that, notably on its [[syntax]({{< ref "2022-03-27-vppcfg-1" >}})]
and its [[planner]({{< ref "2022-04-02-vppcfg-2" >}})].  A complete
configuration guide on vppcfg can be found
[[here](https://github.com/pimvanpelt/vppcfg/blob/main/docs/config-guide.md)].

```
pim@fitlet:~$ sudo dpkg -i {lib,}vpp*23.06*deb
pim@fitlet:~$ sudo apt install python3-pip
pim@fitlet:~$ sudo pip install vppcfg-0.0.3-py3-none-any.whl
```

### Methodology

#### Method 1: Single CPU Thread Saturation

First I will take VPP out for a spin by creating an L2 Cross Connect where any ethernet frame
received on `Gi1/0/0` will be directly transmitted as-is on `Gi3/0/0` and vice versa. This is a
relatively cheap operation for VPP, as it will not have to do any routing table lookups. The
configuration looks like this:

```
pim@fitlet:~$ cat << EOF > l2xc.yaml
interfaces:
  GigabitEthernet1/0/0:
    mtu: 1500
    l2xc: GigabitEthernet3/0/0
  GigabitEthernet3/0/0:
    mtu: 1500
    l2xc: GigabitEthernet1/0/0
EOF
pim@fitlet:~$ vppcfg plan -c l2xc.yaml
[INFO    ] root.main: Loading configfile l2xc.yaml
[INFO    ] vppcfg.config.valid_config: Configuration validated successfully
[INFO    ] root.main: Configuration is valid
[INFO    ] vppcfg.vppapi.connect: VPP version is 23.06-rc0~35-gaf4046134
comment { vppcfg sync: 10 CLI statement(s) follow }
set interface l2 xconnect GigabitEthernet1/0/0 GigabitEthernet3/0/0
set interface l2 tag-rewrite GigabitEthernet1/0/0 disable
set interface l2 xconnect GigabitEthernet3/0/0 GigabitEthernet1/0/0
set interface l2 tag-rewrite GigabitEthernet3/0/0 disable
set interface mtu 1500 GigabitEthernet1/0/0
set interface mtu 1500 GigabitEthernet3/0/0
set interface mtu packet 1500 GigabitEthernet1/0/0
set interface mtu packet 1500 GigabitEthernet3/0/0
set interface state GigabitEthernet1/0/0 up
set interface state GigabitEthernet3/0/0 up
[INFO    ] vppcfg.reconciler.write: Wrote 11 lines to (stdout)
[INFO    ] root.main: Planning succeeded
```

{{< image width="500px" float="right" src="/assets/fitlet2/l2xc-demo1.png" alt="Fitlet2 L2XC First Try" >}}

After I paste these commands on the CLI, I start T-Rex in L2 stateless mode, and start T-Rex, I can
generate some activity by starting the `bench` profile on port 0 with packets of 64 bytes in size
and with varying IPv4 source and destination addresses _and_ ports:

```
tui>start -f stl/bench.py -m 1.48mpps -p 0
          -t size=64,vm=var2
```

Let me explain a few hilights from the picture to the right. When starting this profile, I
specified 1.48Mpps, which is the maximum amount of packets/second that can be generated on a 1Gbit
link when using 64 byte frames (the smallest permissible ethernet frames). I do this because the
loadtester comes with 10Gbit (and 100Gbit) ports, but the Fitlet2 has only 1Gbit ports. Then, I see
that port0 is indeed transmitting (**Tx pps**) 1.48 Mpps, shown in dark blue. This is about 992 Mbps
on the wire (the **Tx bps L1**), but due to the overhead of ethernet (each 64 byte ethernet frame
needs an additional 20 bytes [[details](https://en.wikipedia.org/wiki/Ethernet_frame)]), so the **Tx
bps L2** is about `64/84 * 992.35 = 756.08` Mbps, which lines up.

Then, after the Fitlet2 tries its best to forward those from its receiving Gi1/0/0 port onto its
transmitting port Gi3/0/0, they are received again by T-Rex on port 1. Here, I can see that the **Rx
pps** is 1.29 Mpps, with an **Rx bps** of 660.49 Mbps (which is the L2 counter), and in bright red
at the top I see the **drop_rate** is about 95.59 Mbps. In other words, the Fitlet2 is _not keeping
up_.

But, after I take a look at the runtime statistics, I see that the CPU isn't very busy at all:

```
vpp# show run
...
Thread 1 vpp_wk_0 (lcore 1)
Time 23.8, 10 sec internal node vector rate 4.30 loops/sec 1638976.68
  vector rates in 1.2908e6, out 1.2908e6, drop 0.0000e0, punt 0.0000e0
             Name              State      Calls    Vectors  Suspends   Clocks  Vectors/Call
GigabitEthernet3/0/0-output   active    6323688   27119700         0   9.14e1          4.29
GigabitEthernet3/0/0-tx       active    6323688   27119700         0   1.79e2          4.29
dpdk-input                   polling   44406936   27119701         0   5.35e2           .61
ethernet-input                active    6323689   27119701         0   1.42e2          4.29
l2-input                      active    6323689   27119701         0   9.94e1          4.29
l2-output                     active    6323689   27119701         0   9.77e1          4.29
```

Very interesting! Notice that the line above says `vector rates in .. out ..` are saying that the
thread is receiving only 1.29Mpps, and it is managing to send all of them out as well. When a VPP
worker is busy, each DPDK call will yield many packets, up to 256 in one call, which means the
amount of "vectors per call" will rise. Here, I see that on average, DPDK is returning an average of
only 0.61 packets each time it polls the NIC, and in each time a bunch of the packets are sent off
into the VPP graph, there is an average of 4.29 packets per loop. If the CPU was the bottleneck, it
would look more like 256 in the Vectors/Call column -- so the **bottleneck must be in the NIC**.

Remember above, when I showed the `show hardware` command output? There's a clue in there. The
Fitlet2 has two onboard i211 NICs and one i210 NIC on the _FACET_ card. Despite the lower number,
the i210 is a bit more advanced
[[datasheet](/assets/fitlet2/i210_ethernet_controller_datasheet-257785.pdf)]. If I reverse the
direction of flow (so receiving on the i210 Gi3/0/0, and transmitting on the i211 Gi1/0/0), things
look a fair bit better:

```
vpp# show run
...
Thread 1 vpp_wk_0 (lcore 1)
Time 12.6, 10 sec internal node vector rate 4.02 loops/sec 853956.73
  vector rates in 1.4799e6, out 1.4799e6, drop 0.0000e0, punt 0.0000e0
             Name              State      Calls    Vectors  Suspends   Clocks  Vectors/Call
GigabitEthernet1/0/0-output   active    4642964   18652932         0   9.34e1          4.02
GigabitEthernet1/0/0-tx       active    4642964   18652420         0   1.73e2          4.02
dpdk-input                   polling   12200880   18652933         0   3.27e2          1.53
ethernet-input                active    4642965   18652933         0   1.54e2          4.02
l2-input                      active    4642964   18652933         0   1.04e2          4.02
l2-output                     active    4642964   18652933         0   1.01e2          4.02
```

Hey, would you look at that! The line up top here shows vector rates of in 1.4799e6 (which is
1.48Mpps) and outbound is the same number. And in this configuration as well, the DPDK node isn't
even reading that many packets, and the graph traversal is on average with 4.02 packets per run,
which means that this CPU can do in excess of 1.48Mpps on one (1) CPU thread. Slick!

So what _is_ the maximum throughput per CPU thread? To show this, I will saturate both ports with
line rate traffic, and see what makes it through the other side. After instructing the T-Rex to
perform the following profile:

```
tui>start -f stl/bench.py -m 1.48mpps -p 0 1 \
          -t size=64,vm=var2
```

T-Rex will faithfully start to send traffic on both ports and expect the same amount back from the
Fitlet2 (the _Device Under Test_ or _DUT_). I can see that from T-Rex port 1->0 all traffic makes
its way back, but from port 0->1 there is a little bit of loss (for the 1.48Mpps sent, only 1.43Mpps
is returned). This is the same phenomenon that I explained above -- the i211 NIC is not quite as
good at eating packets as the i210 NIC is.

Even when doing this though, the (still) single threaded VPP is keeping up just fine, CPU wise:

```
vpp# show run
...
Thread 1 vpp_wk_0 (lcore 1)
Time 13.4, 10 sec internal node vector rate 13.59 loops/sec 122820.33
  vector rates in 2.9599e6, out 2.8834e6, drop 0.0000e0, punt 0.0000e0
             Name              State     Calls    Vectors   Suspends   Clocks  Vectors/Call
GigabitEthernet1/0/0-output   active   1822674   19826616          0   3.69e1         10.88
GigabitEthernet1/0/0-tx       active   1822674   19597360          0   1.51e2         10.75
GigabitEthernet3/0/0-output   active   1823770   19826612          0   4.79e1         10.87
GigabitEthernet3/0/0-tx       active   1823770   19029508          0   1.56e2         10.43
dpdk-input                   polling   1827320   39653228          0   1.62e2         21.70
ethernet-input                active   3646444   39653228          0   7.67e1         10.87
l2-input                      active   1825356   39653228          0   4.96e1         21.72
l2-output                     active   1825356   39653228          0   4.58e1         21.72
```

Here we can see 2.96Mpps received (_vector rates in_) while only 2.88Mpps are transmitted (_vector
rates out_). First off, this lines up perfectly with the reporting of T-Rex in the screenshot above,
and it also shows that one direction loses more packets than the other. We're dropping some 80kpps,
but where did they go? Looking at the statistics counters, which include any packets which had
errors in processing, we learn more:

```
vpp# show err
   Count                  Node                              Reason               Severity
3109141488             l2-output                      L2 output packets            error
3109141488              l2-input                       L2 input packets            error
   9936649      GigabitEthernet1/0/0-tx       Tx packet drops (dpdk tx failure)    error
  32120469      GigabitEthernet3/0/0-tx       Tx packet drops (dpdk tx failure)    error
```

{{< image width="500px" float="right" src="/assets/fitlet2/l2xc-demo2.png" alt="Fitlet2 L2XC Second Try" >}}

Aha! From previous experience I know that when DPDK signals packet drops due to 'tx failure',
that this is often because it's trying to hand off the packet to the NIC, which has a ringbuffer to
collect them while the hardware transmits them onto the wire, and this NIC has run out of slots,
which means the packet has to be dropped and a kitten gets hurt. But, I can raise the number of
RX and TX slots, by setting them in VPP's `startup.conf` file:

```
dpdk {
  dev default {
    num-rx-desc 512   ## default
    num-tx-desc 1024
  }
  no-multi-seg
}
```

And with that simple tweak, I've succeeded in configuring the Fitlet2 in a way that it is capable of
receiving and transmitting 64 byte packets in both directions at (almost) line rate, with **one CPU
thread**.

#### Method 2: Rampup using trex-loadtest.py

For this test, I decide to put the Fitlet2 into L3 mode (up until now it was set up in _L2 Cross
Connect_ mode). To do this, I give the interfaces an IPv4 address and set a route for the loadtest
traffic (which will be coming from `16.0.0.0/8` and going to `48.0.0.0/8`). I will once again look
to `vppcfg` to do this, because manipulating the YAML files like this allow me to easily and reliabily
swap back and forth, letting `vppcfg` do the mundane chore of figuring out what commands to type, in
which order, safely.

From my existing L2XC dataplane configuration, I switch to L3 like so:

```
pim@fitlet:~$ cat << EOF > l3.yaml
interfaces:
  GigabitEthernet1/0/0:
    mtu: 1500
    lcp: e1-0-0
    addresses: [ 100.64.10.1/30 ]
  GigabitEthernet3/0/0:
    mtu: 1500
    lcp: e3-0-0
    addresses: [ 100.64.10.5/30 ]
EOF
pim@fitlet:~$ vppcfg plan -c l3.yaml
[INFO    ] root.main: Loading configfile l3.yaml
[INFO    ] vppcfg.config.valid_config: Configuration validated successfully
[INFO    ] root.main: Configuration is valid
[INFO    ] vppcfg.vppapi.connect: VPP version is 23.06-rc0~35-gaf4046134
comment { vppcfg prune: 2 CLI statement(s) follow }
set interface l3 GigabitEthernet1/0/0
set interface l3 GigabitEthernet3/0/0
comment { vppcfg create: 2 CLI statement(s) follow }
lcp create GigabitEthernet1/0/0 host-if e1-0-0
lcp create GigabitEthernet3/0/0 host-if e3-0-0
comment { vppcfg sync: 2 CLI statement(s) follow }
set interface ip address GigabitEthernet1/0/0 100.64.10.1/30
set interface ip address GigabitEthernet3/0/0 100.64.10.5/30
[INFO    ] vppcfg.reconciler.write: Wrote 9 lines to (stdout)
[INFO    ] root.main: Planning succeeded
```

One small note -- `vppcfg` cannot set routes, and this is by design as the Linux Control Plane is
meant to take care of that. I can either set routes using `ip` in the `dataplane` network namespace,
like so:

```
pim@fitlet:~$ sudo nsenter --net=/var/run/netns/dataplane
root@fitlet:/home/pim# ip route add 16.0.0.0/8 via 100.64.10.2
root@fitlet:/home/pim# ip route add 48.0.0.0/8 via 100.64.10.6
```

Or, alternatively, I can set them directly on VPP in the CLI, interestingly with identical syntax:
```
pim@fitlet:~$ vppctl
vpp# ip route add 16.0.0.0/8 via 100.64.10.2
vpp# ip route add 48.0.0.0/8 via 100.64.10.6
```

The loadtester will run a bunch of profiles (1514b, _imix_, 64b with multiple flows, and 64b with
only one flow), either in unidirectional or bidirectional mode, which gives me a wealth of data to
share:

Loadtest             | 1514b    | imix     | Multi 64b | Single 64b
-------------------- | -------- | -------- | --------- | ----------
***Bidirectional***  | [81.7k (100%)](/assets/fitlet2/fitlet2.bench-var2-1514b-bidirectional.html)    | [327k (100%)](/assets/fitlet2/fitlet2.bench-var2-imix-bidirectional.html)    | [1.48M (100%)](/assets/fitlet2/fitlet2.bench-var2-bidirectional.html)    | [1.43M (98.8%)](/assets/fitlet2/fitlet2.bench-bidirectional.html)
***Unidirectional*** | [73.2k (89.6%)](/assets/fitlet2/fitlet2.bench-var2-1514b-unidirectional.html)   | [255k (78.2%)](/assets/fitlet2/fitlet2.bench-var2-imix-unidirectional.html)   | [1.18M (79.4%)](/assets/fitlet2/fitlet2.bench-var2-unidirectional.html)   | [1.23M (82.7%)](/assets/fitlet2/fitlet2.bench-bidirectional.html)

## Caveats

While all results of the loadtests are navigable [[here](/assets/fitlet2/fitlet2.html)], I will cherrypick
one interesting bundle showing the results of _all_ (bi- and unidirectional) tests:

{{< image src="/assets/fitlet2/loadtest.png" alt="Fitlet2 All Loadtests" >}}

I have to admit I was a bit stumped with the unidirectional loadtests - these
are pushing traffic into the i211 (onboard RJ45) NIC, and out of the i210
(_FACET_ SFP) NIC.  What I found super weird (and can't really explain), is
that the _unidirectional_ load, which in the end serves half the packets/sec,
is __lower__ than the _bidirectional_ load, which was almost perfect dropping
only a little bit of traffic at the very end. A picture says a thousand words -
so here's a graph of all the loadtests, which you can also find by clicking on
the links in the table.

## Appendix

### Generating the data

The JSON files that are emitted by my loadtester script can be fed directly into Michal's
[visualizer](https://github.com/wejn/trex-loadtest-viz) to plot interactive graphs (which I've
done for the table above):

```
DEVICE=Fitlet2

## Loadtest

SERVER=${SERVER:=hvn0.lab.ipng.ch}
TARGET=${TARGET:=l3}
RATE=${RATE:=10} ## % of line
DURATION=${DURATION:=600}
OFFSET=${OFFSET:=10}
PROFILE=${PROFILE:="ipng"}

for DIR in unidirectional bidirectional; do
  for SIZE in 1514 imix 64; do
    [ $DIR == "unidirectional" ] && FLAGS="-u "
    ## Multiple Flows
    ./trex-loadtest -s ${SERVER} ${FLAGS} -p $PROFILE}.py -t "offset=${OFFSET},vm=var2,size=${SIZE}" \
      -rd ${DURATION} -rt ${RATE} -o ${DEVICE}-${TARGET}-${PROFILE}-var2-${SIZE}-${DIR}.json

    [ $SIZE -eq 64 ] && {
      ## Specialcase: Single Flow
      ./trex-loadtest -s ${SERVER} ${FLAGS -p ${PROFILE}.py -t "offset=${OFFSET},size=${SIZE}" \
        -rd ${DURATION} -rt ${RATE} -o ${DEVICE}-${TARGET}-${PROFILE}-${SIZE}-${DIR}.json
    }
  done
done

## Graphs

ruby graph.rb -t "${DEVICE} All Loadtests" ${DEVICE}*.json -o ${DEVICE}.html
ruby graph.rb -t "${DEVICE} Unidirectional Loadtests" ${DEVICE}*unidir*.json \
  -o ${DEVICE}.unidirectional.html
ruby graph.rb -t "${DEVICE} Bidirectional Loadtests" ${DEVICE}*bidir*.json \
  -o ${DEVICE}.bidirectional.html

for i in ${PROFILE}-var2-1514 ${PROFILE}-var2-imix ${PROFILE}-var2-64 ${PROFILE}-64; do
  ruby graph.rb -t "${DEVICE} Unidirectional Loadtests" ${DEVICE}*-${i}*unidirectional.json \
    -o ${DEVICE}.$i-unidirectional.html; done
  ruby graph.rb -t "${DEVICE} Bidirectional Loadtests"  ${DEVICE}*-${i}*bidirectional.json \
    -o ${DEVICE}.$i-bidirectional.html; done
done
```
