Exploring reflection in C++26 (P2996)

I’ve been exploring C++ reflection a bit, using the Bloomberg fork of clang. I’ve yet to get my head fully around the syntax and the implications, but I have an obvious use case: more serialization functionality in libDwm.

In order to play around in ‘production’ code, I needed some feature test macros that are not in the Bloomberg fork in order to conditionally include code only when C++26 features I need are present. P2996 tentatively proposed __cpp_impl_reflection and __cpp_lib_reflection and hence I added those. I also need features from P3491, whose proposed test macro is __cpp_lib_define_static which I also added. Finally, I added __cpp_expansion_statements (feature test macro for P1306).

Technically, __cpp_lib_reflection and __cpp_lib_define_static should be in <meta>, but I added them to the compiler built-ins just because it’s convenient for now.

I’ve run into some minor gotchas when implementing some generic reflection for types not already covered by serialization facilities in libDwm.

As an example… what to do about deserializing instances of structs and classes with const data members. Obviously the const members can’t be cleanly written to without going through a constructor. I haven’t given much thought to it yet, but at first glance it’s a bit of a sore spot.

Another is what to do about members with types such as std::mutex, whose presence inside a class would generally imply mutual exclusion logic within the class. That logic can’t be ascertained by reflection. Do I need to lock the discovered mutex during serialization? During deserialization too? Do I serialize the mutex as a boolean so that deserialization can lock or unlock it, or skip it?

For now, since we have P3394 in the Bloomberg clang fork, I’ve decided that using annotations that allow members of a structure or class to be skipped is a good idea. So in my libDwm experiment, I now have a Dwm::skip_io annotation that will cause Read() and Write() functions to skip over data that is marked with this annotation. For example:

  #include <sstream>
  #include "DwmStreamIO.hh"

  struct Foo {
    [[=Dwm::skip_io]] int  i;
    std::string            s;
  };

  int main(int arc, char *argv[]) {
    Foo  foo1 { 42, "hello" };
    Foo  foo2 { 99, "goodbye" };

    std::stringstream  ss;
    if (Dwm::StreamIO::Write(ss, foo1)) {
      if (Dwm::StreamIO::Read(ss, foo2)) {
        std::cout << foo2.i << ' ' << foo2.s << '\n';
      }
    }
    return 0;
  }

Would produce:

  99 hello

The i member of Foo is neither written nor read, while the s member is written and read. Hence we see that foo2.i remains unchanged after Read(), while foo2.s is changed.

Chasing pointers

A significant issue with trying to use reflection for generic serialization/deserialization: what to do with pointers (raw, std::unique_ptr, std::shared_ptr, et. al.). One of the big issues here is rooted in the fact that for my own use, reflection for serialization/deserialization is desired for structures that come from the operating system environment (POSIX, etc.). Those structures were created for C APIs, not C++ APIs, and pointers within them are always raw. Generically, there’s no way to know what they point to. A single element on the heap? An array on the heap? A static array? In other words, I don’t know if the pointer points to one object or an unbounded number of objects, and hence I don’t know how many objects to write, allocate or read.

Even with allocations performed via operator new[], we don’t have a good means of determining how many entries are in the array.

For now, I deny serialization / deserialization of pointers, with one experimental exception: std::unique_ptr whose deleter is std::default_delete<T> (implying a single object).

Reflection-based code is easier to read

Let’s say we have a need to check that all the types in a std::tuple satisfy a boolean predicate named IsStreamable. Before C++26 reflection, I’d wind up writing something like this:

    template <typename TupleType>
    consteval bool TupleIsStreamable()
    {
      auto  l = []<typename ...ElementType>(ElementType && ...args)
        { return (IsStreamable<ElementType>() && ...); };
      return std::apply(l, std::forward<TupleType>(TupleType()));
    }

While this is far from terrible, it’s not much fun to read (especially for a novice), and is still using relatively modern C++ features (lambda expressions with an explicit template parameter list, and consteval). Not to mention that std::apply is only applicable to tuple-like types (std::tuple, std::pair and std::array). And critically, the above requires default constructibility (note the TupleType() construct call). Because of this final requirement, I often have to resort to the old school technique (add a size_t template parameter and use recursion to check all element types) or the almost as old school technique of using make_index_sequence. Also note that real code would have a requires clause on this function template to verify that TupleType is a tuple-like type, I only left it out here for the sake of brevity.

The equivalent with C++26 reflection:

    template <typename TupleType>    
    consteval bool TupleIsStreamable()
    {
      constexpr const auto tmpl_args =
        define_static_array(template_arguments_of(^^TupleType));
      template for (constexpr auto tmpl_arg : tmpl_args) {
        if constexpr (! IsStreamable<typename[:tmpl_arg:]>()) {
          return false;
        }
      }
      return true;
    }

