-
Added Powersave Modes to Version 1.2 and Louia 1.4
12/06/2025 at 02:03 • 0 commentsI added what I learned and did with Louia ObScura over the last 2 1/2 months on the Pi Picos to my original Linux-based Louia, releasing version 1.4 today which also contains a new, experimental powersave mode, turned on by default (although I probably should have kept it off). I also added that powersave mode to this Pico-based project, and released version 1.2 of Louia ObScura today under GPLv3, the tarball attached in the Files section.
I've got both the Pico 2 W version and the Linux-based version running simultaneously over at spartan://greatfractal.com now on different ports, the default Spartan port still directed to the Pico 2 W. The Linux-based version has always been many times faster running on a single-core 32-bit Pi Zero, but of course that is at 1 GHz with 512 MB RAM, and it gets to use all of the CPU, Linux, and LuaJIT optimizations, too. The Pico 2's RP2350 is also 32-bit (even dual-core with two more RISC-V cores), and similarly, I only use one of its two ARM cores, but it only runs at 150 MHz with almost a thousand times less RAM. Using Lua 5.5 to also implement its own OS, the code got 3x larger on the Pico so the server is running a lot more interpreted code per request which also slows it down, as I have not optimized it.
But I did take the time to add one optimization lately to reduce power. In both versions, I had previously turned off blocking for TCP/IP sockets and just quickly looped through the socket function checking to see if there was a new connection before spawning a new coroutine. Since I needed to loop anyway to allow my main dispatch loop (a sort of "kernel") to process multiple, concurrent requests so that I could avoid actual blocking of the socket for other requests, I had to use a lot more CPU cycles when I turned off the blocking on those socket checks. In my early versions of Louia, I used something like the Unix select() function under LuaSocket's socket.select() to check for new or changed connections or block if nothing was happening. But once I got a connection and the dispatch loop returned around to that socket check again, a lack of network activity would block its concurrency, becoming network IO bound and not CPU bound. So I had to time it out quickly, negating much of the reason to use select in the first place, which would have saved me a lot of CPU cycles and power otherwise.
But in working with them lately I realized that there is no good reason to keep the socket check unblocked if there is no concurrency in progress (no running coroutines). So during the idle periods where there are no connections coming in and the last coroutine has ended, it starts blocking again which stops the main dispatch loop to save CPU power. So on the Linux-based version, I just did a simple check to see if there are any coroutines running, and if not, set the socket.select() timeout variable to nil (blocking indefinitely until network activity). It works in testing although I'm not sure yet if the socket itself is timing out after long periods of inactivity since I'm not calling it anymore, as I've gotten occasional locks with 1.4, so I may have to add a (long) timeout or periodic watchdog anyway. Nevertheless, this new feature can be turned off. But if there are coroutines running, or this feature is turned off, it sets it instead to the TIMEOUTCHECKSECS variable, a short delay that I use to control the dispatch loop speed and CPU. On the Pi Zero when running atop Void Linux, when the powersave mode was turned on, the CPU use for louia.lua dropped so low during idle it was off the screen where I couldn't even see it. Great!
Yet... when I tried to do the same thing for the Pico 2 W, I ran into some problems. Firstly, select(), of course, is a Unix-like construct that does not usually exist if you have no Unix-like OS (which is what I have here). MicroPython (which I've never used, although I have used Python 2/3 multithreading and multiprocessing) has something analogous called uselect() but there is no such construct in stock Lua/MicroLua as these rely on coroutines. Thankfully the MicroLua dev wrapped SDK callbacks into an event mechanism using coroutines (which can be thought of as a type of "thread" in this context) which nicely integrated with my own coroutine-based system once I solved my conflicts with it (and there were many). I'm not certain, but I think that the lwIP TCP:accept() function in MicroLua also uses the mechanism, as it is a yieldable function that blocks if you set a deadline of nil or it will timeout if you indicate a timeout value, acting somewhat similar to socket.select().
This is the primary reason I could not merge my Louia and Louia ObScura projects, since the socket libraries were different enough and had different behavior that I had to write so much unique code around each one to get them to act similar. LuaSocket was much faster and more reliable than lwIP for me, so much so that I'm sure I have a glaring bug/inefficiency in my lwIP code--I don't think lwIP should be that slow, even on the slower Picos. But Spartan is so spartan that it still operates quickly enough to act as a server for the small page sizes expected on the Pico 2 W and Pico W internal flash, where all site pages must fit into just over 3 MiB and 1 MiB, respectively (although I've considered adding my own substitution compression routine to increase this).
Furthermore, I could not set an infinite nil timeout on the ObScura version, as the Pico has to wake eventually to check to see if the wifi is still up. For if I did this and the wifi went down, even just for a moment, it would never wake up again, since it would never try to reconnect but would remain idle expecting that a connection will eventually come in ...which can never happen. So I had to time it out every 14 minutes to perform this check, and this, in combination with my hourly wakes using my external RTC/NTP-like system, plus the 15-minute wakes using my external USB-based watchdog means that, even if idle, it's likely going to wake up 9 times per hour when the Linux version would have just remained soundly asleep. Of course Linux is doing all of those background functions for Louia 1.4, tucking it into bed so that Louia can sleep soundly knowing all will be well. But sleep-deprived and overworked Louia ObScura 1.2 is on its own, pressing that snooze button every 14 minutes.
I don't have a USB power meter to see how much power it saved, but I did add a POWERSAVEMODE boolean parameter to toggle it, so someday I'll be able to see if it saved much on the Pico, as this is not an actual hardware-level sleep mode (of which I intentionally disabled on the CYW43439 chip to aid response). And I don't know what MicroLua is doing underneath or whether it's going into an idle state at that point.
Why 14 minutes? That's a story for another day.
Lee Djavaherian