While this is more lines of code, it’s significantly easier to reason about once you understands the basics of P2996. And without changes (other than renaming the function), this works with some other standard class templates such as std::variant. And with some minor additions, it will work for other standard templates. The example below will also handle std::vector, std::set, std::multiset, std::map, std::multimap, std::unordered_set, std::unordered_multiset, std::unordered_map and std::unordered_multimap. We only check type template parameters, and if ParamCount is non-zero, we only look at the first ParamCount template parameters:

    template <typename TemplateType, size_t ParamCount = 0>    
    consteval bool IsStreamableStdTemplate()
    {
      constexpr const auto tmpl_args =
        define_static_array(template_arguments_of(^^TemplateType));
      size_t  numParams = 0, numTypes = 0, numStreamable = 0;
      template for (constexpr auto tmpl_arg : tmpl_args) {
        ++numParams;
        if (ParamCount && (numParams > ParamCount)) {
          break;
        }
        if (std::meta::is_type(tmpl_arg)) {
          ++numTypes;
          if constexpr (! IsStreamable<typename[:tmpl_arg:]>()) {
            break;
          }
          ++numStreamable;
        }
      }
      return (numTypes == numStreamable);
    }

We can use this with std::vector, std::deque, std::list, std::set, std::multiset, std::unordered_set and std::unordered_multiset by using a ParamCount value of 1. We can use this with std::map, std::multimap, std::unordered_map and std::unordered_multimap by using a ParamCount value of 2. Hence the following would be valid calls to this function (at compile time), just as a list of examples:

    IsStreamableStdTemplate<std::array<int,42>>()
    IsStreamableStdTemplate<std::pair<std::string,int>>()
    IsStreamableStdTemplate<std::set<std::string>>()
    IsStreamableStdTemplate<std::vector<int>,1>()
    IsStreamableStdTemplate<std::deque<int>,1>()
    IsStreamableStdTemplate<std::list<int>,1>()
    IsStreamableStdTemplate<std::map<std::string,int>,2>()
    IsStreamableStdTemplate<std::tuple<int,std::string,bool,char>>()
    IsStreamableStdTemplate<std::variant<int,bool,char,std::string>>()

Early thoughts

I have some initial thoughts about what we’re getting with reflection in C++26.

I think that the decision to approve P2996 for C++26 was a good decision. Having std::meta::info be an opaque type to the user with a bunch of functions (more can be added later) to access it is a good thing. From just the simple examples here, it’s pretty easy to see how it’s going to make some code much easier to understand, even without doing anything fancy (noting that I only used a single splice in each example).

I’ve done my fair share of template metaprogramming over the years. While it’s satisfying to be able to do it right when it’s the best (or only) option, it’s generally much more work than I’d like. And more than once I’ve found the cognitive load to be much higher than I’d like. I probably can’t count the number of times that I’ve gone to modify some template metacode 3 years after writing it, only to find that I underestimated the time to make a change just due to the difficulty of comprehending the code (regardless of the quality of the comments and names). This is especially true when I haven’t recently been deep in this kind of code. It has only gotten worse on this front over time; standard C++ has never been a small, simple language. But despite many features in C++11 and beyond being very useful, the scope of the language is now so big that there are probably no programmers on the planet that know the whole language and standard library. It’s too much for any one person to hold in their head.

One other thought is a caution for what some people are expecting without yet digging into details. For example, what we’re getting is not a panacea for serialization and deserialization. Yes, it will indeed be useful for such activities. But it doesn’t magically solve concurrency issues, nor the legacy issues with pointers, unbounded arrays, arrays decaying to pointers, etc. Even our smart pointers introduce unsolved issues for serialization and deserialization. But it’s the legacy stuff we often need from our operating system that will remain largely unsolved for the foreseeable future, as well as any other place where we are forced to interact with C APIs that were not designed for security, safety, and concurrency.

I’m optimistic that what we’re getting is going to be incredibly useful. While I’ve only scratched the surface in this post, I’m already at the point in my own code changes where I wish we had all the voted-in papers in our compilers today. The increased number of features I’m going to be able to cleanly add to my libraries is significant. The refactoring I’m going to be able to do is also significant (already underway with conditional compilation via feature macros I can later remove). I’m anxiously awaiting official compiler support!

Very old UPS still ticking after minor repair

Below is a picture of the front panel of the 3U UPS I’ve been using for my home office computers since about 1996. The main part of the panel has been repainted twice, but the ‘dwm’ (my initials) was done with a Sharpie when I bought the UPS. At the time, this was just used to distinguish it from my wife’s UPS in the same rack.

It lacks modern communication (it only has an oddly-pinned DB9 connector for communication). This is a drawback that will eventually trigger a replacement. However, I like the 3U form factor, since it makes the thermals more reasonable than a 2U unit without being loud. A single 90mm fan is in the rear, and I don’t think I’ve ever heard it since it’s normally in a rack under my desks. It’s also just been super reliable. Until recently…

Due to the lack of good diagnostic communication, the LEDs on the front panel have to be interpreted using the manual (I have a PDF copy). For the last couple of months, once in a while it would show one of two diagnostics after a power blip. One indicating low battery, one indicating ‘main relay failure’. If I cycled it off and on, the issue would sometimes toggle, sometimes repeat, and usually go away after I reduced the load.

Figuring it was worth a shot, I bought new batteries before digging in. Once I had the old batteries out, sure enough they were completely dead. As in 0V across the series (should be 48V). Hmm, they’re brand-name that I’ve used many times for replacement, that usually last 3.5 to 5 years. These were less than 2 years old.

I taped the new batteries together as usual to make a pack, then installed them. I then powered the UPS and heard arcing inside. Oh no! I unplugged it from the mains.

I removed the UPS from the rack and removed the top. Nothing appeared to be cooked, and no detectable scent of magic smoke. So I turned off the lights in the room, plugged it back into the mains and turned it on. During its self-test… arcing at the connection to the negative end of the series of batteries.

The issue was a loose fast-on connector. The female side had opened up over the years, presumably from the many battery swaps this unit has seen. I squished it with some Knipex pliers to make it tight again and reinstalled. I reran my testing, with no arcing and no overheating when running a 75% load (using a convenient multi-setting portable electric heater I find handy for such things) on and off battery. I didn’t leave it on battery for long since I don’t want to drain the new batteries, but I think it’s good to go.

Here’s a picture of the inside. Doesn’t look too bad for being nearly 30 years old!

I like the fact that the relays have clear housings. I was able to watch all of them operate, with no visible arcing or stickiness. One of my fears was that one of them was welded or intermittently sticking from contact material transfer, which would’ve required removing the main board, finding suitable replacement relays, and a decent amount of desoldering and soldering work.

I suspect this might be the last time I replace the batteries in this unit. I like the Eaton 5P UPS units I have elsewhere, especially since they have USB connections that work with NUT (Network UPS Tools). Replacing this old UPS with a 5P1500RT would not be a bad thing, and it would free up space to put a 1U Raspberry Pi rack in my office cabinet like the one I have in one of the basement racks. It would also homogenize my UPSes, which would be convenient from a monitoring perspective.

Kind of funny that Best Power, the company that made my old UPS, was eventually bought by Eaton, who is my preferred UPS maker today. Best Power made great stuff, and I’ve been very happy with my Eaton UPSes. I bought several Best Power UPSes back in the day based on a recommendation from a coworker (David Bolen at ANS), but this is the only one I still use and it’s been in continuous 24-hour use for decades. There are advantages to the lack of things like a backlit LCD screen: longevity.

mcrover can now check backup status (dumpdates)

I finally finished the small remaining piece of work to have mcrover monitor my backups. Specifically, those using ‘dump’. I have 10 computers using dump for automated nightly backups, over the network. Once in a while I’ll accidentally create a problem via access configuration and not realize that my nightly backups aren’t happening for one of the 10 computers. Since I run mcroverd on all of these computers, it makes sense for it to be able to tell me when a backup hasn’t occurred as scheduled.

Right now mcrover only looks for any level of dump within a configured interval for the given configured devices, not for specific levels. I’ll augment this with the ability to check specific levels later, once I decide on a sane configuration syntax to support it. At the moment I trust my dump driver, I’ve been using it for more than 20 years at this point and have done multiple partial and full recoveries from the dumps. If dumpdates is correct, it just works.

The current configuration syntax in the local stanza of the configuration looks like this:

 
 #--------------------------------------------------------------------  
 #  Check that a 'dump' of the given devices has occurred within the
 #  given time interval.  
 #--------------------------------------------------------------------  
 DUMPS = [ { device = "/dev/gpt/gprootfs"; period = 1d6h; },
           { device = "/dev/gpt/gpusrfs";  period = 1d6h; },
           { device = "/dev/gpt/gpvarfs";  period = 1d6h; } ];
 

This ticks a box for yet another thing I’ve always wanted at home. mcrover only shows me problems, and that’s what I need. I don’t want a dashboard I have to make time to read. Checking my backups here is handy.

Web server OS upgrade woes (FreeBSD 12.4-STABLE to 13.2-STABLE)

Well, I’m still working on it after 4 days, but… my web server is running FreeBSD 13.2-STABLE.

This all started with the desire to have C++20 on all of my general purpose computers. Obviously I don’t get all of C++20 with clang++ or g++, but my web server was the only machine left that didn’t have the parts of C++20 that I need. And my only FreeBSD host not yet running 13.2-STABLE (I have 5 FreeBSD hosts, 2 of which are Raspberry Pis),

The OS upgrade went fine as usual. It’s dealing with all the other stuff that gave me grief. A biggie was that ‘pkg upgrade’ (which I did not intend to run without arguments) updated mysql57 to mysql80, breaking database access for Randy’s photo gallery and my blog.

The php upgrade meant I had to hack some php in Piwigo, update WordPress for my blog, and fix some of my rudimentary php classes.

A boost library upgrade rendered a bunch of my web apps nonfunctional due to incorrect shared library path.

There’s a long list. But a lot of it is just due to procrastination on my part. Making changes to a ‘production’ server at home is always risky, but it gets worse the longer you wait. I mean, I had waited so long on php that the language changed (and broke existing code).

Lesson learned. I’m mostly back up and running, but I still have a to-do list. That includes possibly migrating to different hardware. My web server was built in November of 2012; it’s old. It was specifically built with an i5 2405S CPU to keep power consumption reasonable. And the load is pretty light. It’s in an overkill 4U case with 12 hot swap bays wired to onboard SATA and an HBA in a PCI slot. It also has a Mellanox 10G ethernet card. All I’d really like is modern I/O (NVMe) and 8 or more cores. The 2405S does not have HyperThreading. The current motherboard is MicroATX, so I think it’s reasonable to find a MicroATX board to replace it.

The other option is to buy a replacement for my storage server (dual Xeon L5640) and then use the existing storage server to replace my web server.

Modernizing mcpigdo

My custom garage door opener appliance is running FreeBSD 11.0-ALPHA5 on a Raspberry Pi 2B. It has worked fine for about 8 years now. However, I want to migrate it to FreeBSD 13.2-STABLE and from libDwmAuth to libDwmCredence. And just bring the code current.

The tricky part is that I never quite finished packaging up my device driver for the rotary encoders, and it was somewhat experimental (hence the alpha release of FreeBSD). But as of today it appears I have the rotary encoder device drivers working fine on FreeBSD 13.2-STABLE on a Raspberry Pi 4B. The unit tests for libDwmPi are passing, and I’m adding to them and doing a little cleanup so I’ll be able to maintain it longer-term.

I should note that the reason I went with FreeBSD at the time was pretty simple: the kernel infrastructure for what I needed to do was significantly better versus linux. That may or may not be true today, but for the moment I have no need to look at doing this on linux. The only non-portable code here is my device driver, and it’s relatively tiny (including boilerplate stuff).

Looking back at this project, I should have made a few more hardware-wise. The Raspberry Pi 2B is more than powerful enough for the job, and given that I put it inside a sealed enclosure, the lower power consumption versus a 4B is nice. I’m pretty sure my mom would appreciate one of these, if just by virtue of being able to open her garage doors with her phone or watch. The hardware (the Pi and the HAT I created) has been flawless, and I’ve had literally zero issues despite it being in a garage with no climate control (so it’s seen plenty of -10F and 95F days). It just works.

However, today I could likely do this in a smaller enclosure, thanks to PoE HATs. Unfortunately not the official latest Raspberry Pi PoE HAT because its efficiency is abysmal (generates too much heat). If I bump the Pi to a 4B, I’ll probably stick with a separate PoE splitter (fanless). I’ll need a new one since the power connector has changed.

The arguments for moving to a Pi 4B:

  • future-proofing. If I want to build another one, I’m steered toward the Pi 4B simply because it’s what I can buy and what’s current.
  • faster networking (1G versus 100M)
  • more oomph for compiling C and C++ code locally
  • Some day, the Pi 2B is going to stop working. I’ve no idea when that day might be, but 8 years in Michigan weather seems like it has probably taken a significant toll. On the other hand it could last another 20 years. There are no electrolytic capacitors, I’m using it headless, and none of the USB ports are in use.

The arguments against it:

  • higher power consumption, hence more heat
  • the Pi 2B isn’t dead yet

I think it’s pretty clear that during this process, I should try a Pi 4B. The day will come when I’ll have to abandon the 2B, and I’d rather do it on my timeline. No harm in keeping the 2B in a box while I try a 4B. Other than the PoE splitter, it should be a simple swap. Toward that end, I ordered a 4B with 4G of RAM (I don’t need 8G of RAM here). I still need to order a PoE splitter, but I can probably scavenge an original V2 PoE HAT from one of my other Pis and stack with stacking headers.

Over the weekend I started building FreeBSD 13.2-STABLE (buildworld) on the Pi 2B and as usual hit the limits. The problem is that 1G of RAM isn’t sufficient to utilize the 4 cores. It’s terribly slow even when you can use all 4 cores, but if you start swapping to a microSD card… it takes days for 'make buildworld‘ to finish. And since I have a device driver I’m maintaining for this device, it’s expected that I’ll need to rebuild the kernel somewhat regularly and also build the world occasionally. This is the main motivation for bumping to a Raspberry Pi 4B with 4G of RAM. It is possible it’ll still occasionally start swapping with a ‘make -j4 buildworld‘ , but the cores are faster and I don’t frequently see a single instance of the compiler or llvm-tblgen go over 500M, but it does happen. I think 4G is sufficient to avoid swapping during a full build.

Update Aug 26, 2023: duh, a while after I first created mcpigdo, it became possible to do what I need to do with the rotary encoders from user space. With FreeBSD 13.2, I can configure interrupts on the GPIO pins and be notified via a number of means. I’m going to work on changing my code to not need my device driver. This is good news since I’ve had some problems with my very old device driver despite refactoring, and I don’t have time to keep maintaining it. Moving my code to user space will make it more portable going forward, though it’ll still be FreeBSD-only. It will also allow for more flexibility.

Striping 4 Samsung 990 Pro 2TB on Ubuntu 22.04

On Prime Day I ordered four Samsung 990 Pro 2TB NVMe SSDs to install in my Threadripper machine. I’ve had an unopened Asus Hyper M.2 x16 Gen4 card for years waiting for drives. Just never got around to finishing the plan for my Threadripper machine.

The initial impression is positive. Just for fun, I striped all 4 of them and put an ext4 filesystem on the group, just to grab some out-of-the-box numbers. First up: a simple read test, which yielded more than 24 gigabytes/second. Nice.

dwm@thrip:/hyperx/dwm% fio --name TEST --eta-newline=5s --filename=temp.file --rw=read --size=2g --io_size=10g --blocksize=1024k --ioengine=libaio --fsync=10000 --iodepth=32 --direct=1 --numjobs=1 --runtime=60 --group_reporting

TEST: (g=0): rw=read, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=libaio, iodepth=32
fio-3.28
Starting 1 process
TEST: Laying out IO file (1 file / 2048MiB)

TEST: (groupid=0, jobs=1): err= 0: pid=6333: Wed Jul 19 02:11:19 2023
  read: IOPS=25.2k, BW=24.6GiB/s (26.4GB/s)(10.0GiB/407msec)
    slat (usec): min=27, max=456, avg=38.46, stdev=21.03
    clat (usec): min=174, max=10736, avg=1206.67, stdev=443.19
     lat (usec): min=207, max=11193, avg=1245.21, stdev=460.96
    clat percentiles (usec):
     |  1.00th=[  971],  5.00th=[ 1020], 10.00th=[ 1037], 20.00th=[ 1057],
     | 30.00th=[ 1074], 40.00th=[ 1074], 50.00th=[ 1074], 60.00th=[ 1090],
     | 70.00th=[ 1123], 80.00th=[ 1172], 90.00th=[ 1975], 95.00th=[ 2024],
     | 99.00th=[ 2245], 99.50th=[ 2278], 99.90th=[ 7832], 99.95th=[ 9241],
     | 99.99th=[10421]
  lat (usec)   : 250=0.05%, 500=0.24%, 750=0.26%, 1000=1.76%
  lat (msec)   : 2=88.58%, 4=8.87%, 10=0.21%, 20=0.03%
  cpu          : usr=2.71%, sys=96.06%, ctx=144, majf=0, minf=8205
  IO depths    : 1=0.1%, 2=0.1%, 4=0.2%, 8=0.4%, 16=0.8%, 32=98.5%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
     issued rwts: total=10240,0,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=32

Run status group 0 (all jobs):
   READ: bw=24.6GiB/s (26.4GB/s), 24.6GiB/s-24.6GiB/s (26.4GB/s-26.4GB/s), io=10.0GiB (10.7GB), run=407-407msec

Disk stats (read/write):
    dm-0: ios=151773/272, merge=0/0, ticks=47264/0, in_queue=47264, util=83.33%, aggrios=10240/21, aggrmerge=30720/63, aggrticks=3121/2, aggrin_queue=3124, aggrutil=76.09%
  nvme3n1: ios=10240/21, merge=30720/63, ticks=3146/3, in_queue=3149, util=76.09%
  nvme4n1: ios=10240/21, merge=30720/63, ticks=3653/3, in_queue=3657, util=76.09%
  nvme1n1: ios=10240/21, merge=30720/63, ticks=2504/3, in_queue=2507, util=76.09%
  nvme2n1: ios=10240/21, merge=30720/63, ticks=3182/2, in_queue=3184, util=76.09%

A short while later, I ran a simple write test. Here I see more than 13 gigabytes/second.

dwm@thrip:/hyperx/dwm% fio --name TEST --eta-newline=5s --filename=temp.file --rw=write --size=2g --io_size=10g --blocksize=1024k --ioengine=libaio --fsync=10000 --iodepth=32 --direct=1 --numjobs=1 --runtime=60 --group_reporting
TEST: (g=0): rw=write, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=libaio, iodepth=32
fio-3.28
Starting 1 process

TEST: (groupid=0, jobs=1): err= 0: pid=6682: Wed Jul 19 02:15:31 2023
  write: IOPS=13.7k, BW=13.4GiB/s (14.4GB/s)(10.0GiB/746msec); 0 zone resets
    slat (usec): min=35, max=297, avg=69.19, stdev=14.38
    clat (usec): min=48, max=9779, avg=2242.89, stdev=738.00
     lat (usec): min=105, max=9837, avg=2312.18, stdev=740.08
    clat percentiles (usec):
     |  1.00th=[ 1549],  5.00th=[ 2040], 10.00th=[ 2057], 20.00th=[ 2073],
     | 30.00th=[ 2089], 40.00th=[ 2089], 50.00th=[ 2114], 60.00th=[ 2114],
     | 70.00th=[ 2114], 80.00th=[ 2147], 90.00th=[ 2278], 95.00th=[ 3195],
     | 99.00th=[ 6456], 99.50th=[ 8979], 99.90th=[ 9503], 99.95th=[ 9634],
     | 99.99th=[ 9765]
   bw (  MiB/s): min=13578, max=13578, per=98.92%, avg=13578.00, stdev= 0.00, samples=1
   iops        : min=13578, max=13578, avg=13578.00, stdev= 0.00, samples=1
  lat (usec)   : 50=0.01%, 100=0.02%, 250=0.09%, 500=0.15%, 750=0.16%
  lat (usec)   : 1000=0.19%
  lat (msec)   : 2=1.06%, 4=96.87%, 10=1.46%
  fsync/fdatasync/sync_file_range:
    sync (nsec): min=180, max=180, avg=180.00, stdev= 0.00
    sync percentiles (nsec):
     |  1.00th=[  181],  5.00th=[  181], 10.00th=[  181], 20.00th=[  181],
     | 30.00th=[  181], 40.00th=[  181], 50.00th=[  181], 60.00th=[  181],
     | 70.00th=[  181], 80.00th=[  181], 90.00th=[  181], 95.00th=[  181],
     | 99.00th=[  181], 99.50th=[  181], 99.90th=[  181], 99.95th=[  181],
     | 99.99th=[  181]
  cpu          : usr=36.11%, sys=53.56%, ctx=9861, majf=0, minf=14
  IO depths    : 1=0.1%, 2=0.1%, 4=0.2%, 8=0.4%, 16=0.8%, 32=98.5%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,10240,0,1 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=32

Run status group 0 (all jobs):
  WRITE: bw=13.4GiB/s (14.4GB/s), 13.4GiB/s-13.4GiB/s (14.4GB/s-14.4GB/s), io=10.0GiB (10.7GB), run=746-746msec

Disk stats (read/write):
    dm-0: ios=0/135825, merge=0/0, ticks=0/12864, in_queue=12864, util=87.00%, aggrios=0/10276, aggrmerge=0/30823, aggrticks=0/1124, aggrin_queue=1125, aggrutil=82.63%
  nvme3n1: ios=0/10275, merge=0/30822, ticks=0/1109, in_queue=1109, util=82.20%
  nvme4n1: ios=0/10276, merge=0/30826, ticks=0/1000, in_queue=1001, util=82.63%
  nvme1n1: ios=0/10276, merge=0/30822, ticks=0/1366, in_queue=1367, util=82.63%
  nvme2n1: ios=0/10277, merge=0/30825, ticks=0/1022, in_queue=1023, util=82.20%

It’s worth noting that I don’t consider this configuration a good idea for anything other than scratch space (perhaps for DL training data sets, etc.); 4 striped drives is as my friend Ben put it, risky. I of course trust SSD more than spinning rust here, and historically I’ve had no failures with Samsung SSD drives, but… that’s a hard thing to judge from just my personal observations and from where the industry has gone. I still have Samsung SATA SSDs from the 830 and 840 series, and they’re still healthy. But… we’ve gone from SLC to TLC to QLC to… losing a hair of reliability (and a chunk of warranty) at each step. And I’d be remiss if I didn’t mention Samsung’s botched firmware in the last two generations (980 and 990). In fact I’m annoyed that 2 of the 4 drives I received have old firmware that I’ll need to update.

Raspberry Pi PoE+ is a typo (should be PoS)

Seriously. ‘S’ is adjacent ‘E’ on a QWERTY keyboard.

I knew the official PoE+ HATs were pieces of poop before I bought them. This isn’t news. You don’t have to look hard to find Jeff Geerling’s comments, Martin Rowan’s comments or the many others who’ve complained. I had read them literally years before I purchased.

I decided to buy 4 of them despite the problems, for a specific purpose (4 rack mounted Pi4B 8G, all powered via PoE alone). I’ve had those running for a few days and they’re working. They’re inefficient, but so far they work.

I also ordered 2 more, with the intent of using one of them on a Raspberry Pi 4B 8G in an Anidees Pro case and keeping the other as a spare. Well, in literally 36 hours, one of them is already dead. I believe it destroyed itself via heat. And therein lies part of the problem. I’ll explain what I casually observed, since I wasn’t taking measurements.

I ran the Pi from USB-C for about a day, without the PoE HAT installed. It was in the Anidees Pro case, fully assembled. It was fine, idling around 37.4C and not seeming to go above 44C when running high loads (make -j4 on some medium-sized C++ projects; a prominent workload for me). Solid proof that for my use, the Anidees Pro case works exactly as intended. The case is a big heatsink. Note that I have the 5mm spacers installed for the lid, so it’s open around the entire top perimeter.

I then installed the PoE+ HAT, with extension headers and the correct length standoffs that are needed in the Anidees Pro case. Note that this activity isn’t trivial; the standoffs occupy the same screw holes as the bottom of the case (from opposite directions), and an unmodified standoff is likely to bottom out as it collides with the end of the case bottom screw. You can shorten the threaded end of the standoff, or do as I did and use shorter standoffs and add a nut and washers to take up some of the thread. I don’t advise shortening the screws for the bottom of the case.

I plugged in the PoE ethernet from my office lab 8-port PoE switch, which has been powering the 4 racked Pis for a few days. And observed the expected horrible noise noted by others. Since I expected it, I immediately unplugged the USB-C power. I continued installing software and started compiling and installing my own software (libDwm, libCredence, mcweather, DwmDns, libDwmWebUtils, mcloc, mcrover, etc.). It was late, so I stopped here. On my way out of the home office, I put my hand on the Pi. It was much warmer than when running from the USB-C. In fact, uncomfortably warm. I checked the CPU temperature with vcgencmd, it was under 40C. Hmm. I was tired, so I decided to leave it until the next day and see what happens.

In the morning the Pi had no power. I unplugged and plugged both ends of the 12″ PoE cable. Nothing.

It turns out that the PoE+ HAT is dead. Less than 48 hours of runtime. As near as I can tell, it cooked itself. The PoE port on the ethernet switch still works great. The Pi still works great (powered from USB-C after the dead PoE+ HAT was removed).

I find this saddening and unacceptable. “If it’s not tested, it’s broken.”. Hey Eben: it’s broken. No, literally, it’s broken. And looks to not even be smoke tested. In fact I’d say it’s worse than the issues with Rev. 1 of the original PoE HAT. This is a design problem, not a testing problem. In other words, the problem occurred at the very beginning of the process. Which means it passed through all of engineering. And this issue lies with leadership, not the engineers.

So not only have you gone backward, you’ve gone further back than you were for Rev. 1 of the PoE HAT. And you discontinued the only good PoE HAT you had? Now I’n just left with, “Don’t believe anything, ANYTHING from the mouth of Eben Upton.”.

I’m angry because my trust of Raspberry Pi has been eroding for years, and this is just another lump of coal. We hobbyists were basically screwed for 3 years on availability of all things Pi 4, and you’re still selling a PoE HAT that no one should use.

I’ve been saying this for a couple of years now: there is opportunity for disruption here. While I appreciate the things the Raspberry Pi Foundation has done, I’m starting to feel like I can’t tell anyone that the Pi hardware landscape is great. In fact for many things, it has stagnated.

For anyone for whom 20 US dollars matters, do NOT buy an official PoE+ HAT. Not that it matters… it’s June 2023 and it’s still not trivial to find something to use it (a Raspberry Pi 3 or 4).

There comes a time when a platform dies. They get lazy after the ecosystem builds around them. I’m wondering if I am seeing that on the horizon for the Raspberry Pi.

More Raspberry Pi? Thanks!

Years ago I bought a fairly simple 1U rack mount for four Raspberry Pi Model 4B computers. Then the COVID-19 pandemic happened, and for years it wasn’t possible to find Model 4B’s with 4G or 8G of RAM at less than scandalous scalper prices. So the inexpensive rack mount sat for years collecting dust.

This month, June 2023, I was finally able to buy four Model 4B Raspberry Pis with 8G of RAM, at retail price ($75 each). Hallelujah.

I also bought four of the PoE+ HATs. Which IMHO suck compared to the v2 version of the original PoE HATs; the efficiency is terrible at the tiny loads I have on them (no peripherals), they consume a lot more power and waste it as heat. I don’t need to repeat what’s been written elsewhere by those who’ve published measurements. There also appears to be a PoE to USB-C isolation issue, but fortunately for me I won’t have anything plugged into the USB-C on these Pis.

The plan is to put these four Pis in the wall-mounted switch rack in the basement. They’re mostly going to provide physical redundancy for services I run that don’t require much CPU or network and storage bandwidth. DNS, DHCP, mcrover and mcweather, for example.

I am using Samsung Pro Endurance 128G microSD cards for longevity. If I needed more and faster I/O, I’d be using a rack with space for M.2 SATA per Pi, but I don’t need it for these.

I’ve loaded the latest Raspberry Pi OS Lite 64-bit on them, configured DHCP and DNS for them (later I’ll configure static IPs on them), and started installing the things I know I want/need. They all have their PoE+ HATs on, and are installed in the rack mount. I’ll put the mount into the rack this weekend. The Pis are named grover, nomnom, snoopy and lassie.

Separately, I ordered 2 more Raspberry Pis (same model 4B with 8G of RAM), two more PoE+ HATs and 2 cases: an Argon ONE v2 and an Anidees AI-PI4-SG-PRO. Both of these turn a significant part of the case into a heatsink.

The Argon ONE v2 comes with a fan and can’t use the PoE+ HAT, but can accept an M2 SATA add-on. I’m planning to play with using this one in the master bedroom, connected to the TV. It’s nice that it routes everything to the rear of the case; it’s much easier to use in an entertainment center context.

I believe the Anidees AI-PI4-SG-PRO will allow me to use a PoE+ HAT, but I’ll need extension headers which I’ll order soon. I’ve liked my other Anidees cases, and I think this latest one should be the best I’ve had from them. They’re pricey but premium.

It’s nice that I can finally do some of the work I planned years ago. Despite hoping that I’d see RISC-V equivalents by now, the reality is that the Pi has a much larger ecosystem than anything equivalent. It’s still the go-to for several things and I’m happy.

mcblockd 5 years on

mcblockd, the firewall automation I created 5 years ago, continues to work.

However, it’s interesting to note how things have changed. Looking at just the addresses I block from accessing port 22…

While China remains at the top of my list of total number of blocked IP addresses, the US is now in 2nd place. In 2017, the US wasn’t even in the top 20. What has changed?

Most of the change here is driven by my automation seeing more and more attacks originating from cloud hosted services. Amazon EC2, Google, Microsoft, DigitalOcean, Linode, Oracle, et. al. While my automation policy won’t go wider than a /24 for a probe from a known US entity, over time I see probes from entire swaths of contiguous /24 networks from the same address space allocation, which will be coalesced to reduce firewall table size. Two adjacent /24 networks become a single /23. Two adjacent /23 networks become a single /22. All the way up to a possible /8 (the automation stops there).

So today, the last of 2022, I see some very large blocks owned by our cloud providers being blocked by my automation due to receiving ssh probes from large contiguous swaths of their address space.

I am very appreciative of the good things from big tech. But I’m starting to see the current cloud computing companies as the arms dealers of cyberspace.

My top 2 countries:

    CN 131,560,960 addresses
       /9 networks:    1 (8,388,608 addresses)
      /10 networks:   10 (41,943,040 addresses)
      /11 networks:   12 (25,165,824 addresses)
      /12 networks:   18 (18,874,368 addresses)
      /13 networks:   29 (15,204,352 addresses)
      /14 networks:   48 (12,582,912 addresses)
      /15 networks:   48 (6,291,456 addresses)
      /16 networks:   37 (2,424,832 addresses)
      /17 networks:   14 (458,752 addresses)
      /18 networks:    7 (114,688 addresses)
      /19 networks:   10 (81,920 addresses)
      /20 networks:    5 (20,480 addresses)
      /21 networks:    3 (6,144 addresses)
      /22 networks:    3 (3,072 addresses)
      /23 networks:    1 (512 addresses)

    US 92,199,996 addresses
       /9 networks:    3 (25,165,824 addresses)
      /10 networks:    5 (20,971,520 addresses)
      /11 networks:   10 (20,971,520 addresses)
      /12 networks:    9 (9,437,184 addresses)
      /13 networks:   16 (8,388,608 addresses)
      /14 networks:   10 (2,621,440 addresses)
      /15 networks:    8 (1,048,576 addresses)
      /16 networks:   42 (2,752,512 addresses)
      /17 networks:   10 (327,680 addresses)
      /18 networks:   11 (180,224 addresses)
      /19 networks:    8 (65,536 addresses)
      /20 networks:   10 (40,960 addresses)
      /21 networks:    2 (4,096 addresses)
      /22 networks:    9 (9,216 addresses)
      /23 networks:    9 (4,608 addresses)
      /24 networks:  818 (209,408 addresses)
      /25 networks:    4 (512 addresses)
      /26 networks:    5 (320 addresses)
      /27 networks:    5 (160 addresses)
      /28 networks:    2 (32 addresses)
      /29 networks:    7 (56 addresses)
      /30 networks:    1 (4 addresses)

You can clearly see the effect of my automation policy for the US. Lots of /24 networks get added, most of them with a 30 to 35 day expiration. Note that expirations increase for repeat offenses. But over time, as contiguous /24 networks are added due to sending probes at my firewall, aggregation will lead to wider net masks (shorter prefix lengths). Since I’m sorting countries based on the total number of addresses I’m blocking, obviously shorter prefixes have a much more profound effect than long prefixes.

TREBLEET Super Thunderbolt 3 Dock: First Impressions

TREBLEET Super Thunderbolt 3 Dock at Amazon

https://www.trebleet.com/product-page/mac-mini-thunderbolt-3-dock-with-nvme-sata-slot-cfexpress-card-slot-gray

I received this on August 25, 2022. I immediately installed a Samsung 980 Pro 1TB NVMe, then plugged the dock into AC power via the included power supply brick and into the Mac Studio M1 Ultra via the included Thunderbolt 3 cable. The performance to/from the Samsung 980 Pro 1TB NVMe is what I had hoped.

This is more than 3X faster than any other dock in this form factor available today. Sure, it’s not PCIe 4.0 NVMe speeds, but given that all other docks available in this form factor max out at 770 MB/s, and that Thunderbolt 3/4 is 5 GB/s, this is great.

I also checked some of the data in system report. All looks OK.

My first impression: this is the only dock to buy if you want NVMe in this form factor. Nothing else comes close speed-wise. Yes it’s pricey. Yes, it’s not a big brand name in North America. But they did the right thing with PCIe lane allocation, which hasn’t happened with OWC, Satechi or anyone else.

There’s really no point in buying a dock with NVMe if it won’t ever be able to run much faster than a good SATA SSD (I hope OWC, Satechi, Hagibis, AGPTEK, Qwizlab and others are paying attention). Buy this dock if you need NVMe storage. I can’t speak to longevity yet, but my initial rating: 5 out of 5 stars.