diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00b9f8f4..399f0ab3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -760,7 +760,15 @@ jobs: FROM --platform=$TARGETPLATFORM debian:trixie-slim AS fetch ARG TARGETARCH COPY projectmm_$TARGETARCH.deb /tmp/projectmm.deb - RUN dpkg-deb -x /tmp/projectmm.deb /rootfs + # libcurl4t64 is installed because the binary LINKS it (the one outbound HTTPS call), and + # the ldd sweep then collects it plus everything it needs. Naming the libraries by hand + # would be wrong the first time that chain moved. + RUN apt-get update \ + && apt-get install -y --no-install-recommends libcurl4t64 \ + && rm -rf /var/lib/apt/lists/* \ + && dpkg-deb -x /tmp/projectmm.deb /rootfs \ + && mkdir -p /deps \ + && ldd /rootfs/usr/bin/projectMM | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\.so' | xargs -I{} cp -L {} /deps/ # debian13, NOT debian12: the amd64 binary is built on ubuntu-24.04 (glibc 2.39) and # needs glibc >= 2.38, where bookworm ships 2.36 and it dies at startup. The arm64 binary # is built on ubuntu-22.04-arm and floors at 2.35, so it would also run on a bookworm @@ -772,6 +780,10 @@ jobs: # amd64 and arm64; buildx resolves the right manifest per platform. FROM gcr.io/distroless/cc-debian13@sha256:9b615fff20e1a4fad29c2b30562580b212c7dd5e2225236735cca0070ed11c78 COPY --from=fetch /rootfs/usr/bin/projectMM /usr/bin/projectMM + # The libraries the loader resolved, collected in the fetch stage. Without them the + # container dies at startup on "libcurl.so.4: cannot open shared object file": this job + # builds the image but never runs it, so that failure passed CI and reached a user. + COPY --from=fetch /deps/ /usr/lib/ ENV XDG_DATA_HOME=/data VOLUME /data EXPOSE 8080 diff --git a/CMakeLists.txt b/CMakeLists.txt index fa616f09..c8246ad7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,7 +167,11 @@ target_include_directories(mm_platform PUBLIC src/ src/platform/desktop/) # build needs no SDK and the binary runs without it. # `dl` on Linux: miniaudio (platform_desktop_audio.cpp) runtime-links ALSA/PulseAudio via # dlopen, so no audio SDK is a build requirement, the same no-SDK rule as Npcap above. +# `winhttp` on Windows: the one outbound HTTPS call (MoonCloud). It is the OS's own client, in the +# SDK Visual Studio already installs, so Windows needs no libcurl and no vendored TLS. The other +# platforms use libcurl for the same reason, each taking the stack its OS ships. target_link_libraries(mm_platform PUBLIC $<$:ws2_32> $<$:iphlpapi> + $<$:winhttp> $<$:dl> $<$:pthread>) # The two libraries genuinely need each other: mm_core calls into the platform surface, and the @@ -201,8 +205,9 @@ elseif(DEFINED ENV{MM_PACKAGING} AND NOT WIN32) # including the sanitizer lanes that configure CMake directly and legitimately have no libcurl, # so keying on it failed builds that produce no artifact at all. # - # Windows is excluded until its job installs libcurl (vcpkg), which is real work rather than an - # apt line. Until then it warns like any other machine, and its binary cannot report. + # Windows is exempt, and for a reason that is now permanent rather than pending: it sends + # through WinHTTP (httpsPost), so libcurl being absent there costs it nothing. Failing a + # Windows build over a library it does not use would block a binary that reports perfectly. message(FATAL_ERROR "libcurl not found while packaging a release. A published build with the client compiled " "out asks for consent it can never honor. Install it in the job " diff --git a/Dockerfile b/Dockerfile index a15692f0..20e707f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,9 +22,10 @@ # **When L2 matters.** mDNS discovery (finding boards, being found by them) is multicast and does # not cross a bridge network, and Art-Net's broadcast mode has the same problem. For those, attach # the container to the host's network directly (`--network host`, or an L2 CNI on Kubernetes). -# Unicast output needs none of it. NOT verified on a Linux host yet: on macOS and Windows, Docker -# Desktop runs a Linux VM, so `--network host` joins the VM rather than the machine's LAN and the -# question cannot be answered there. +# Unicast output needs none of it. Verified on a NanoPi R28S (arm64, Debian 13): the container +# serves its UI and reaches the LAN through `--network host`. On macOS and Windows the question +# cannot be answered, because Docker Desktop runs a Linux VM and host networking joins the VM +# rather than the machine's LAN. # # **Capabilities.** None. It binds 8080 as an ordinary process and needs no added capability. # @@ -52,8 +53,12 @@ ARG TARGETARCH ARG RELEASE=latest ARG REPO=MoonModules/projectMM +# libcurl4t64 is installed, not merely downloaded: the release binary links it (the one outbound +# HTTPS call), so the image must carry it AND everything it in turn needs. Letting apt resolve that +# is the point: the chain runs deep (TLS, Kerberos, LDAP, SASL, libssh2, compression), and a +# hand-written COPY list would be wrong the first time any of them changed. RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl \ + && apt-get install -y --no-install-recommends ca-certificates curl libcurl4t64 \ && if [ "$RELEASE" = "stable" ]; then \ api="https://api.github.com/repos/${REPO}/releases/latest"; \ else \ @@ -62,13 +67,23 @@ RUN apt-get update \ && url=$(curl -fsSL "$api" | grep -o "https://[^\"]*_${TARGETARCH}\.deb" | head -1) \ && test -n "$url" || { echo "no ${TARGETARCH} .deb in release ${RELEASE}" >&2; exit 1; } \ && curl -fsSL -o /tmp/projectmm.deb "$url" \ - && dpkg-deb -x /tmp/projectmm.deb /rootfs + && dpkg-deb -x /tmp/projectmm.deb /rootfs \ + # Every shared object the binary resolves to, gathered by asking the loader rather than by + # listing names: ldd walks the whole transitive chain, so this stays correct as that chain moves. + # The four the distroless base already carries (libc, libstdc++, libm, libgcc_s) are EXCLUDED + # rather than copied over: the base and this trixie stage are pinned independently, so shipping + # both would put two glibc builds in one image and let the loader pick by path order. + && mkdir -p /deps \ + && ldd /rootfs/usr/bin/projectMM | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\.so' | xargs -I{} cp -L {} /deps/ # --- stage 2: the image that ships ------------------------------------------------------------ -# Distroless: the binary plus its four shared libraries, with no shell and no package manager, so -# the attack surface is the application rather than a distribution. `ldd` on the release binary -# lists exactly libstdc++, libm, libgcc_s and libc, which is the whole reason this fits: nothing -# else has to come along. 45 MB against 140 MB for the full-Debian form. +# Distroless: the binary plus the shared libraries it resolves, with no shell and no package +# manager, so the attack surface is the application rather than a distribution. +# +# It used to be four libraries (libstdc++, libm, libgcc_s, libc), all of them in the base. Linking +# libcurl for the one outbound HTTPS call added a chain of its own (TLS, Kerberos, LDAP, compression), +# which is why the fetch stage now collects what the loader actually resolves instead of the image +# relying on the base to happen to carry it. # # **debian13, NOT debian12**, and this is load-bearing. The release is built on ubuntu-24.04 # (glibc 2.39), so the binary requires glibc >= 2.38. The debian12/bookworm images ship 2.36, where @@ -80,6 +95,11 @@ RUN apt-get update \ FROM gcr.io/distroless/cc-debian13@sha256:9b615fff20e1a4fad29c2b30562580b212c7dd5e2225236735cca0070ed11c78 COPY --from=fetch /rootfs/usr/bin/projectMM /usr/bin/projectMM +# The libraries the loader resolved in the fetch stage, collected there rather than named here. +# Without this the container starts and dies immediately on "libcurl.so.4: cannot open shared +# object file", which is what shipped between dev.126 and this fix: the publish job builds the +# image but never runs it, so a missing library passes CI and fails on a user's board. +COPY --from=fetch /deps/ /usr/lib/ # WHERE THE CONFIG LIVES, and why this line is required rather than a convenience. The desktop # build resolves its data directory from the environment (platform_desktop.cpp, userDataDir): on diff --git a/docs/explanation/mooncloud.md b/docs/explanation/mooncloud.md index d21c5ed1..39b8c24e 100644 --- a/docs/explanation/mooncloud.md +++ b/docs/explanation/mooncloud.md @@ -6,16 +6,17 @@ It is deliberately small. Each member is a separate choice with its own checkbox ## Stats -One report about this install, sent once when the firmware is installed or upgraded, and the totals from everyone else back on the same card. +One report about this install, sent once when the firmware is installed or upgraded, and again whenever you press **send update** on the card, for a setup that changed without a version change. The totals from everyone else come back on the same card. -### Why we collect this +### Why you might like this -projectMM is built by a small group of volunteers, so where the effort goes is the most consequential decision the project makes. Without numbers that decision is made from whoever spoke up most recently on Discord, which is a real signal but a badly skewed one: it over-weights the loud, the new, and the broken. +Switching this on gets you the totals back on the same card: what other people run, on what hardware, at what scale. That is the half you can see immediately, and it is worth having on its own. - **To build what people use.** Which effects, layouts, modifiers, drivers and services run on real devices tells us where the next improvement is worth the most. An effect on nearly every install earns polish; one almost nobody enables does not get rewritten ahead of it. - **To know what we can stop carrying.** Every feature costs flash, memory and maintenance forever, and on an ESP32 that budget is genuinely scarce. Something no install uses is a candidate for removal, and that is hard to justify on a hunch. - **To test on the hardware people own.** Chip, flash, PSRAM and device model tell us which boards to keep on the bench and which variants must keep building. We would rather find a break on a board we own than have you find it. - **To size things for real installations.** How many lights are driven, and how much memory is free, say whether a default is sensible or whether we tuned it for a device nobody runs. A layout that assumes 256 lights is the wrong default if most walls are far bigger. +- **To show you what other people run.** The totals come back onto the same card, so the effects, layouts and scripts other installations use are visible to you too. That is worth having on its own: it is a way to find something you did not know existed and try it tonight. - **To know whether an upgrade reached anyone.** The running version against the last reported one distinguishes an upgrade from a fresh install, which is what tells us whether a release is being picked up or a problem is stranding people on an old one. Development is not held hostage to these numbers: something rare and excellent stays. They inform the decision rather than make it. @@ -30,11 +31,11 @@ The totals are shown on the same card that asks: contributing earns the answer b A public message board between projectMM devices. Off until you turn it on, and a message is sent only because you typed one and pressed send. -Everything posted is public and permanent: no private message, no recipient, no delete. Your device name rides along only if you separately switch that on; otherwise messages show the first 8 characters of your installation id, which groups them without naming you. +Everything posted is public and permanent: no private message, no recipient, no delete. Your device name rides along only if you separately switch that on; otherwise messages show the first 8 characters of your installation id, which groups them without naming you, the way a Meshtastic node id does. ## Sync (planned) -Device to device over the internet, for a joint show across houses. Not built yet, and not built on Stats: it shares this container and the installation id, nothing else. It will be its own opt-in, described in the privacy policy before it ships. +Device to device over the internet, for a joint show across houses. Not built yet, and not built on Stats: it shares this container and the installation id, nothing else. Like every other member it will be its own separate choice, off until you turn it on. ## Turning it off diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index a301e3d4..937b4744 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -120,7 +120,7 @@ Detail: [technical](moxygen/FirmwareUpdateModule.md) The container for everything projectMM does with a server MoonModules runs. It holds no settings of its own: each thing MoonCloud does is a child with its own consent, because a user who wants one has not thereby agreed to the other. -- **Stats**, below: one opt-in report per install or upgrade, and the totals back. +- **Stats**, below: one opt-in report per install or upgrade, plus one whenever you press send update, and the totals back. - **Talk**, below: a public message board between devices. - **Sync** (planned): device to device over the internet, a joint show across houses. Not built on Stats; they share this container and the installation id, nothing else. @@ -132,8 +132,9 @@ One opt-in report about this install, sent once when the firmware is installed o - `consent`: a checkbox, off by default. Nothing is sent, and no identifier is computed, while it is off. - read-only: `version` (what is running) and `reportedVersion` (what last produced a report). They differ exactly when a report is due, which is what makes one upgrade send one report and a reboot send nothing. +- `send update`: a button that reports again now, for a setup that changed without a version change. It replaces this install's row rather than adding one, and says on the card whether it sent. Distinct from the ⟲ above the charts, which only re-reads the totals. -The report carries hardware and configuration: chip, flash, PSRAM, SDK, device model, total and free memory, how many lights are driven, and which drivers, services, layouts, effects and modifiers you added, each tagged by role. It carries no device name, no addresses, no credentials and no text you typed, and a unit test asserts those cannot appear in it. +The report carries hardware and configuration: chip, flash, PSRAM, SDK, device model, total and free memory, how many lights are driven, and which drivers, services, layouts, effects and modifiers you added, each tagged by role. A scripted module also names the script it runs, but only when that script is one we ship: a script you wrote yourself is counted under its module type and its name is never sent. It carries no device name, no addresses, no credentials and no text you typed, and a unit test asserts those cannot appear in it. The card also shows the totals everyone else reported: contributing earns the answer back where you already are. The charts are drawn empty until consent is on, so what saying yes gets you is visible before you say it. diff --git a/docs/tutorials/installing-on-linux.md b/docs/tutorials/installing-on-linux.md index 6c1e3e2a..fac175d8 100644 --- a/docs/tutorials/installing-on-linux.md +++ b/docs/tutorials/installing-on-linux.md @@ -144,6 +144,8 @@ That is a running system. Everything below is optional. ## Keeping it running after a reboot +For the package or a source build. A container does this with `--restart unless-stopped` instead, see [Docker](#docker). + Give it a systemd unit at `/etc/systemd/system/projectmm.service`: ```ini @@ -167,7 +169,7 @@ sudo systemctl enable --now projectmm systemctl status projectmm ``` -`Restart=always` covers a crash as well as a reboot. Adjust `User` and the path to where you built. +`Restart=always` covers a crash as well as a reboot. Adjust `User` and the path: `/usr/bin/projectMM` for the package, or where you built for a source build. ## Shutting down @@ -179,9 +181,65 @@ sudo shutdown now # or: sudo reboot projectMM writes to disk only when settings change, so the card is a fine home for it. The risk is the operating system's own writes. -## Containers +## Docker + +The same program, installed as a container rather than a package. One published image carries amd64 and arm64, so a board and a server pull the same tag and each gets native instructions. + +```sh +docker run -d --name projectmm --network host \ + -v projectmm-data:/data --restart unless-stopped \ + ghcr.io/moonmodules/projectmm:latest +``` + +Open `http://:8080`. `--restart unless-stopped` is what brings it back after a reboot, so Docker replaces the systemd unit above rather than needing one of its own. + +**Use `--network host`.** The web UI and unicast fixture output work through ordinary port mapping (`-p 8080:8080`), but mDNS discovery is multicast and Art-Net's broadcast mode is too, and neither crosses a bridge network. Host networking is what lets a container find boards and be found by them. Verified on a NanoPi R28S driving a ColorLight card. + +**The volume is the configuration.** `/data` holds everything you set up; without it, every `docker rm` starts you over. + +### When the container exits at once + +**`failed to mount ... fstype: overlay ... invalid argument`** means the machine's root filesystem is itself an overlay, which several board and NAS systems use, and Docker's default `overlay2` driver cannot stack one on another. Check with `findmnt -no FSTYPE /`; if it says `overlay`, switch drivers: + +```sh +sudo mkdir -p /etc/docker +# Add the key to whatever is already there. A plain `tee` would discard existing settings, and a +# NAS or a board image often ships some. +sudo sh -c 'f=/etc/docker/daemon.json; [ -s "$f" ] || echo "{}" > "$f"; \ + tmp=$(mktemp); python3 -c "import json,sys;d=json.load(open(sys.argv[1]));d[\"storage-driver\"]=\"vfs\";json.dump(d,open(sys.argv[2],\"w\"),indent=2)" "$f" "$tmp" && mv "$tmp" "$f"' +sudo systemctl restart docker +``` + +`vfs` copies layers instead of sharing them, so it uses more disk and pulls are slower. `fuse-overlayfs` is the faster alternative where the package exists. + +## Package or container: which to pick + +| | Package (`.deb`) | Docker | +|---|---|---| +| Install | `apt install ./projectmm_*.deb` | one `docker run` | +| Updates | download the new `.deb` | `docker pull` and recreate | +| Survives a reboot | needs a systemd unit | `--restart unless-stopped` | +| Runs on | Debian family, glibc 2.35+ | any Linux with Docker | +| Disk | the binary plus libraries the system already has | an image carrying its own libraries | +| Isolation | none: an ordinary program | its own filesystem and process space | +| Upgrades cleanly | apt handles dependencies | the image carries its own | +| Debugging | logs in the terminal, files on disk | `docker logs`, and no shell inside the image | + +**Take the package** on a board you own and administer: it is smaller, it starts faster, and its files are where you expect them. + +**Take Docker** where you would rather not install anything permanent, where the machine already runs containers, or where the distribution is too old for the package's glibc floor. A container carries its own libraries, so the host's age stops mattering. + +## Where else this runs + +Docker widens the target from "a Debian-family board" to "anything that runs containers", which is a much larger set: + +- **Raspberry Pi** (3, 4, 5, and Zero 2 W): arm64, so the same image and the same commands as a NanoPi. Nothing here is NanoPi-specific. +- **A NAS**: Synology (Container Manager), QNAP (Container Station), Unraid, TrueNAS. Give it host networking, or mDNS and Art-Net broadcast will not leave the box. +- **A mini PC or home server**: amd64, the largest and least surprising target. +- **A router or firewall appliance** running OpenWrt or OPNsense with Docker: plausible where the device has the RAM, and the overlay-root note above is likely to apply. +- **Kubernetes**: one pod, one volume. Multicast needs an L2 CNI rather than the default bridge. -Docker runs a full instance on amd64 and arm64 alike; the command is in the [README](https://github.com/MoonModules/projectMM#readme). The published image carries both, so a board and a server pull the same tag and each gets native instructions. +The realistic limits are architecture and memory, not the kind of device. It needs 64-bit, arm64 or amd64, because no 32-bit image is published. Memory is rarely the binding constraint: a container rendering a 16x16 grid measures 4.5 MB on a NanoPi, and a large installation grows that by its buffers rather than by a fixed overhead. A coffee machine is not out of the question if it meets both and runs Docker, though the fixtures would have to come to it. ## Where to go next diff --git a/docs/work/future/backlog-core.md b/docs/work/future/backlog-core.md index c96bde68..eede0979 100644 --- a/docs/work/future/backlog-core.md +++ b/docs/work/future/backlog-core.md @@ -56,20 +56,25 @@ running projectMM permanently on a NAS or a Pi. ## Distribution -### An arm64 Linux release build, so SBCs stop building from source (2026-09-09) +### The arm64 `.deb` is untested on Raspberry Pi OS bookworm (2026-09-13) -Every Linux job in `release.yml` runs on `ubuntu-latest`, which is x86-64, so the release publishes -`projectMM-linux-x64` and `projectmm_X.Y.Z_amd64.deb` and nothing for arm64. That one gap is why a -Raspberry Pi or a NanoPi has to clone and compile, and why the container image can only be amd64 -(the image in PR #98 installs the released `.deb`, so an arm64 image needs an arm64 `.deb` first). +**Shipped** (`a229be6d`): `build-linux-arm64` publishes `projectmm_X.Y.Z_arm64.deb` and a tarball, +the container image is a multi-arch manifest, and the SBC route is `apt install`. Verified on a +NanoPi R28S (Debian 13 trixie): installed, ran, and reported to MoonStats as `linux-arm64`. -GitHub offers arm64 Linux runners for public repositories (`ubuntu-24.04-arm`), so this is a second -job rather than cross-compilation. Unverified against this repo: whether `package_desktop.py` runs -there unmodified, and whether the runner is available on this plan. Check both before promising it. +One piece of that verification was never done, because the bench has no such board. The job runs on +`ubuntu-22.04-arm` **deliberately**, for glibc 2.35: Raspberry Pi OS bookworm ships 2.36, and a +build on `ubuntu-24.04-arm` (glibc 2.39) would install there and then die at startup with +`GLIBC_2.38 not found`, the same failure the Dockerfile records hitting on debian12. The floor is +what makes bookworm work, and **bookworm is the one target nobody has run it on**. -Shipping it collapses three problems into one fix: the SBC route becomes `apt install`, the -container can publish a multi-arch manifest (one tag, Docker picks per host), and -[installing-on-linux.md](../../tutorials/installing-on-linux.md) loses its build-from-source branch. +Two halves are untested together: that the binary starts (the symbol check says the highest +requirement is `GLIBC_2.35`, which is the proxy), and that `Depends: libcurl4 | libcurl4t64` +resolves there. Trixie provides only `libcurl4t64`, bookworm only `libcurl4`, and only the first +half has been exercised. A wrong alternation fails visibly at `apt install` with an unsatisfiable +dependency rather than installing something broken, which is the good failure mode. + +**A user with a Pi settles it in one command.** Closing this needs a report, not a code change. ### A flashable SD image with projectMM already on it (robwomp, 2026-09-08) diff --git a/docs/work/present/Plan-20260910 - MoonCloud.md b/docs/work/present/Plan-20260910 - MoonCloud.md index 9d1c1669..cd18d0ea 100644 --- a/docs/work/present/Plan-20260910 - MoonCloud.md +++ b/docs/work/present/Plan-20260910 - MoonCloud.md @@ -90,11 +90,11 @@ The card explains what consent buys above the checkbox, and `inApMode()` asks th One POST, fire and forget, off the render thread on `tick1s`. A failure is silent and not retried: a lost report costs one row in an aggregate, where a retry queue is persistence, scheduling and a failure mode for something nobody is waiting for. `markReported()` runs on hand-off, so an unreachable server leaves the device quiet rather than re-sending every boot. -**HTTPS through one seam, `platform::httpsPost`, using the TLS each OS already ships.** libcurl on desktop, present on macOS and every Linux distribution and found with `find_package(CURL)`; `esp_http_client` with `esp_crt_bundle_attach` on ESP32, the same pair the OTA path uses, so a device adds call-site code rather than a TLS stack. Nothing is vendored and no certificate store is ours to maintain. +**HTTPS through one seam, `platform::httpsPost`, using the TLS each OS already ships.** libcurl on macOS and Linux, present on both and found with `find_package(CURL)`; WinHTTP on Windows, which is in the SDK and needs nothing acquired; `esp_http_client` with `esp_crt_bundle_attach` on ESP32, the same pair the OTA path uses, so a device adds call-site code rather than a TLS stack. Nothing is vendored and no certificate store is ours to maintain. `platform::httpRequest` stays what it is: a LAN socket for the Philips Hue v1 API, plain HTTP, host given as a dotted-quad IP. `httpsPost` is the one that resolves names and verifies certificates. -**libcurl is optional.** Without it the build succeeds, `MM_HAVE_CURL` stays undefined and `httpsPost` returns false, so a build never fails over an opt-in statistic. CMake prints which way it went. +**libcurl is optional.** Without it a macOS or Linux build succeeds, `MM_HAVE_CURL` stays undefined and `httpsPost` returns false, so a build never fails over an opt-in statistic. CMake prints which way it went, and fails a packaging build outright (`MM_PACKAGING`), since a published binary that asks for consent must be able to honor it. Windows is exempt: it never uses libcurl. Verified against live endpoints: a valid certificate sends, and `expired.badssl.com` is refused, so the verification is real. `VERIFYPEER` and `VERIFYHOST` are set explicitly rather than left to curl's defaults, so a later edit has to say out loud that it is turning them off. @@ -136,7 +136,7 @@ The address is compiled in (`kHost` in `MoonCloudModule.h`, `kMoonCloudUrl` in ` ## Risks - **The totals are trust-based.** Any sender id can be claimed, so the numbers rest on people having no reason to fabricate them. Accepted: the alternative is authenticating users to defend a statistic. Rate limiting at the edge blunts casual abuse, and the privacy policy says this out loud. -- **A prompt that annoys is worse than no data.** It appears once per install or upgrade, and Never is permanent. If it ever fires more often than that, it is a bug. +- **A prompt that annoys is worse than no data.** It appears once per install or upgrade, and Never is permanent. If it ever fires more often than that, it is a bug. (The refresh button sends on demand, which is the user asking rather than the device prompting.) - **`std::random_device` is deterministic on some libstdc++ targets**, which would make two fresh containers share an id. Container-only, and detectable in the data as an implausibly popular id. - **The server is a standing commitment.** It needs an owner, a domain and an account, and that rather than the code is what kept this in the backlog. diff --git a/docs/work/present/Plan-20260912 - Ship an arm64 Linux package.md b/docs/work/present/Plan-20260912 - Ship an arm64 Linux package.md deleted file mode 100644 index c013a543..00000000 --- a/docs/work/present/Plan-20260912 - Ship an arm64 Linux package.md +++ /dev/null @@ -1,67 +0,0 @@ -# Plan: ship an arm64 Linux package - -## Context - -projectMM releases Linux binaries for x86-64 only. An arm64 board (Raspberry Pi, NanoPi, most SBCs) has to clone the repo and compile, which takes an hour on first run and needs swap on a 1 GB board or the compiler is OOM-killed with no error. [installing-on-linux.md](docs/tutorials/installing-on-linux.md) documents that route, and the PO walked it on a NanoPi R28S: `.local` did not resolve, the routing table was skipped by a deep link, and the build step was still ahead. - -The blocker was never the code. `package_desktop.py` hand-rolls the `.deb` (an `ar` archive of two tarballs plus a control file), so nothing about it is x86-specific; it was that no arm64 Linux machine was available to build on. GitHub's arm64 runners went generally available for public repositories on 2025-08-07, which removes that. The release workflow already anticipates this at [release.yml:629](.github/workflows/release.yml): *"An arm64 image needs an arm64 Linux build in `build-linux` first, at which point this job gains a `platforms:` line and nothing else changes."* (`build-linux` is `build-linux-x64` since the jobs gained their architecture in the name.) - -Outcome: a user on a Pi or a NanoPi runs `sudo apt install ./projectmm_X.Y.Z_arm64.deb` and is done, and the container image runs on arm64 hosts. - -## What the exploration settled - -- **The glibc floor is the whole decision.** The amd64 binary is built on `ubuntu-24.04` (glibc 2.39) and requires ≥ 2.38. Raspberry Pi OS Bookworm ships **2.36**, so an arm64 build on `ubuntu-24.04-arm` would install cleanly and then die with `GLIBC_2.38 not found` — the exact failure [Dockerfile:68](Dockerfile) records hitting on debian12. Building on **`ubuntu-22.04-arm` (glibc 2.35)** covers Bookworm (2.36), Debian 13 trixie (2.41) and Ubuntu 24.04 (2.39), because glibc symbol versioning is backward compatible. -- **GCC 12.3.0 is preinstalled on `ubuntu-22.04-arm`**, so the low floor costs no compiler setup (no PPA, no sysroot, no static-link tradeoff). [CMakeLists.txt:73](CMakeLists.txt) already demotes four false-positive warnings for `GNU AND VERSION_LESS 16`, so GCC 12 is inside the range the project already supports. -- **The PO's NanoPi is not the constraint**: measured over ssh as Debian 13 trixie, **glibc 2.41**, GCC 14.2.0, aarch64, 964 MB RAM with **swap 0**, cmake not installed. It installs a package built against any of these floors. Its value here is *verification on real arm64 hardware*, which CI cannot do. -- **Three hardcoded strings block arm64** in [moondeck/ci/package_desktop.py](moondeck/ci/package_desktop.py): the arch gate at line 612, `"Architecture: amd64"` in the control file at line 164, and the `_amd64.deb` / `-linux-x64-` names at lines 170 and 100. -- **The container job needs one line plus a glob fix.** [release.yml:700](.github/workflows/release.yml) pins `platforms: linux/amd64`, and its Dockerfile step selects `dist/projectmm_*_amd64.deb` at line 674. - -## Steps - -### 1. Teach `package_desktop.py` about arm64 - -In [moondeck/ci/package_desktop.py](moondeck/ci/package_desktop.py), derive the Debian arch rather than hardcoding it: - -- `main()` line 612: accept `aarch64`/`arm64` alongside `x86_64`/`amd64`, and pass the resolved Debian arch (`amd64` or `arm64`) down. -- `package_deb()` line 164: `f"Architecture: {deb_arch}\n"`; line 170: `projectmm_{version}_{deb_arch}.deb`. -- `package_linux()` line 100: `projectMM-linux-{label}-v{version}.tar.gz` where label is `x64` or `arm64`; line 102 passes the matching `platform_label` to `readme_text()`. -- Update the module docstring at lines 7-8, which lists the produced asset names. - -One new helper, `_linux_arch()`, returning `(deb_arch, tarball_label, readme_label)`. No behaviour change on x86-64: same names, same bytes. - -### 2. Add the `build-linux-arm64` job - -In [.github/workflows/release.yml](.github/workflows/release.yml), copy `build-linux-x64` (line 392) to `build-linux-arm64` with `runs-on: ubuntu-22.04-arm`, uploading artifact name `desktop-linux-arm64`. The tag/version resolution blocks are identical by design across the build jobs, so keep them identical here too. - -Add the job to `release`'s `needs:` list (line 626). The "Flatten artifacts into dist/" step already globs every artifact, so the new assets ride along with no change. - -**Carry a comment explaining the runner choice**, because `ubuntu-22.04-arm` next to `ubuntu-latest` looks like an oversight: 22.04 is deliberate for glibc 2.35, the floor that keeps Raspberry Pi OS Bookworm supported. - -### 3. Publish a multi-arch container image - -In the `publish-container` job: download both Linux artifacts, select the `.deb` per platform, and set `platforms: linux/amd64,linux/arm64`. Delete the now-stale "amd64 only" comment at line 629. - -The base image is already multi-arch (`gcr.io/distroless/cc-debian13`), but it is **pinned by digest**, and a digest names one architecture. Switch that pin to the multi-arch index digest, keeping the pin-not-tag discipline and its comment. - -### 4. Documentation - -- [docs/tutorials/installing-on-linux.md](docs/tutorials/installing-on-linux.md): the routing table at line 19 sends `aarch64` to "Build from source"; it becomes "Install the package". Rewrite the `x86-64:` section as one **Install the package** section covering both arches, keep build-from-source as the fallback for other distributions, and update the line-22 paragraph that states the binaries are x86-64 only. Keep the swap note: it still applies to anyone building from source on a 1 GB board. -- [README.md:171](README.md): the asset list gains the arm64 line. -- [Dockerfile:32](Dockerfile) and line 170 of the tutorial: both say the image is amd64-only. -- A [MIGRATING](docs/reference/MIGRATING.md) entry is **not** needed: this adds assets and breaks nothing. - -## Verification - -1. `uv run moondeck/ci/package_desktop.py --version 0.0.0-test` on macOS still produces the unchanged `.dmg` path (the refactor must not touch non-Linux branches). -2. Push the branch and let the release workflow run on a dispatch: `build-linux-x64` and `build-linux-arm64` both green, and the release carries `projectmm_X.Y.Z_amd64.deb`, `projectmm_X.Y.Z_arm64.deb` and both tarballs. -3. `dpkg-deb -I projectmm_X.Y.Z_arm64.deb` reports `Architecture: arm64`. -4. **On the NanoPi** (the gate that matters, PO's call): `sudo apt install ./projectmm_*_arm64.deb`, then `projectMM`, then open `http://192.168.1.156:8080` and see the UI render. This is the only step that proves the binary runs on real arm64 hardware. -5. On the NanoPi: `ldd $(which projectMM)` resolves every library, and `strings` confirms no `GLIBC_2.3[89]` requirement above 2.35. -6. `docker buildx imagetools inspect ghcr.io/moonmodules/projectmm:latest` lists both `linux/amd64` and `linux/arm64`. -7. Ideally on a Raspberry Pi OS Bookworm board if one is available, since 2.36 is the floor this whole design protects. Absent a Pi, step 5's symbol check is the proxy. - -## Risks - -- **GCC 12 vs the CI's 13.** The four demoted warnings are already handled for GCC < 16, but GCC 12 is a version nothing currently compiles with. A new warning there fails the job; the fix is the same demotion block, and the job failing loudly in CI is the right place to find out. -- **22.04 leaves GitHub-hosted support in 2027.** The fallback is a sysroot build on a newer runner, which is more setup; worth a backlog note rather than building now. -- **Multi-arch digest pinning.** Repinning the distroless base to an index digest is the one step where a wrong value silently produces an amd64-only image; step 6 is what catches it. diff --git a/mooncloud/worker.js b/mooncloud/worker.js index 3915ea61..bed94842 100644 --- a/mooncloud/worker.js +++ b/mooncloud/worker.js @@ -99,8 +99,12 @@ async function handleReport(request, env) { if (typeof report.installationId !== "string" || !/^[0-9a-f]{32}$/.test(report.installationId)) { return json({ ok: true }, 202); } - // `event` reaches every user's card as a pie legend, so it is one of two words or nothing. - if (report.event !== undefined && report.event !== "install" && report.event !== "upgrade") { + // `event` reaches every user's card as a pie legend, so it is one of three words or nothing. + // "refresh" is a user pressing the button on the card: the same payload sent again because their + // setup changed without a version change. It overwrites their row in `reports` like any other + // report, and is distinct in `events` so the install count stays a count of installs. + if (report.event !== undefined && report.event !== "install" && + report.event !== "upgrade" && report.event !== "refresh") { return json({ ok: true }, 202); } diff --git a/src/core/MoonStatsModule.h b/src/core/MoonStatsModule.h index 759959a6..65ea7927 100644 --- a/src/core/MoonStatsModule.h +++ b/src/core/MoonStatsModule.h @@ -33,6 +33,7 @@ #include "platform/platform.h" #include "core/LightSummary.h" // lightCount: the POD the light domain publishes #include "light/drivers/Drivers.h" // Drivers::latestSummary(): the real light total +#include "light/moonlive/MoonLiveScriptFile.h" // isFactoryScript: a shipped name is ours, not yours namespace mm { @@ -45,7 +46,10 @@ namespace mm { // asserts the forbidden names cannot appear whatever the tree holds. /// What the report says happened. -enum class MoonStatsEvent : uint8_t { Install, Upgrade }; +/// Install and Upgrade are automatic and happen once each. Refresh is the user pressing the +/// button: same payload, same consent, sent again because their setup changed in a way no version +/// bump describes (they wired the real fixtures up after reporting a bare board). +enum class MoonStatsEvent : uint8_t { Install, Upgrade, Refresh }; /// Read one control's value out of `mod` by name. Named lookup rather than an index: a renamed /// control makes the field disappear rather than emitting whatever moved into its slot. @@ -100,6 +104,21 @@ inline void field(JsonSink& sink, const MoonModule* mod, const char* control, co /// separately: one list in one column, four charts out of it. `ModuleRole` already exists on every /// module, so nothing new is invented and nothing a user typed is sent: both halves come from the /// catalog's fixed vocabulary. +/// The shipped script a module runs, or nullptr when it runs none, runs one a user wrote, or is not +/// a scripted module at all. Only a name from the catalog is returned, so nothing a user typed can +/// reach a caller (the report test pins that). +inline const char* factoryScriptOf(const MoonModule* m) { + if (!m) return nullptr; + const ControlList& cs = m->controls(); + for (uint8_t i = 0; i < cs.count(); i++) { + if (cs[i].type != ControlType::FilePath || !cs[i].name) continue; + if (std::strcmp(cs[i].name, "script") != 0) continue; + const char* value = static_cast(cs[i].ptr); + return moonlive::isFactoryScript(value) ? value : nullptr; + } + return nullptr; +} + inline void reportModules(JsonSink& sink, const MoonModule* const* mods, uint8_t count, bool& first) { for (uint8_t i = 0; i < count; i++) { @@ -115,8 +134,17 @@ inline void reportModules(JsonSink& sink, const MoonModule* const* mods, uint8_t // remains is what somebody chose: drivers, services, layouts, effects, modifiers. const ModuleRole role = m->role(); if (m->name() && role != ModuleRole::Generic && role != ModuleRole::Layer) { + // A scripted module is "MoonLive" whatever it runs, so the type name alone says nothing + // about what the device is actually doing. The script name is the interesting half, and + // it is reported ONLY when it is one we ship: those come from our own catalog, the same + // fixed vocabulary as a module type. A name the user invented is text they typed, which + // the report never carries, so it degrades to the bare type name. + const char* script = factoryScriptOf(m); char entry[80]; - std::snprintf(entry, sizeof(entry), "%s:%s", roleName(role), m->name()); + if (script) + std::snprintf(entry, sizeof(entry), "%s:%s/%s", roleName(role), m->name(), script); + else + std::snprintf(entry, sizeof(entry), "%s:%s", roleName(role), m->name()); if (!first) sink.append(","); first = false; sink.writeJsonString(entry); @@ -150,7 +178,8 @@ inline void buildMoonStatsReport(JsonSink& sink, if (!first) sink.append(","); sink.append("\"event\":"); - sink.writeJsonString(event == MoonStatsEvent::Upgrade ? "upgrade" : "install"); + sink.writeJsonString(event == MoonStatsEvent::Refresh ? "refresh" + : event == MoonStatsEvent::Upgrade ? "upgrade" : "install"); first = false; if (version && *version) { @@ -213,13 +242,46 @@ class MoonStatsModule : public MoonModule { /// line names them instead of repeating them. void refreshStatus() { if (consent_) clearStatus(); - else setStatus("Off. Switch on to share what hardware you run, once per install or upgrade: " + else setStatus("Off. Switch on to share what hardware you run, once per install or upgrade " + "and whenever you press send update: " "the empty charts below are exactly what it contributes to, and what you get " "back. No device name, no addresses, no credentials."); } void onControlChanged(const char* name) override { - if (name && std::strcmp(name, "consent") == 0) refreshStatus(); + if (!name) return; + if (std::strcmp(name, "consent") == 0) { refreshStatus(); return; } + if (std::strcmp(name, "send update") != 0) return; + // Every outcome says something. A button that sometimes does nothing and never explains why + // leaves a user unable to tell "it worked" from "it was ignored", which is the state this + // card was in: the only way to know was to read the database. + // + // Consent is re-read rather than assumed: a control write arrives from the API as readily + // as from the card, and this is the one path where a user action opens a connection. + if (!consent_) { setStatus("Switch consent on first: nothing is sent while it is off.", + Severity::Warning); return; } + if (!platform::httpsAvailable()) { + setStatus("This build cannot send: it was compiled without an HTTPS client.", + Severity::Error); + return; + } + if (!platform::networkReady()) { + setStatus("No network yet. Press send update again once this device is online.", + Severity::Warning); + return; + } + if (inApMode()) { + setStatus("Serving its own access point, so there is no route out. " + "Join a network, then press send update.", Severity::Warning); + return; + } + if (sendReport(MoonStatsEvent::Refresh)) { + setStatus("Sent. The charts below now describe this device as it is now.", + Severity::Status); + } else { + setStatus("Could not reach the server. Nothing was sent; press send update to try again.", + Severity::Error); + } } void defineControls() override { @@ -244,6 +306,20 @@ class MoonStatsModule : public MoonModule { // has nothing to persist. controls_.addReadOnly("version", runningVersion_, sizeof(runningVersion_)); + // Send again on demand. The automatic trigger is a version comparison, which says nothing + // about a setup that changed WITHOUT a version change: someone who reported a bare board, + // then wired up the fixtures they actually run, has no way to correct what the charts say + // about them. Pressing this replaces their row rather than adding one, because the server + // keys on (installationId, version). + // + // "send update", not "refresh": the charts below already carry a ⟲ that re-reads them, and + // two controls both called refresh would be one word for two different jobs. This one + // SENDS. The event it carries on the wire is still "refresh", which the server and the + // stored rows already use. + // + // A press that cannot send says why (see onControlChanged) rather than doing nothing: the + // card gave no sign either way, and the only way to tell was to read the database. + controls_.addButton("send update"); } /// True when a report is due: the user said yes, and the running version differs from the one @@ -312,7 +388,7 @@ class MoonStatsModule : public MoonModule { if (!platform::networkReady()) return; // nothing to do yet; try again next second // Serving our own AP means no route out, so a send would fail and mark itself reported. if (inApMode()) return; - sendReport(); + sendReport(dueEvent()); } /// Asked of the platform rather than pushed in by NetworkModule: a stale copy is a prompt that @@ -322,16 +398,18 @@ class MoonStatsModule : public MoonModule { private: /// Build the report and POST it once. Marked reported on hand-off: re-sending until a server /// answers would turn one report into a heartbeat. - void sendReport() { + /// `kind` defaults to the automatic install-or-upgrade decision. The button passes Refresh, + /// which is the one case the version comparison cannot express. + bool sendReport(MoonStatsEvent kind) { char id[kInstallationIdChars + 1] = {}; installationId(id); - if (!id[0]) return; // no consent, no id, no report + if (!id[0]) return false; // no consent, no id, no report // Scheduler exposes module(i) rather than the array. 32 is its own capacity, so this // cannot truncate a tree it accepted. MoonModule* tree[32] = {}; auto* sched = Scheduler::instance(); - if (!sched) return; + if (!sched) return false; uint8_t count = 0; for (uint8_t i = 0; i < sched->moduleCount() && count < 32; i++) { if (MoonModule* m = sched->module(i)) tree[count++] = m; @@ -339,17 +417,33 @@ class MoonStatsModule : public MoonModule { JsonSink body; const LightSummary* lights = Drivers::latestSummary(); + // previousVersion ONLY on an upgrade: the server reads its presence as what makes a row an + // upgrade (worker.js), and previousVersion() returns reportedVersion_, which is non-empty + // whenever a refresh is pressed. Sending it there would report every refresh as an upgrade + // from the version already running. + const char* prev = (kind == MoonStatsEvent::Upgrade) ? previousVersion() : nullptr; buildMoonStatsReport(body, tree, count, - dueEvent(), id, runningVersion_, previousVersion(), + kind, id, runningVersion_, prev, lights ? lights->lightCount : 0, static_cast(platform::totalHeap()), static_cast(platform::freeHeap())); - // Sent through the container, which owns the address. The response is discarded. + // Sent through the container, which owns the address. The response body is discarded, but + // WHETHER it was accepted is not: the button reports it, so a press is never silent. + bool sent = false; if (auto* cloud = static_cast(parent())) { - (void)cloud->post("/api/report", body.data()); + sent = cloud->post("/api/report", body.data()); } - markReported(); + // The AUTOMATIC report marks itself either way: nobody is waiting for it, and re-sending + // until a server answers would turn one report into a heartbeat. + // + // A BUTTON press is different, and marking it would lose data. Pressing it before the + // automatic report has gone out (no network yet at boot, then a failed send) would set + // reportedVersion to the running version, reportDue() would be false forever, and the + // install would never be counted: a press the user was TOLD had failed, silently + // consuming the report they were waiting for. + if (kind != MoonStatsEvent::Refresh || sent) markReported(); + return sent; } bool consent_ = false; diff --git a/src/light/moonlive/MoonLiveScriptFile.h b/src/light/moonlive/MoonLiveScriptFile.h index 646436f3..92da5402 100644 --- a/src/light/moonlive/MoonLiveScriptFile.h +++ b/src/light/moonlive/MoonLiveScriptFile.h @@ -1,6 +1,7 @@ #pragma once #include "core/moonlive/MoonLive.h" +#include "light/moonlive/script_catalog.h" // the shipped names, for isFactoryScript below #include "platform/platform.h" #include @@ -157,6 +158,28 @@ inline bool isScriptExt(const char* ext) { std::strcmp(ext, kPaletteExt) == 0; } +/// Is `name` one of the scripts we ship? Beside `isScriptExt` for the same reason: the catalog is +/// the only list of shipped names, so a second copy would drift the day a script is added. +/// +/// The distinction matters wherever a script name leaves the device: a shipped name comes from our +/// own catalog, while a name a user invented is text they typed. +inline bool isFactoryScript(const char* name) { + if (!name || !*name) return false; + const char* ext = std::strrchr(name, '.'); + if (!ext) return false; + const char* const* cat = nullptr; + size_t n = 0; + if (std::strcmp(ext, kEffectExt) == 0) { cat = kEffectCatalog; n = kEffectCatalogCount; } + else if (std::strcmp(ext, kLayoutExt) == 0) { cat = kLayoutCatalog; n = kLayoutCatalogCount; } + else if (std::strcmp(ext, kModifierExt) == 0) { cat = kModifierCatalog; n = kModifierCatalogCount; } + else if (std::strcmp(ext, kServiceExt) == 0) { cat = kServiceCatalog; n = kServiceCatalogCount; } + else if (std::strcmp(ext, kPaletteExt) == 0) { cat = kPaletteCatalog; n = kPaletteCatalogCount; } + else return false; + for (size_t i = 0; i < n; i++) + if (std::strcmp(cat[i], name) == 0) return true; + return false; +} + /// The largest script the loader will read into RAM at once. Not a language limit — the buffer is /// sized to the FILE and freed the moment the compile ends — but a bound so a stray large file /// cannot ask a 320 KB device for an allocation it will not survive. diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 9e456cc3..715b2805 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -36,6 +36,7 @@ #include // _fileno, _commit (POSIX fileno/fsync equivalents) #include // GetIfTable2 — real link state + negotiated speed (ethLinkUp) #include // MIB_IF_ROW2: sees a NIC a Hyper-V vSwitch hides from GetAdaptersAddresses +#include // https POST: Windows' own TLS, so the .exe needs no bundled library #else #include #include @@ -539,7 +540,8 @@ const char* chipModel() { } bool httpsAvailable() MM_NONBLOCKING { -#ifdef MM_HAVE_CURL +#if defined(_WIN32) || defined(MM_HAVE_CURL) + // Windows needs no libcurl: WinHTTP ships with the OS, so the send path is always compiled in. return true; #else return false; // built without libcurl: httpsPost can never succeed @@ -547,7 +549,84 @@ bool httpsAvailable() MM_NONBLOCKING { } bool httpsPost(const char* url, const char* body, uint32_t timeoutMs) { -#ifdef MM_HAVE_CURL +#ifdef _WIN32 + // WinHTTP rather than libcurl on Windows. Same reason the other platforms use libcurl: the TLS + // the OS already ships, nothing vendored. libcurl has no dev package on the runner image and no + // public binary cache, so acquiring it would mean building curl from source on every release; + // winhttp.lib is in the SDK and costs one link entry. Schannel and the Windows certificate + // store are used implicitly, which is the same trust root every other Windows program gets. + if (!url || !*url) return false; + + // WinHTTP takes the pieces of a URL separately, and as wide strings, where libcurl takes one + // byte string. WinHttpCrackUrl does the split so no parsing is hand-rolled here. + const int wideLen = MultiByteToWideChar(CP_UTF8, 0, url, -1, nullptr, 0); + if (wideLen <= 0) return false; + std::wstring wideUrl(static_cast(wideLen), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, url, -1, wideUrl.data(), wideLen); + + wchar_t host[256] = {}; + wchar_t path[1024] = {}; + wchar_t query[1024] = {}; + URL_COMPONENTS parts = {}; + parts.dwStructSize = sizeof(parts); + parts.lpszHostName = host; parts.dwHostNameLength = ARRAYSIZE(host); + parts.lpszUrlPath = path; parts.dwUrlPathLength = ARRAYSIZE(path); + // lpszExtraInfo is the query string, which WinHttpCrackUrl splits OUT of the path. Asking for + // it and re-joining is what keeps this a general seam: the one caller today passes no query, + // and a later one that did would otherwise have it dropped silently. + parts.lpszExtraInfo = query; parts.dwExtraInfoLength = ARRAYSIZE(query); + if (!WinHttpCrackUrl(wideUrl.c_str(), 0, 0, &parts)) return false; + // Anything but https is a caller error rather than something to downgrade into cleartext. + if (parts.nScheme != INTERNET_SCHEME_HTTPS) return false; + const std::wstring target = std::wstring(path) + query; + + HINTERNET session = WinHttpOpen(L"projectMM", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!session) return false; + + // The one timeout the caller gives, applied to every phase: a hung DNS or TLS handshake must + // bound the same way a hung read does, or the "bounded blocking send" contract is not kept. + const int t = static_cast(timeoutMs); + WinHttpSetTimeouts(session, t, t, t, t); + + bool ok = false; + if (HINTERNET connection = WinHttpConnect(session, host, parts.nPort, 0)) { + if (HINTERNET request = WinHttpOpenRequest(connection, L"POST", target.c_str(), nullptr, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_SECURE)) { + // No redirect following, matching the libcurl path: the one endpoint is ours and + // answers directly, and following one would let a server move a POST body elsewhere. + DWORD noRedirects = WINHTTP_DISABLE_REDIRECTS; + WinHttpSetOption(request, WINHTTP_OPTION_DISABLE_FEATURE, + &noRedirects, sizeof(noRedirects)); + + const char* payload = body ? body : ""; + const DWORD payloadLen = static_cast(std::strlen(payload)); + // lpOptional carries the whole body, which is what lets a one-shot POST skip + // WinHttpWriteData entirely. + if (WinHttpSendRequest(request, + L"Content-Type: application/json\r\n", + static_cast(-1), + const_cast(payload), payloadLen, payloadLen, 0) + && WinHttpReceiveResponse(request, nullptr)) { + DWORD status = 0, statusSize = sizeof(status); + if (WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize, + WINHTTP_NO_HEADER_INDEX)) { + ok = status >= 200 && status < 300; + } + } + WinHttpCloseHandle(request); + } + WinHttpCloseHandle(connection); + } + WinHttpCloseHandle(session); + // The response body is never read, matching the contract in platform.h: the one caller has + // nothing to do with it, and the handles close either way. + return ok; +#elif defined(MM_HAVE_CURL) if (!url || !*url) return false; // curl_global_init is NOT thread-safe and must run once before any easy handle. Doing it in a @@ -1033,6 +1112,9 @@ bool ethRestartFails_ = false; // simulated recovery failure // The bound raw socket, or -1 for capture mode (the default, and all any test sees). int ethRawFd_ = -1; unsigned ethRawIfIndex_ = 0; // Linux AF_PACKET needs the index; BPF binds by name +// The name the raw sender bound to, so ethLinkUp/ethLinkSpeedMbps describe THAT NIC rather than +// whichever one the host lists first. Windows keeps boundGuid_ for the same reason. +char ethRawIfName_[64] = {}; #ifdef _WIN32 // --- Npcap/WinPcap, loaded at RUN TIME --------------------------------------------------------- @@ -1351,6 +1433,7 @@ bool ethBindRawInterface(const char* ifName) { #else if (ethRawFd_ >= 0) { ::close(ethRawFd_); ethRawFd_ = -1; } ethRawIfIndex_ = 0; + ethRawIfName_[0] = '\0'; if (!ifName || !ifName[0]) return true; // explicit return to capture mode #if defined(__linux__) @@ -1360,6 +1443,7 @@ bool ethBindRawInterface(const char* ifName) { if (idx == 0) { ::close(fd); return false; } ethRawFd_ = fd; ethRawIfIndex_ = idx; + std::snprintf(ethRawIfName_, sizeof(ethRawIfName_), "%s", ifName); return true; #elif defined(__APPLE__) // BPF has no single device: open the first free /dev/bpfN, then bind it to the interface. @@ -1377,6 +1461,7 @@ bool ethBindRawInterface(const char* ifName) { unsigned hdrComplete = 1; if (::ioctl(fd, BIOCSHDRCMPLT, &hdrComplete) < 0) { ::close(fd); return false; } ethRawFd_ = fd; + std::snprintf(ethRawIfName_, sizeof(ethRawIfName_), "%s", ifName); return true; } return false; @@ -1570,9 +1655,90 @@ uint16_t ethLinkSpeedMbps() MM_NONBLOCKING { return ethTestLinkSpeed_; // unbound, or a speed Windows would not state } #else -bool ethLinkUp() MM_NONBLOCKING { return false; } -bool ethConnected() MM_NONBLOCKING { return false; } -uint16_t ethLinkSpeedMbps() MM_NONBLOCKING { return ethTestLinkSpeed_; } +namespace { +/// (carrier up, Mbit) for a named interface. The ONE place either question is asked of the OS. +/// +/// Both callers need it: the link-state query below describes the NIC the sender bound to, and the +/// interface labels name a speed beside each adapter. They were two copies of the same ioctl and +/// the same six-case subtype table, which is two chances to drift when a rate is added. +/// +/// `mbps` of 0 means the OS states no rate (a virtual interface, a down link, or macOS Wi-Fi +/// reporting only "autoselect"), which is honest rather than a guess. +bool posixIfLink(const char* ifname, uint16_t& mbps) MM_NONBLOCKING { + mbps = 0; + if (!ifname || !*ifname) return false; +#if defined(__linux__) + // operstate is the kernel's own word for the carrier: "up", "down", or "unknown" for a virtual + // interface with no carrier concept. Only "up" counts. + char path[128]; + std::snprintf(path, sizeof(path), "/sys/class/net/%s/operstate", ifname); + FILE* f = std::fopen(path, "r"); + if (!f) return false; + char state[16] = {}; + const bool read = std::fscanf(f, "%15s", state) == 1; + std::fclose(f); + if (!read || std::strcmp(state, "up") != 0) return false; + // Speed rides along from the same sysfs tree, in Mbit. Absent or -1 where none is stated. + std::snprintf(path, sizeof(path), "/sys/class/net/%s/speed", ifname); + if (FILE* sf = std::fopen(path, "r")) { + long v = 0; + if (std::fscanf(sf, "%ld", &v) == 1 && v > 0) mbps = static_cast(v); + std::fclose(sf); + } + return true; +#elif defined(__APPLE__) + // IFM_ACTIVE is the carrier bit; the negotiated rate is encoded as the media SUBTYPE. + const int fd = ::socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) return false; + ifmediareq req{}; + // A name longer than ifm_name could never have bound (the kernel caps one at IFNAMSIZ), so + // refuse rather than query a truncated one and describe the wrong NIC. GCC proves the + // truncation is possible and rejects a plain snprintf without this. + if (std::strlen(ifname) >= sizeof(req.ifm_name)) { ::close(fd); return false; } + std::memcpy(req.ifm_name, ifname, std::strlen(ifname) + 1); + bool up = false; + if (::ioctl(fd, SIOCGIFMEDIA, &req) == 0 && (req.ifm_status & IFM_ACTIVE)) { + up = true; + switch (IFM_SUBTYPE(req.ifm_active)) { + case IFM_10_T: mbps = 10; break; + case IFM_100_TX: mbps = 100; break; + case IFM_1000_T: mbps = 1000; break; + case IFM_2500_T: mbps = 2500; break; + case IFM_5000_T: mbps = 5000; break; + case IFM_10G_T: mbps = 10000; break; + default: break; + } + } + ::close(fd); + return up; +#else + (void)ifname; + return false; // a host OS with no raw-L2 path: nothing to describe +#endif +} + +/// (link up, Mbit) for the interface the raw sender bound to, or (false, 0) when nothing is bound. +/// +/// Was a stub returning false on every non-Windows host, which made PanelCardDriver report "no +/// ethernet link" while it drove a card perfectly: the SEND path is implemented here (AF_PACKET on +/// Linux, BPF on macOS) and only the link-STATE query was missing, so the driver's health check +/// contradicted its own output. Reported by a user driving a ColorLight card from a NanoPi. +bool posixAdapterLink(uint16_t& mbps) MM_NONBLOCKING { + mbps = 0; + if (!ethRawIfName_[0]) return false; // capture mode, or a bind that failed + return posixIfLink(ethRawIfName_, mbps); +} +} // namespace + +bool ethLinkUp() MM_NONBLOCKING { uint16_t m = 0; return posixAdapterLink(m); } +bool ethConnected() MM_NONBLOCKING { return ethLinkUp(); } +uint16_t ethLinkSpeedMbps() MM_NONBLOCKING { + uint16_t m = 0; + if (posixAdapterLink(m)) return m; // bound and up: the OS's answer, 0 included + // Only when nothing is bound. A bound NIC that states no rate reports 0 above rather than + // this, because inventing a speed for a real adapter is worse than admitting none is known. + return ethTestLinkSpeed_; +} #endif size_t ethTestFrameCount() { return ethTestCount_; } @@ -3128,44 +3294,13 @@ size_t rawInterfaces(const char* const** optionsOut) { // (a virtual interface, a link that is down, Wi-Fi on macOS which reports only // "autoselect"). Rides in the label for the same reason as the Windows branch: the // name alone does not say which entry is the 1 Gb NIC and which is a tunnel. + // posixIfLink is the one place either the carrier or the rate is asked of the OS; + // the label wants only the rate, so it discards the carrier. This was a second copy + // of the same ioctl and the same subtype table. auto linkMbps = [](const char* ifname) -> unsigned { -#ifdef __linux__ - // sysfs states it directly, in Mbit. Absent or -1 for a virtual or down link. - char path[128]; - std::snprintf(path, sizeof(path), "/sys/class/net/%s/speed", ifname); - FILE* f = std::fopen(path, "r"); - if (!f) return 0; - long v = 0; - const bool ok = std::fscanf(f, "%ld", &v) == 1; - std::fclose(f); - return (ok && v > 0) ? static_cast(v) : 0; -#elif defined(__APPLE__) - // macOS has no speed field: the negotiated rate is encoded as the media - // SUBTYPE, so map the ones that name a rate. Wi-Fi and "autoselect" report no - // subtype we can turn into a number, which is exactly when 0 is the honest - // answer rather than a guess. - const int fd = ::socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) return 0; - ifmediareq req{}; - std::snprintf(req.ifm_name, sizeof(req.ifm_name), "%s", ifname); - unsigned mbps = 0; - if (::ioctl(fd, SIOCGIFMEDIA, &req) == 0 && (req.ifm_status & IFM_ACTIVE)) { - switch (IFM_SUBTYPE(req.ifm_active)) { - case IFM_10_T: mbps = 10; break; - case IFM_100_TX: mbps = 100; break; - case IFM_1000_T: mbps = 1000; break; - case IFM_2500_T: mbps = 2500; break; - case IFM_5000_T: mbps = 5000; break; - case IFM_10G_T: mbps = 10000; break; - default: break; - } - } - ::close(fd); + uint16_t mbps = 0; + posixIfLink(ifname, mbps); return mbps; -#else - (void)ifname; - return 0; -#endif }; auto isVirtual = [](const char* n) { diff --git a/src/platform/platform.h b/src/platform/platform.h index fa441c2c..a5dba583 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -862,17 +862,19 @@ int httpRequest(const char* method, const char* host, uint16_t port, const char* // it needs TLS, certificate verification and DNS, none of which we should be writing ourselves. // // Each platform uses the TLS ITS OS ALREADY SHIPS, so nothing is vendored: `esp_http_client` with -// `esp_crt_bundle` on ESP32 (the same pair the OTA path uses), and libcurl on desktop, which is -// present on macOS and every Linux distribution and carries its own certificate handling. +// `esp_crt_bundle` on ESP32 (the same pair the OTA path uses), WinHTTP on Windows (in the SDK, +// Schannel and the Windows certificate store), and libcurl on macOS and Linux, present on both and +// carrying its own certificate handling. // // Response body is discarded: the one caller (MoonCloud Stats) has nothing to do with it, and // buffering a reply from an untrusted server is a risk with no benefit. Blocking, so callers run // it off the render path. bool httpsPost(const char* url, const char* body, uint32_t timeoutMs); -/// Whether this build can make an outbound HTTPS request at all. False only on a desktop build -/// compiled without libcurl, where `httpsPost` always returns false: a structural inability rather -/// than a failed attempt, which a caller must be able to tell apart from network loss. +/// Whether this build can make an outbound HTTPS request at all. False only on a macOS or Linux +/// build compiled without libcurl, where `httpsPost` always returns false: a structural inability +/// rather than a failed attempt, which a caller must be able to tell apart from network loss. +/// Windows is always true, since WinHTTP ships with the OS and needs nothing found at build time. bool httpsAvailable() MM_NONBLOCKING; // Improv WiFi provisioning over UART0. diff --git a/src/ui/app.js b/src/ui/app.js index d3a24c8e..13776767 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -554,12 +554,12 @@ async function sendControl(moduleName, controlName, value) { // switched. The routine WS push cannot carry it: renderCards is suppressed while the user // is interacting, and operating this select is exactly that. else if (moduleName === "Firmware" && controlName === "image") refetchState(); - // Three writes change what a MoonCloud card shows: pressing `send` publishes a message, and - // either member's `consent` decides whether that member reads at all. Typing in `message` or - // toggling `shareName` changes only the device, and re-reading after those showed exactly - // what was already on screen. + // Four writes change what a MoonCloud card shows: pressing `send` publishes a message, + // pressing `send update` re-sends this device's report, and either member's `consent` decides + // whether that member reads at all. Typing in `message` or toggling `shareName` changes only + // the device, and re-reading after those showed exactly what was already on screen. else if (mod?.type === "MoonTalkModule" ? (controlName === "send" || controlName === "consent") - : mod?.type === "MoonStatsModule" ? controlName === "consent" + : mod?.type === "MoonStatsModule" ? (controlName === "consent" || controlName === "send update") : false) { // Switching consent OFF changes nothing on the server (a report already sent stays // sent), so the card rebuilds from what it has and nothing is re-read. The rebuild is @@ -577,6 +577,9 @@ async function sendControl(moduleName, controlName, value) { moonCloudStatsCache.clear(); moonTalkCache = null; refetchState(); + // The wait is for `consent` alone: that report leaves from tick1s, so an immediate read + // would show the totals without the very row the reader is looking for. `send update` + // sends inside this write, like MoonTalk's `send`, so it re-reads with no delay. }, mod?.type === "MoonStatsModule" && controlName === "consent" ? 2000 : 0); } } catch (e) { @@ -1985,7 +1988,15 @@ function createCard(mod, depth) { if (statsConsent === false) { const nudge = document.createElement("div"); nudge.className = "mooncloud-nudge"; - nudge.textContent = "Turn on MoonCloud stats to share and see what everyone else is running."; + nudge.textContent = "Turn on MoonCloud stats to share and see what everyone else is running. "; + // The reasons live in one place, and it is a page rather than a paragraph here: a nudge + // long enough to make the case stops being a nudge. + const why = document.createElement("a"); + why.href = "https://moonmodules.org/projectMM/mooncloud.html#why-you-might-like-this"; + why.target = "_blank"; + why.rel = "noopener"; + why.textContent = "Why you might like this"; + nudge.appendChild(why); host.appendChild(nudge); } @@ -6261,7 +6272,7 @@ function renderMoonCloudStats(host, mod) { ["Flash", "flash", "flash"], ["PSRAM", "psram", "psram"], ["SDK", "sdk", "sdk"], - ["Install or upgrade", "events", "event"], + ["Report event", "events", "event"], ["Upgraded from", "previousVersions", "previousVersion"], ["Drivers", "drivers", "driver"], ["Services", "services", "service"], diff --git a/test/js/mooncloud-report.test.mjs b/test/js/mooncloud-report.test.mjs index d4896c88..e949f9b5 100644 --- a/test/js/mooncloud-report.test.mjs +++ b/test/js/mooncloud-report.test.mjs @@ -56,6 +56,19 @@ test("a report cannot smuggle a field the firmware never sends", () => { assert.equal(row.chip, "ESP32-S3"); }); +// The event reaches every user's card as a pie legend, so the worker constrains it to the words it +// knows. "refresh" is the button on the Stats card: the same payload re-sent because a setup changed +// without a version change, and it must survive the filter or the button silently does nothing. +test("the event is one of the three words the card can show", () => { + const accepted = source.slice(source.indexOf('report.event !== undefined'), + source.indexOf('const row = clean(')); + for (const word of ["install", "upgrade", "refresh"]) { + assert.ok(accepted.includes(`"${word}"`), `${word} must be accepted`); + } + // Anything else is dropped rather than stored: a legend cannot render a word we never chose. + assert.ok(accepted.includes("202"), "an unknown event is accepted and discarded"); +}); + test("the country comes from the edge, never from the report", () => { // A caller claiming a different country must not be believed: the value is the one Cloudflare // resolved, and the whole reason it is trustworthy is that no address was stored to derive it. diff --git a/test/js/ui-mooncloud.test.mjs b/test/js/ui-mooncloud.test.mjs index adfa4126..b567a39d 100644 --- a/test/js/ui-mooncloud.test.mjs +++ b/test/js/ui-mooncloud.test.mjs @@ -50,8 +50,8 @@ test("the card re-reads only on a write that changed the server, once", () => { const trigger = app.slice(i, i + 260); assert.match(trigger, /controlName === "send" \|\| controlName === "consent"/, "Talk must rebuild on its own consent, not only on send"); - assert.match(trigger, /"MoonStatsModule" \? controlName === "consent"/, - "Stats rebuilds on its consent"); + assert.match(trigger, /"MoonStatsModule" \? \(controlName === "consent" \|\| controlName === "send update"\)/, + "Stats rebuilds on its consent, and on the button that re-sends its report"); const rule = app.slice(i, app.indexOf("\n }", i) + 1); assert.ok(rule.includes("refetchState"), "the rule must refresh the card"); assert.ok(!/\.value\s*=\s*""/.test(rule), "it must not reach into the DOM"); diff --git a/test/scenarios/core/scenario_MoonModule_control_change.json b/test/scenarios/core/scenario_MoonModule_control_change.json index e18acf82..cd24259d 100644 --- a/test/scenarios/core/scenario_MoonModule_control_change.json +++ b/test/scenarios/core/scenario_MoonModule_control_change.json @@ -117,14 +117,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 126, - "p95": 246, + "p50": 125, + "p95": 196, "min": 118, - "max": 302, + "max": 246, "n": 32, - "samples": [302, 129, 118, 246, 120, 133, 122, 121, 122, 126, 124, 118, 118, 120, 123, 196, 125, 129, 130, 144, 160, 120, 161, 163, 194, 186, 133, 122, 196, 123, 194, 119] + "samples": [118, 246, 120, 133, 122, 121, 122, 126, 124, 118, 118, 120, 123, 196, 125, 129, 130, 144, 160, 120, 161, 163, 194, 186, 133, 122, 196, 123, 194, 119, 121, 126] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { @@ -299,14 +299,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 125, - "p95": 146, + "p50": 124, + "p95": 145, "min": 118, "max": 260, "n": 32, - "samples": [135, 146, 121, 260, 123, 118, 120, 124, 123, 137, 125, 118, 124, 120, 123, 132, 126, 130, 131, 145, 123, 120, 125, 135, 131, 129, 131, 123, 132, 124, 130, 119] + "samples": [121, 260, 123, 118, 120, 124, 123, 137, 125, 118, 124, 120, 123, 132, 126, 130, 131, 145, 123, 120, 125, 135, 131, 129, 131, 123, 132, 124, 130, 119, 122, 127] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { @@ -486,9 +486,9 @@ "min": 115, "max": 152, "n": 32, - "samples": [129, 122, 121, 152, 122, 120, 120, 126, 123, 128, 123, 120, 126, 120, 124, 117, 136, 130, 130, 125, 118, 118, 120, 116, 118, 117, 120, 121, 120, 121, 115, 119] + "samples": [121, 152, 122, 120, 120, 126, 123, 128, 123, 120, 126, 120, 124, 117, 136, 130, 130, 125, 118, 118, 120, 116, 118, 117, 120, 121, 120, 121, 115, 119, 125, 125] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { @@ -676,9 +676,9 @@ "min": 117, "max": 201, "n": 32, - "samples": [141, 124, 121, 151, 122, 119, 121, 123, 121, 129, 120, 129, 126, 118, 120, 120, 201, 129, 129, 125, 121, 126, 122, 117, 119, 119, 120, 121, 121, 119, 118, 119] + "samples": [121, 151, 122, 119, 121, 123, 121, 129, 120, 129, 126, 118, 120, 120, 201, 129, 129, 125, 121, 126, 122, 117, 119, 119, 120, 121, 121, 119, 118, 119, 122, 125] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { diff --git a/test/scenarios/light/scenario_Audio_mutation.json b/test/scenarios/light/scenario_Audio_mutation.json index e9b5ddeb..f878b725 100644 --- a/test/scenarios/light/scenario_Audio_mutation.json +++ b/test/scenarios/light/scenario_Audio_mutation.json @@ -105,13 +105,13 @@ "desktop-macos": { "tick_us": { "p50": 17, - "p95": 25, + "p95": 22, "min": 16, - "max": 26, + "max": 25, "n": 32, - "samples": [26, 16, 17, 25, 19, 20, 20, 17, 20, 22, 17, 17, 17, 20, 20, 16, 21, 19, 19, 17, 16, 17, 16, 16, 16, 16, 16, 20, 16, 20, 16, 19] + "samples": [17, 25, 19, 20, 20, 17, 20, 22, 17, 17, 17, 20, 20, 16, 21, 19, 19, 17, 16, 17, 16, 16, 16, 16, 16, 20, 16, 20, 16, 19, 18, 21] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -201,14 +201,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 21, + "p50": 20, "p95": 137, "min": 17, "max": 216, "n": 32, - "samples": [28, 24, 137, 27, 17, 20, 19, 39, 20, 31, 22, 216, 36, 21, 18, 18, 43, 20, 41, 23, 18, 67, 37, 18, 17, 25, 19, 19, 21, 18, 20, 21] + "samples": [137, 27, 17, 20, 19, 39, 20, 31, 22, 216, 36, 21, 18, 18, 43, 20, 41, 23, 18, 67, 37, 18, 17, 25, 19, 19, 21, 18, 20, 21, 18, 20] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -320,9 +320,9 @@ "min": 17, "max": 108, "n": 32, - "samples": [28, 30, 40, 32, 26, 18, 20, 36, 21, 25, 21, 108, 24, 20, 19, 18, 37, 20, 56, 23, 21, 65, 29, 20, 18, 28, 18, 20, 17, 19, 19, 21] + "samples": [40, 32, 26, 18, 20, 36, 21, 25, 21, 108, 24, 20, 19, 18, 37, 20, 56, 23, 21, 65, 29, 20, 18, 28, 18, 20, 17, 19, 19, 21, 19, 20] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -412,14 +412,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 24, + "p50": 23, "p95": 61, "min": 18, "max": 62, "n": 32, - "samples": [28, 31, 44, 29, 29, 21, 24, 30, 24, 30, 22, 61, 25, 23, 22, 19, 27, 22, 62, 25, 21, 47, 19, 22, 19, 38, 22, 20, 18, 24, 24, 22] + "samples": [44, 29, 29, 21, 24, 30, 24, 30, 22, 61, 25, 23, 22, 19, 27, 22, 62, 25, 21, 47, 19, 22, 19, 38, 22, 20, 18, 24, 24, 22, 23, 21] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -507,14 +507,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 22, + "p50": 21, "p95": 37, "min": 18, "max": 74, "n": 32, - "samples": [28, 22, 29, 36, 19, 21, 23, 25, 21, 21, 19, 74, 22, 23, 27, 19, 25, 24, 37, 25, 20, 28, 21, 20, 18, 26, 21, 20, 18, 35, 20, 23] + "samples": [29, 36, 19, 21, 23, 25, 21, 21, 19, 74, 22, 23, 27, 19, 25, 24, 37, 25, 20, 28, 21, 20, 18, 26, 21, 20, 18, 35, 20, 23, 21, 21] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -607,9 +607,9 @@ "min": 17, "max": 57, "n": 32, - "samples": [25, 17, 20, 24, 19, 17, 20, 22, 21, 19, 17, 57, 21, 20, 20, 17, 24, 20, 30, 22, 17, 33, 17, 17, 20, 18, 20, 20, 17, 21, 21, 19] + "samples": [20, 24, 19, 17, 20, 22, 21, 19, 17, 57, 21, 20, 20, 17, 24, 20, 30, 22, 17, 33, 17, 17, 20, 18, 20, 20, 17, 21, 21, 19, 21, 20] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Aurora_fps.json b/test/scenarios/light/scenario_Aurora_fps.json index d402abdd..92c29451 100644 --- a/test/scenarios/light/scenario_Aurora_fps.json +++ b/test/scenarios/light/scenario_Aurora_fps.json @@ -84,14 +84,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 879, + "p50": 875, "p95": 1751, "min": 821, "max": 2054, "n": 32, - "samples": [858, 885, 1527, 1317, 853, 859, 861, 1047, 962, 1024, 870, 1427, 907, 879, 833, 937, 1314, 1009, 2054, 1751, 859, 1266, 840, 873, 821, 923, 837, 846, 890, 853, 875, 854] + "samples": [1527, 1317, 853, 859, 861, 1047, 962, 1024, 870, 1427, 907, 879, 833, 937, 1314, 1009, 2054, 1751, 859, 1266, 840, 873, 821, 923, 837, 846, 890, 853, 875, 854, 851, 843] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -114,14 +114,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 322, + "p50": 321, "p95": 607, "min": 308, "max": 1465, "n": 32, - "samples": [322, 495, 395, 449, 317, 318, 317, 431, 362, 359, 340, 522, 317, 318, 315, 361, 607, 374, 1465, 489, 316, 442, 314, 312, 308, 322, 318, 317, 321, 321, 315, 326] + "samples": [395, 449, 317, 318, 317, 431, 362, 359, 340, 522, 317, 318, 315, 361, 607, 374, 1465, 489, 316, 442, 314, 312, 308, 322, 318, 317, 321, 321, 315, 326, 323, 320] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -149,9 +149,9 @@ "min": 181, "max": 429, "n": 32, - "samples": [188, 222, 232, 245, 186, 188, 185, 201, 205, 212, 203, 406, 185, 185, 193, 212, 429, 221, 331, 275, 186, 285, 185, 181, 181, 189, 188, 189, 190, 191, 185, 189] + "samples": [232, 245, 186, 188, 185, 201, 205, 212, 203, 406, 185, 185, 193, 212, 429, 221, 331, 275, 186, 285, 185, 181, 181, 189, 188, 189, 190, 191, 185, 189, 189, 195] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -174,14 +174,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 559, + "p50": 567, "p95": 1037, "min": 532, "max": 1419, "n": 32, - "samples": [553, 567, 702, 720, 550, 545, 552, 583, 570, 599, 637, 1419, 542, 542, 578, 620, 1011, 631, 1037, 772, 541, 642, 538, 532, 535, 559, 551, 546, 549, 549, 541, 607] + "samples": [702, 720, 550, 545, 552, 583, 570, 599, 637, 1419, 542, 542, 578, 620, 1011, 631, 1037, 772, 541, 642, 538, 532, 535, 559, 551, 546, 549, 549, 541, 607, 567, 719] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -204,14 +204,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 960, + "p50": 977, "p95": 1647, "min": 915, "max": 2085, "n": 32, - "samples": [953, 952, 1158, 1152, 946, 1029, 970, 1026, 955, 1046, 996, 1647, 938, 936, 934, 1055, 1373, 1070, 2085, 1253, 934, 1063, 1007, 918, 915, 960, 1015, 940, 945, 949, 938, 934] + "samples": [1158, 1152, 946, 1029, 970, 1026, 955, 1046, 996, 1647, 938, 936, 934, 1055, 1373, 1070, 2085, 1253, 934, 1063, 1007, 918, 915, 960, 1015, 940, 945, 949, 938, 934, 977, 1001] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -239,9 +239,9 @@ "min": 1410, "max": 2727, "n": 32, - "samples": [1467, 1490, 1853, 1666, 1473, 1474, 1871, 1512, 1472, 1672, 1547, 1784, 1445, 1443, 1441, 1616, 1865, 1631, 2727, 1797, 1431, 1894, 1531, 1417, 1410, 1461, 1454, 1453, 1462, 1460, 1448, 1443] + "samples": [1853, 1666, 1473, 1474, 1871, 1512, 1472, 1672, 1547, 1784, 1445, 1443, 1441, 1616, 1865, 1631, 2727, 1797, 1431, 1894, 1531, 1417, 1410, 1461, 1454, 1453, 1462, 1460, 1448, 1443, 1470, 1502] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -264,14 +264,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 1556, + "p50": 1573, "p95": 2013, "min": 1488, "max": 4090, "n": 32, - "samples": [1526, 1556, 1761, 1695, 1534, 1514, 1828, 1601, 1518, 1642, 4090, 1584, 1505, 1488, 1490, 1685, 1792, 1669, 2013, 1785, 1525, 1655, 1573, 1496, 1498, 1899, 1508, 1648, 1506, 1505, 1505, 1493] + "samples": [1761, 1695, 1534, 1514, 1828, 1601, 1518, 1642, 4090, 1584, 1505, 1488, 1490, 1685, 1792, 1669, 2013, 1785, 1525, 1655, 1573, 1496, 1498, 1899, 1508, 1648, 1506, 1505, 1505, 1493, 1624, 1526] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -295,13 +295,13 @@ "desktop-macos": { "tick_us": { "p50": 1119, - "p95": 1443, + "p95": 1828, "min": 1078, "max": 2118, "n": 32, - "samples": [1120, 1131, 1303, 1210, 1117, 1118, 1105, 1167, 1161, 1167, 2118, 1144, 1111, 1097, 1110, 1257, 1325, 1214, 1351, 1260, 1087, 1443, 1096, 1078, 1088, 1131, 1119, 1117, 1109, 1117, 1101, 1099] + "samples": [1303, 1210, 1117, 1118, 1105, 1167, 1161, 1167, 2118, 1144, 1111, 1097, 1110, 1257, 1325, 1214, 1351, 1260, 1087, 1443, 1096, 1078, 1088, 1131, 1119, 1117, 1109, 1117, 1101, 1099, 1828, 1258] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } } diff --git a/test/scenarios/light/scenario_Driver_mutation.json b/test/scenarios/light/scenario_Driver_mutation.json index 3f5fe4b6..5da693e6 100644 --- a/test/scenarios/light/scenario_Driver_mutation.json +++ b/test/scenarios/light/scenario_Driver_mutation.json @@ -76,14 +76,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 18, - "p95": 21, + "p50": 19, + "p95": 32, "min": 16, - "max": 32, + "max": 46, "n": 32, - "samples": [16, 19, 19, 17, 19, 18, 20, 18, 17, 17, 32, 17, 16, 17, 17, 18, 18, 17, 18, 18, 20, 21, 19, 20, 20, 21, 19, 21, 20, 16, 20, 20] + "samples": [19, 17, 19, 18, 20, 18, 17, 17, 32, 17, 16, 17, 17, 18, 18, 17, 18, 18, 20, 21, 19, 20, 20, 21, 19, 21, 20, 16, 20, 20, 46, 23] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -174,13 +174,13 @@ "desktop-macos": { "tick_us": { "p50": 20, - "p95": 23, + "p95": 29, "min": 16, - "max": 29, + "max": 38, "n": 32, - "samples": [16, 20, 20, 18, 20, 20, 20, 23, 17, 17, 29, 17, 17, 17, 20, 18, 18, 17, 18, 18, 20, 21, 20, 20, 20, 20, 20, 20, 20, 16, 20, 19] + "samples": [20, 18, 20, 20, 20, 23, 17, 17, 29, 17, 17, 17, 20, 18, 18, 17, 18, 18, 20, 21, 20, 20, 20, 20, 20, 20, 20, 16, 20, 19, 38, 19] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -271,13 +271,13 @@ "desktop-macos": { "tick_us": { "p50": 19, - "p95": 21, - "min": 16, + "p95": 22, + "min": 17, "max": 37, "n": 32, - "samples": [16, 20, 19, 17, 20, 20, 20, 21, 18, 17, 37, 17, 20, 19, 20, 18, 18, 17, 18, 18, 20, 20, 20, 19, 20, 20, 19, 20, 20, 18, 20, 19] + "samples": [19, 17, 20, 20, 20, 21, 18, 17, 37, 17, 20, 19, 20, 18, 18, 17, 18, 18, 20, 20, 20, 19, 20, 20, 19, 20, 20, 18, 20, 19, 22, 17] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -366,13 +366,13 @@ "desktop-macos": { "tick_us": { "p50": 20, - "p95": 21, + "p95": 29, "min": 17, "max": 33, "n": 32, - "samples": [20, 20, 19, 21, 19, 20, 20, 20, 20, 17, 33, 17, 20, 20, 20, 18, 18, 17, 19, 18, 20, 20, 20, 20, 20, 20, 20, 21, 20, 20, 20, 19] + "samples": [19, 21, 19, 20, 20, 20, 20, 17, 33, 17, 20, 20, 20, 18, 18, 17, 19, 18, 20, 20, 20, 20, 20, 20, 20, 21, 20, 20, 20, 19, 19, 29] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -465,9 +465,9 @@ "min": 17, "max": 36, "n": 32, - "samples": [20, 20, 19, 17, 19, 19, 19, 21, 20, 17, 36, 17, 20, 20, 20, 18, 18, 17, 19, 18, 20, 19, 20, 20, 20, 19, 20, 19, 20, 20, 20, 19] + "samples": [19, 17, 19, 19, 19, 21, 20, 17, 36, 17, 20, 20, 20, 18, 18, 17, 19, 18, 20, 19, 20, 20, 20, 19, 20, 19, 20, 20, 20, 19, 20, 18] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Effects_composition.json b/test/scenarios/light/scenario_Effects_composition.json index 7229e6dd..5ffc7882 100644 --- a/test/scenarios/light/scenario_Effects_composition.json +++ b/test/scenarios/light/scenario_Effects_composition.json @@ -107,13 +107,13 @@ "desktop-macos": { "tick_us": { "p50": 146, - "p95": 169, + "p95": 309, "min": 143, - "max": 309, + "max": 656, "n": 32, - "samples": [148, 164, 169, 144, 147, 144, 147, 146, 143, 152, 309, 152, 143, 144, 147, 153, 155, 147, 161, 153, 143, 151, 145, 145, 143, 145, 144, 144, 144, 145, 147, 143] + "samples": [169, 144, 147, 144, 147, 146, 143, 152, 309, 152, 143, 144, 147, 153, 155, 147, 161, 153, 143, 151, 145, 145, 143, 145, 144, 144, 144, 145, 147, 143, 158, 656] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Fields_polar_lut.json b/test/scenarios/light/scenario_Fields_polar_lut.json index 608c95a5..8d9f12c6 100644 --- a/test/scenarios/light/scenario_Fields_polar_lut.json +++ b/test/scenarios/light/scenario_Fields_polar_lut.json @@ -85,14 +85,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 293, + "p50": 297, "p95": 421, "min": 274, "max": 808, "n": 32, - "samples": [287, 294, 337, 332, 293, 290, 300, 301, 421, 298, 808, 301, 288, 284, 297, 314, 323, 305, 336, 335, 274, 297, 281, 279, 278, 290, 293, 285, 285, 283, 286, 284] + "samples": [337, 332, 293, 290, 300, 301, 421, 298, 808, 301, 288, 284, 297, 314, 323, 305, 336, 335, 274, 297, 281, 279, 278, 290, 293, 285, 285, 283, 286, 284, 421, 369] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -116,13 +116,13 @@ "desktop-macos": { "tick_us": { "p50": 289, - "p95": 340, + "p95": 373, "min": 274, - "max": 373, + "max": 524, "n": 32, - "samples": [290, 293, 340, 320, 284, 287, 285, 289, 295, 295, 322, 296, 281, 282, 289, 311, 323, 301, 333, 373, 274, 292, 279, 276, 276, 290, 290, 283, 284, 287, 283, 284] + "samples": [340, 320, 284, 287, 285, 289, 295, 295, 322, 296, 281, 282, 289, 311, 323, 301, 333, 373, 274, 292, 279, 276, 276, 290, 290, 283, 284, 287, 283, 284, 308, 524] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -145,14 +145,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 286, + "p50": 287, "p95": 337, "min": 275, - "max": 337, + "max": 372, "n": 32, - "samples": [309, 284, 337, 317, 285, 283, 284, 289, 296, 287, 302, 287, 282, 280, 288, 311, 322, 301, 321, 334, 275, 337, 278, 277, 281, 286, 283, 284, 283, 289, 280, 282] + "samples": [337, 317, 285, 283, 284, 289, 296, 287, 302, 287, 282, 280, 288, 311, 322, 301, 321, 334, 275, 337, 278, 277, 281, 286, 283, 284, 283, 289, 280, 282, 297, 372] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -176,13 +176,13 @@ "desktop-macos": { "tick_us": { "p50": 285, - "p95": 339, + "p95": 334, "min": 274, - "max": 347, + "max": 339, "n": 32, - "samples": [347, 283, 339, 306, 284, 293, 285, 295, 284, 289, 299, 283, 282, 281, 285, 325, 322, 301, 323, 334, 274, 334, 281, 275, 278, 283, 282, 286, 287, 283, 282, 281] + "samples": [339, 306, 284, 293, 285, 295, 284, 289, 299, 283, 282, 281, 285, 325, 322, 301, 323, 334, 274, 334, 281, 275, 278, 283, 282, 286, 287, 283, 282, 281, 297, 324] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -210,9 +210,9 @@ "min": 135, "max": 253, "n": 32, - "samples": [155, 141, 166, 153, 141, 140, 140, 146, 144, 147, 146, 141, 141, 138, 140, 169, 160, 149, 160, 164, 135, 253, 140, 139, 136, 141, 139, 140, 141, 141, 139, 139] + "samples": [166, 153, 141, 140, 140, 146, 144, 147, 146, 141, 141, 138, 140, 169, 160, 149, 160, 164, 135, 253, 140, 139, 136, 141, 139, 140, 141, 141, 139, 139, 146, 156] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -236,13 +236,13 @@ "desktop-macos": { "tick_us": { "p50": 431, - "p95": 512, + "p95": 502, "min": 407, "max": 622, "n": 32, - "samples": [433, 512, 502, 461, 424, 421, 425, 443, 431, 448, 448, 421, 421, 422, 418, 481, 479, 458, 479, 494, 407, 622, 421, 411, 414, 439, 421, 420, 446, 463, 419, 418] + "samples": [502, 461, 424, 421, 425, 443, 431, 448, 448, 421, 421, 422, 418, 481, 479, 458, 479, 494, 407, 622, 421, 411, 414, 439, 421, 420, 446, 463, 419, 418, 434, 492] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -271,14 +271,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 1298, - "p95": 1892, + "p50": 1320, + "p95": 1686, "min": 1219, "max": 2429, "n": 32, - "samples": [1236, 1892, 1402, 1341, 1261, 1229, 1250, 1339, 1271, 1686, 1458, 1231, 1320, 1238, 1225, 1355, 1349, 1298, 1354, 1396, 1237, 2429, 1286, 1229, 1219, 1531, 1219, 1451, 1439, 1624, 1275, 1237] + "samples": [1402, 1341, 1261, 1229, 1250, 1339, 1271, 1686, 1458, 1231, 1320, 1238, 1225, 1355, 1349, 1298, 1354, 1396, 1237, 2429, 1286, 1229, 1219, 1531, 1219, 1451, 1439, 1624, 1275, 1237, 1551, 1340] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -306,9 +306,9 @@ "min": 461, "max": 1469, "n": 32, - "samples": [481, 491, 538, 530, 477, 475, 477, 1469, 496, 528, 500, 473, 468, 509, 476, 518, 520, 502, 568, 534, 471, 540, 479, 464, 461, 478, 473, 474, 475, 479, 476, 482] + "samples": [538, 530, 477, 475, 477, 1469, 496, 528, 500, 473, 468, 509, 476, 518, 520, 502, 568, 534, 471, 540, 479, 464, 461, 478, 473, 474, 475, 479, 476, 482, 482, 520] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } } diff --git a/test/scenarios/light/scenario_Fluid_solver.json b/test/scenarios/light/scenario_Fluid_solver.json index be722400..9cc46183 100644 --- a/test/scenarios/light/scenario_Fluid_solver.json +++ b/test/scenarios/light/scenario_Fluid_solver.json @@ -83,9 +83,9 @@ "min": 28, "max": 56, "n": 32, - "samples": [29, 30, 33, 32, 29, 29, 29, 56, 30, 33, 31, 30, 30, 29, 30, 32, 32, 30, 33, 33, 28, 33, 29, 28, 28, 29, 29, 29, 30, 29, 30, 29] + "samples": [33, 32, 29, 29, 29, 56, 30, 33, 31, 30, 30, 29, 30, 32, 32, 30, 33, 33, 28, 33, 29, 28, 28, 29, 29, 29, 30, 29, 30, 29, 33, 31] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -105,9 +105,9 @@ "min": 18, "max": 44, "n": 32, - "samples": [19, 20, 21, 21, 19, 19, 19, 44, 19, 21, 21, 19, 20, 19, 19, 21, 21, 20, 22, 22, 19, 24, 19, 18, 19, 19, 19, 19, 19, 19, 19, 19] + "samples": [21, 21, 19, 19, 19, 44, 19, 21, 21, 19, 20, 19, 19, 21, 21, 20, 22, 22, 19, 24, 19, 18, 19, 19, 19, 19, 19, 19, 19, 19, 21, 21] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -122,14 +122,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 67, + "p50": 69, "p95": 76, "min": 66, "max": 115, "n": 32, - "samples": [67, 70, 76, 75, 67, 67, 67, 115, 70, 76, 70, 69, 70, 67, 69, 73, 73, 71, 76, 76, 66, 73, 66, 67, 66, 66, 66, 66, 66, 67, 66, 67] + "samples": [76, 75, 67, 67, 67, 115, 70, 76, 70, 69, 70, 67, 69, 73, 73, 71, 76, 76, 66, 73, 66, 67, 66, 66, 66, 66, 66, 67, 66, 67, 72, 72] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -149,9 +149,9 @@ "min": 28, "max": 71, "n": 32, - "samples": [29, 30, 33, 33, 29, 29, 29, 71, 29, 33, 30, 30, 31, 29, 30, 32, 32, 31, 33, 33, 29, 31, 28, 29, 28, 29, 29, 29, 29, 29, 29, 29] + "samples": [33, 33, 29, 29, 29, 71, 29, 33, 30, 30, 31, 29, 30, 32, 32, 31, 33, 33, 29, 31, 28, 29, 28, 29, 29, 29, 29, 29, 29, 29, 32, 32] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -171,9 +171,9 @@ "min": 61, "max": 95, "n": 32, - "samples": [62, 66, 71, 70, 63, 62, 62, 95, 67, 72, 66, 64, 65, 63, 66, 69, 70, 67, 72, 71, 62, 69, 63, 61, 61, 64, 62, 62, 63, 63, 62, 64] + "samples": [71, 70, 63, 62, 62, 95, 67, 72, 66, 64, 65, 63, 66, 69, 70, 67, 72, 71, 62, 69, 63, 61, 61, 64, 62, 62, 63, 63, 62, 64, 65, 70] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -192,9 +192,9 @@ "min": 126, "max": 208, "n": 32, - "samples": [128, 131, 145, 145, 131, 127, 129, 176, 208, 154, 139, 130, 136, 129, 136, 144, 143, 138, 149, 146, 127, 164, 128, 126, 127, 129, 128, 130, 129, 129, 127, 166] + "samples": [145, 145, 131, 127, 129, 176, 208, 154, 139, 130, 136, 129, 136, 144, 143, 138, 149, 146, 127, 164, 128, 126, 127, 129, 128, 130, 129, 129, 127, 166, 131, 146] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } }, "description": "And the height, making it 64x64: four times the cells of the 32x32 the pair started from. Reallocating on each axis separately is the shape a UI resize actually takes." @@ -215,9 +215,9 @@ "min": 127, "max": 264, "n": 32, - "samples": [138, 129, 145, 145, 131, 129, 132, 264, 150, 161, 145, 129, 137, 129, 131, 143, 143, 138, 147, 146, 128, 157, 128, 129, 127, 129, 129, 129, 129, 130, 128, 130] + "samples": [145, 145, 131, 129, 132, 264, 150, 161, 145, 129, 137, 129, 131, 143, 143, 138, 147, 146, 128, 157, 128, 129, 127, 129, 129, 129, 129, 130, 128, 130, 133, 157] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -237,9 +237,9 @@ "min": 125, "max": 472, "n": 32, - "samples": [129, 129, 146, 145, 129, 128, 127, 296, 138, 158, 135, 130, 130, 128, 135, 143, 143, 139, 149, 149, 127, 223, 130, 125, 128, 129, 127, 130, 129, 472, 127, 130] + "samples": [146, 145, 129, 128, 127, 296, 138, 158, 135, 130, 130, 128, 135, 143, 143, 139, 149, 149, 127, 223, 130, 125, 128, 129, 127, 130, 129, 472, 127, 130, 131, 143] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -259,9 +259,9 @@ "min": 34, "max": 106, "n": 32, - "samples": [35, 35, 40, 40, 35, 35, 35, 106, 37, 42, 37, 35, 34, 34, 37, 38, 38, 37, 40, 40, 34, 39, 35, 35, 35, 35, 35, 35, 35, 34, 34, 37] + "samples": [40, 40, 35, 35, 35, 106, 37, 42, 37, 35, 34, 34, 37, 38, 38, 37, 40, 40, 34, 39, 35, 35, 35, 35, 35, 35, 35, 34, 34, 37, 35, 37] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -280,9 +280,9 @@ "min": 10, "max": 80, "n": 32, - "samples": [11, 11, 12, 12, 11, 11, 10, 80, 11, 13, 11, 11, 10, 10, 11, 12, 12, 11, 12, 12, 10, 11, 11, 10, 10, 10, 10, 10, 10, 11, 10, 11] + "samples": [12, 12, 11, 11, 10, 80, 11, 13, 11, 11, 10, 10, 11, 12, 12, 11, 12, 12, 10, 11, 11, 10, 10, 10, 10, 10, 10, 11, 10, 11, 11, 11] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -296,14 +296,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 220, + "p50": 221, "p95": 265, "min": 214, "max": 326, "n": 32, - "samples": [226, 219, 249, 247, 217, 218, 215, 326, 222, 259, 228, 217, 215, 224, 265, 237, 236, 228, 246, 246, 214, 227, 216, 214, 218, 217, 220, 216, 216, 217, 215, 231] + "samples": [249, 247, 217, 218, 215, 326, 222, 259, 228, 217, 215, 224, 265, 237, 236, 228, 246, 246, 214, 227, 216, 214, 218, 217, 220, 216, 216, 217, 215, 231, 221, 235] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -323,9 +323,9 @@ "min": 10, "max": 13, "n": 32, - "samples": [11, 10, 12, 12, 11, 11, 11, 12, 12, 13, 11, 10, 11, 11, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11] + "samples": [12, 12, 11, 11, 11, 12, 12, 13, 11, 10, 11, 11, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -345,9 +345,9 @@ "min": 8, "max": 10, "n": 32, - "samples": [9, 8, 9, 10, 8, 9, 9, 10, 9, 10, 9, 9, 8, 8, 9, 9, 9, 9, 10, 10, 8, 9, 9, 9, 8, 8, 9, 8, 8, 8, 8, 9] + "samples": [9, 10, 8, 9, 9, 10, 9, 10, 9, 9, 8, 8, 9, 9, 9, 9, 10, 10, 8, 9, 9, 9, 8, 8, 9, 8, 8, 8, 8, 9, 9, 9] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -366,9 +366,9 @@ "min": 6, "max": 8, "n": 32, - "samples": [7, 6, 8, 7, 6, 7, 7, 8, 7, 8, 7, 6, 6, 7, 7, 7, 7, 7, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7] + "samples": [8, 7, 6, 7, 7, 8, 7, 8, 7, 6, 6, 7, 7, 7, 7, 7, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } } diff --git a/test/scenarios/light/scenario_GridBlacks_blackpixel.json b/test/scenarios/light/scenario_GridBlacks_blackpixel.json index 81158e34..1dfeb546 100644 --- a/test/scenarios/light/scenario_GridBlacks_blackpixel.json +++ b/test/scenarios/light/scenario_GridBlacks_blackpixel.json @@ -97,7 +97,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -198,9 +198,9 @@ "min": 2, "max": 3, "n": 32, - "samples": [2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + "samples": [2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_GridLayout_resize.json b/test/scenarios/light/scenario_GridLayout_resize.json index b9770f59..f30ad696 100644 --- a/test/scenarios/light/scenario_GridLayout_resize.json +++ b/test/scenarios/light/scenario_GridLayout_resize.json @@ -117,14 +117,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 122, + "p50": 123, "p95": 143, "min": 117, "max": 189, "n": 32, - "samples": [120, 124, 125, 126, 119, 189, 124, 141, 131, 143, 119, 121, 118, 118, 124, 121, 122, 120, 126, 129, 117, 126, 127, 117, 120, 121, 120, 118, 123, 124, 120, 124] + "samples": [125, 126, 119, 189, 124, 141, 131, 143, 119, 121, 118, 118, 124, 121, 122, 120, 126, 129, 117, 126, 127, 117, 120, 121, 120, 118, 123, 124, 120, 124, 126, 132] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { @@ -299,14 +299,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 65, + "p50": 64, "p95": 113, "min": 59, "max": 151, "n": 32, - "samples": [65, 66, 62, 63, 64, 60, 64, 67, 151, 69, 59, 66, 66, 113, 65, 61, 60, 61, 63, 67, 63, 64, 70, 64, 68, 65, 61, 65, 65, 66, 64, 68] + "samples": [62, 63, 64, 60, 64, 67, 151, 69, 59, 66, 66, 113, 65, 61, 60, 61, 63, 67, 63, 64, 70, 64, 68, 65, 61, 65, 65, 66, 64, 68, 66, 64] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { @@ -486,9 +486,9 @@ "min": 118, "max": 190, "n": 32, - "samples": [120, 122, 126, 127, 120, 120, 142, 190, 142, 142, 118, 123, 120, 121, 119, 121, 122, 121, 126, 135, 119, 126, 126, 118, 120, 120, 124, 119, 122, 121, 120, 125] + "samples": [126, 127, 120, 120, 142, 190, 142, 142, 118, 123, 120, 121, 119, 121, 122, 121, 126, 135, 119, 126, 126, 118, 120, 120, 124, 119, 122, 121, 120, 125, 121, 120] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth-wifi": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layer_base_pipeline.json b/test/scenarios/light/scenario_Layer_base_pipeline.json index e0dc57c3..0d4f831f 100644 --- a/test/scenarios/light/scenario_Layer_base_pipeline.json +++ b/test/scenarios/light/scenario_Layer_base_pipeline.json @@ -88,9 +88,9 @@ "min": 65, "max": 241, "n": 32, - "samples": [70, 70, 68, 69, 70, 67, 139, 241, 77, 77, 65, 68, 67, 70, 71, 67, 66, 67, 69, 72, 69, 72, 66, 66, 70, 71, 67, 71, 68, 70, 66, 71] + "samples": [68, 69, 70, 67, 139, 241, 77, 77, 65, 68, 67, 70, 71, 67, 66, 67, 69, 72, 69, 72, 66, 66, 70, 71, 67, 71, 68, 70, 66, 71, 70, 70] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layer_memory_1to1.json b/test/scenarios/light/scenario_Layer_memory_1to1.json index c6d24037..fa792466 100644 --- a/test/scenarios/light/scenario_Layer_memory_1to1.json +++ b/test/scenarios/light/scenario_Layer_memory_1to1.json @@ -85,9 +85,9 @@ "min": 4, "max": 24, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 6, 24, 5, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 5, 4, 5, 5, 5, 5, 5, 6, 5, 5] + "samples": [5, 5, 5, 5, 6, 24, 5, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 5, 4, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layouts_mutation.json b/test/scenarios/light/scenario_Layouts_mutation.json index 8bec3237..49589132 100644 --- a/test/scenarios/light/scenario_Layouts_mutation.json +++ b/test/scenarios/light/scenario_Layouts_mutation.json @@ -83,9 +83,9 @@ "min": 16, "max": 83, "n": 32, - "samples": [16, 17, 18, 18, 16, 17, 16, 83, 17, 20, 17, 17, 17, 17, 17, 17, 18, 18, 19, 19, 16, 17, 16, 16, 17, 17, 17, 18, 16, 16, 16, 17] + "samples": [18, 18, 16, 17, 16, 83, 17, 20, 17, 17, 17, 17, 17, 17, 18, 18, 19, 19, 16, 17, 16, 16, 17, 17, 17, 18, 16, 16, 16, 17, 19, 17] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -211,9 +211,9 @@ "min": 45, "max": 174, "n": 32, - "samples": [49, 48, 46, 49, 46, 50, 48, 174, 50, 72, 45, 47, 47, 47, 45, 45, 46, 46, 47, 53, 46, 53, 46, 50, 50, 50, 58, 51, 49, 47, 48, 49] + "samples": [46, 49, 46, 50, 48, 174, 50, 72, 45, 47, 47, 47, 45, 45, 46, 46, 47, 53, 46, 53, 46, 50, 50, 50, 58, 51, 49, 47, 48, 49, 53, 56] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -334,9 +334,9 @@ "min": 90, "max": 211, "n": 32, - "samples": [93, 93, 96, 95, 93, 95, 96, 211, 97, 114, 90, 95, 94, 93, 95, 92, 92, 93, 95, 101, 93, 94, 94, 93, 94, 94, 92, 97, 91, 93, 92, 93] + "samples": [96, 95, 93, 95, 96, 211, 97, 114, 90, 95, 94, 93, 95, 92, 92, 93, 95, 101, 93, 94, 94, 93, 94, 94, 92, 97, 91, 93, 92, 93, 95, 99] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -456,9 +456,9 @@ "min": 17, "max": 29, "n": 32, - "samples": [20, 18, 18, 18, 20, 20, 20, 29, 22, 26, 17, 20, 20, 20, 20, 17, 17, 17, 18, 18, 19, 21, 20, 19, 20, 19, 19, 21, 20, 20, 19, 20] + "samples": [18, 18, 20, 20, 20, 29, 22, 26, 17, 20, 20, 20, 20, 17, 17, 17, 18, 18, 19, 21, 20, 19, 20, 19, 19, 21, 20, 20, 19, 20, 20, 17] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_MoonLiveEffect_livescript.json b/test/scenarios/light/scenario_MoonLiveEffect_livescript.json index 5b9c81bf..7d338a6c 100644 --- a/test/scenarios/light/scenario_MoonLiveEffect_livescript.json +++ b/test/scenarios/light/scenario_MoonLiveEffect_livescript.json @@ -93,9 +93,9 @@ "min": 5, "max": 15, "n": 32, - "samples": [5, 6, 5, 6, 5, 14, 15, 7, 5, 12, 15, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 6, 5, 14, 15, 7, 5, 12, 15, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -211,9 +211,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [5, 6, 5, 5, 5, 9, 7, 6, 6, 10, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5] + "samples": [5, 5, 5, 9, 7, 6, 6, 10, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 6] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -321,9 +321,9 @@ "min": 4, "max": 12, "n": 32, - "samples": [5, 6, 5, 5, 5, 12, 5, 6, 6, 10, 5, 6, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 4, 5, 6] + "samples": [5, 5, 5, 12, 5, 6, 6, 10, 5, 6, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 4, 5, 6, 6, 6] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -431,9 +431,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [5, 5, 5, 5, 5, 6, 5, 7, 6, 10, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 6] + "samples": [5, 5, 5, 6, 5, 7, 6, 10, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -534,9 +534,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [5, 5, 5, 5, 5, 10, 5, 7, 6, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 10, 5, 7, 6, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -637,9 +637,9 @@ "min": 4, "max": 14, "n": 32, - "samples": [5, 6, 5, 5, 5, 14, 5, 7, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 4, 5, 5] + "samples": [5, 5, 5, 14, 5, 7, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 4, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -740,9 +740,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [7, 5, 5, 5, 5, 10, 6, 7, 6, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5] + "samples": [5, 5, 5, 10, 6, 7, 6, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -762,9 +762,9 @@ "min": 4, "max": 12, "n": 32, - "samples": [6, 6, 5, 5, 5, 6, 12, 6, 6, 9, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5] + "samples": [5, 5, 5, 6, 12, 6, 6, 9, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -784,9 +784,9 @@ "min": 4, "max": 11, "n": 32, - "samples": [6, 6, 5, 6, 5, 7, 6, 6, 6, 8, 11, 5, 6, 5, 5, 6, 5, 5, 6, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5] + "samples": [5, 6, 5, 7, 6, 6, 6, 8, 11, 5, 6, 5, 5, 6, 5, 5, 6, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -804,9 +804,9 @@ "min": 5, "max": 10, "n": 32, - "samples": [6, 5, 5, 5, 5, 9, 6, 6, 6, 6, 10, 5, 5, 5, 5, 6, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 9, 6, 6, 6, 6, 10, 5, 5, 5, 5, 6, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -907,9 +907,9 @@ "min": 4, "max": 9, "n": 32, - "samples": [5, 5, 5, 5, 5, 6, 9, 6, 6, 7, 8, 5, 5, 5, 6, 5, 6, 6, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 4] + "samples": [5, 5, 5, 6, 9, 6, 6, 7, 8, 5, 5, 5, 6, 5, 6, 6, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 4, 7, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1010,9 +1010,9 @@ "min": 5, "max": 8, "n": 32, - "samples": [5, 5, 5, 5, 5, 7, 7, 6, 6, 8, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 7, 7, 6, 6, 8, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32": { "tick_us": { diff --git a/test/scenarios/light/scenario_MoonLive_pipeline.json b/test/scenarios/light/scenario_MoonLive_pipeline.json index a6272887..52103659 100644 --- a/test/scenarios/light/scenario_MoonLive_pipeline.json +++ b/test/scenarios/light/scenario_MoonLive_pipeline.json @@ -379,9 +379,9 @@ "min": 4, "max": 15, "n": 32, - "samples": [5, 5, 6, 5, 5, 5, 5, 6, 7, 6, 15, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5] + "samples": [6, 5, 5, 5, 5, 6, 7, 6, 15, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -536,9 +536,9 @@ "min": 5, "max": 7, "n": 32, - "samples": [5, 5, 6, 6, 5, 5, 5, 6, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [6, 6, 5, 5, 5, 6, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -687,9 +687,9 @@ "min": 5, "max": 11, "n": 32, - "samples": [5, 6, 5, 5, 5, 6, 8, 11, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 6, 8, 11, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -989,9 +989,9 @@ "min": 5, "max": 11, "n": 32, - "samples": [5, 5, 5, 6, 5, 5, 5, 5, 6, 7, 11, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 6, 5, 5, 5, 5, 6, 7, 11, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1131,9 +1131,9 @@ "min": 4, "max": 7, "n": 32, - "samples": [5, 5, 6, 6, 5, 5, 5, 6, 5, 7, 7, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 4] + "samples": [6, 6, 5, 5, 5, 6, 5, 7, 7, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 4, 5, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json b/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json index d2161ae9..05cb5ff2 100644 --- a/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json +++ b/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json @@ -94,9 +94,9 @@ "min": 2, "max": 4, "n": 32, - "samples": [3, 3, 3, 3, 3, 3, 3, 3, 2, 4, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 4, 3, 3, 3, 3, 3, 2] + "samples": [3, 3, 3, 3, 3, 3, 2, 4, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 4, 3, 3, 3, 3, 3, 2, 3, 3] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_MultiplyModifier_pipeline.json b/test/scenarios/light/scenario_MultiplyModifier_pipeline.json index 90dfa4ad..17b902d3 100644 --- a/test/scenarios/light/scenario_MultiplyModifier_pipeline.json +++ b/test/scenarios/light/scenario_MultiplyModifier_pipeline.json @@ -94,9 +94,9 @@ "min": 117, "max": 172, "n": 32, - "samples": [119, 121, 127, 129, 119, 121, 119, 172, 126, 146, 144, 122, 118, 118, 120, 126, 121, 121, 126, 136, 118, 123, 117, 118, 117, 120, 119, 121, 119, 119, 118, 118] + "samples": [127, 129, 119, 121, 119, 172, 126, 146, 144, 122, 118, 118, 120, 126, 121, 121, 126, 136, 118, 123, 117, 118, 117, 120, 119, 121, 119, 119, 118, 118, 119, 121] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Trails_ladder.json b/test/scenarios/light/scenario_Trails_ladder.json index d3425402..a03fd101 100644 --- a/test/scenarios/light/scenario_Trails_ladder.json +++ b/test/scenarios/light/scenario_Trails_ladder.json @@ -91,9 +91,9 @@ "min": 11, "max": 15, "n": 32, - "samples": [11, 11, 13, 13, 11, 12, 11, 15, 11, 14, 12, 11, 11, 11, 11, 13, 12, 12, 13, 13, 11, 11, 11, 11, 11, 12, 11, 11, 11, 11, 11, 11] + "samples": [13, 13, 11, 12, 11, 15, 11, 14, 12, 11, 11, 11, 11, 13, 12, 12, 13, 13, 11, 11, 11, 11, 11, 12, 11, 11, 11, 11, 11, 11, 11, 13] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -128,9 +128,9 @@ "min": 44, "max": 60, "n": 32, - "samples": [46, 49, 50, 53, 45, 44, 46, 49, 60, 56, 46, 47, 46, 45, 45, 51, 49, 49, 51, 53, 45, 45, 45, 44, 44, 46, 45, 44, 46, 47, 44, 45] + "samples": [50, 53, 45, 44, 46, 49, 60, 56, 46, 47, 46, 45, 45, 51, 49, 49, 51, 53, 45, 45, 45, 44, 44, 46, 45, 44, 46, 47, 44, 45, 46, 47] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -165,9 +165,9 @@ "min": 175, "max": 233, "n": 32, - "samples": [179, 187, 200, 202, 178, 178, 178, 233, 191, 222, 188, 222, 178, 180, 184, 207, 195, 194, 202, 211, 177, 182, 180, 177, 175, 178, 177, 179, 178, 178, 176, 177] + "samples": [200, 202, 178, 178, 178, 233, 191, 222, 188, 222, 178, 180, 184, 207, 195, 194, 202, 211, 177, 182, 180, 177, 175, 178, 177, 179, 178, 178, 176, 177, 189, 189] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -209,9 +209,9 @@ "min": 344, "max": 466, "n": 32, - "samples": [351, 349, 396, 395, 347, 349, 348, 394, 372, 437, 372, 429, 344, 351, 344, 394, 381, 438, 395, 413, 345, 355, 348, 345, 346, 356, 347, 347, 348, 347, 466, 347] + "samples": [396, 395, 347, 349, 348, 394, 372, 437, 372, 429, 344, 351, 344, 394, 381, 438, 395, 413, 345, 355, 348, 345, 346, 356, 347, 347, 348, 347, 466, 347, 351, 358] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -235,13 +235,13 @@ "desktop-macos": { "tick_us": { "p50": 359, - "p95": 423, + "p95": 433, "min": 351, - "max": 433, + "max": 523, "n": 32, - "samples": [362, 373, 423, 414, 354, 360, 355, 373, 384, 433, 368, 366, 359, 358, 352, 398, 395, 400, 394, 411, 351, 367, 359, 358, 352, 359, 353, 354, 356, 356, 352, 352] + "samples": [423, 414, 354, 360, 355, 373, 384, 433, 368, 366, 359, 358, 352, 398, 395, 400, 394, 411, 351, 367, 359, 358, 352, 359, 353, 354, 356, 356, 352, 352, 359, 523] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -269,9 +269,9 @@ "min": 344, "max": 490, "n": 32, - "samples": [351, 348, 451, 413, 350, 350, 349, 373, 412, 434, 368, 350, 344, 345, 345, 407, 490, 395, 395, 414, 345, 359, 347, 352, 347, 350, 346, 348, 400, 349, 346, 346] + "samples": [451, 413, 350, 350, 349, 373, 412, 434, 368, 350, 344, 345, 345, 407, 490, 395, 395, 414, 345, 359, 347, 352, 347, 350, 346, 348, 400, 349, 346, 346, 351, 426] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } }, @@ -313,9 +313,9 @@ "min": 175, "max": 268, "n": 32, - "samples": [180, 183, 250, 210, 177, 177, 181, 203, 210, 221, 187, 179, 176, 175, 178, 203, 211, 202, 201, 210, 176, 187, 177, 178, 177, 177, 177, 179, 268, 178, 176, 176] + "samples": [250, 210, 177, 177, 181, 203, 210, 221, 187, 179, 176, 175, 178, 203, 211, 202, 201, 210, 176, 187, 177, 178, 177, 177, 177, 179, 268, 178, 176, 176, 179, 186] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" } } } diff --git a/test/scenarios/light/scenario_modifier_chain.json b/test/scenarios/light/scenario_modifier_chain.json index dec8a9d3..a2ebb0f8 100644 --- a/test/scenarios/light/scenario_modifier_chain.json +++ b/test/scenarios/light/scenario_modifier_chain.json @@ -103,13 +103,13 @@ "desktop-macos": { "tick_us": { "p50": 10, - "p95": 13, + "p95": 11, "min": 8, - "max": 24, + "max": 13, "n": 32, - "samples": [10, 24, 10, 8, 10, 10, 13, 11, 9, 10, 8, 10, 10, 8, 10, 9, 9, 9, 9, 10, 10, 8, 10, 10, 8, 8, 10, 10, 9, 10, 8, 10] + "samples": [10, 8, 10, 10, 13, 11, 9, 10, 8, 10, 10, 8, 10, 9, 9, 9, 9, 10, 10, 8, 10, 10, 8, 8, 10, 10, 9, 10, 8, 10, 9, 11] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -165,11 +165,11 @@ "p50": 8, "p95": 10, "min": 6, - "max": 56, + "max": 10, "n": 32, - "samples": [9, 56, 8, 9, 9, 9, 10, 10, 8, 8, 7, 9, 9, 7, 9, 8, 8, 8, 8, 8, 8, 7, 9, 8, 9, 7, 9, 7, 9, 9, 6, 9] + "samples": [8, 9, 9, 9, 10, 10, 8, 8, 7, 9, 9, 7, 9, 8, 8, 8, 8, 8, 8, 7, 9, 8, 9, 7, 9, 7, 9, 9, 6, 9, 9, 9] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -225,9 +225,9 @@ "min": 21, "max": 28, "n": 32, - "samples": [25, 26, 24, 24, 24, 25, 28, 25, 23, 24, 27, 25, 24, 22, 25, 22, 23, 22, 22, 23, 24, 24, 25, 22, 24, 22, 24, 21, 25, 25, 21, 24] + "samples": [24, 24, 24, 25, 28, 25, 23, 24, 27, 25, 24, 22, 25, 22, 23, 22, 22, 23, 24, 24, 25, 22, 24, 22, 24, 21, 25, 25, 21, 24, 25, 24] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -254,13 +254,13 @@ "desktop-macos": { "tick_us": { "p50": 44, - "p95": 48, + "p95": 47, "min": 38, "max": 50, "n": 32, - "samples": [43, 48, 43, 45, 44, 44, 50, 44, 40, 45, 38, 45, 44, 43, 44, 39, 42, 42, 40, 42, 44, 46, 44, 45, 44, 38, 44, 47, 42, 43, 45, 43] + "samples": [43, 45, 44, 44, 50, 44, 40, 45, 38, 45, 44, 43, 44, 39, 42, 42, 40, 42, 44, 46, 44, 45, 44, 38, 44, 47, 42, 43, 45, 43, 45, 44] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_modifier_swap.json b/test/scenarios/light/scenario_modifier_swap.json index 3557f448..58c0bfa8 100644 --- a/test/scenarios/light/scenario_modifier_swap.json +++ b/test/scenarios/light/scenario_modifier_swap.json @@ -156,9 +156,9 @@ "min": 8, "max": 12, "n": 32, - "samples": [8, 8, 12, 9, 9, 10, 8, 8, 9, 10, 8, 8, 8, 8, 8, 9, 9, 9, 9, 10, 8, 8, 8, 9, 8, 9, 8, 8, 11, 8, 8, 8] + "samples": [12, 9, 9, 10, 8, 8, 9, 10, 8, 8, 8, 8, 8, 9, 9, 9, 9, 10, 8, 8, 8, 9, 8, 9, 8, 8, 11, 8, 8, 8, 10, 8] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth": { "tick_us": { @@ -295,14 +295,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 22, - "p95": 24, + "p50": 23, + "p95": 25, "min": 20, "max": 31, "n": 32, - "samples": [20, 21, 31, 24, 24, 24, 20, 24, 23, 24, 21, 22, 20, 20, 21, 22, 23, 22, 22, 24, 20, 21, 23, 23, 23, 24, 21, 21, 24, 21, 21, 24] + "samples": [31, 24, 24, 24, 20, 24, 23, 24, 21, 22, 20, 20, 21, 22, 23, 22, 22, 24, 20, 21, 23, 23, 23, 24, 21, 21, 24, 21, 21, 24, 24, 25] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth": { "tick_us": { @@ -444,9 +444,9 @@ "min": 8, "max": 106, "n": 32, - "samples": [9, 8, 106, 9, 10, 10, 10, 11, 12, 10, 9, 10, 11, 10, 10, 9, 9, 9, 9, 9, 10, 11, 10, 10, 10, 10, 10, 8, 8, 10, 10, 10] + "samples": [106, 9, 10, 10, 10, 11, 12, 10, 9, 10, 11, 10, 10, 9, 9, 9, 9, 9, 10, 11, 10, 10, 10, 10, 10, 8, 8, 10, 10, 10, 10, 11] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32-eth": { "tick_us": { diff --git a/test/scenarios/light/scenario_perf_full.json b/test/scenarios/light/scenario_perf_full.json index 579fb511..c375da56 100644 --- a/test/scenarios/light/scenario_perf_full.json +++ b/test/scenarios/light/scenario_perf_full.json @@ -92,7 +92,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -212,7 +212,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -332,7 +332,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -575,7 +575,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -691,9 +691,9 @@ "min": 1, "max": 2, "n": 32, - "samples": [1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -822,7 +822,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -955,7 +955,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1062,7 +1062,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -1175,7 +1175,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1297,9 +1297,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 6, 5, 4, 4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 5, 4, 4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1421,9 +1421,9 @@ "min": 17, "max": 79, "n": 32, - "samples": [18, 18, 23, 21, 18, 18, 18, 20, 20, 21, 18, 18, 17, 18, 18, 20, 21, 20, 20, 21, 18, 18, 18, 17, 17, 18, 17, 18, 18, 79, 17, 18] + "samples": [23, 21, 18, 18, 18, 20, 20, 21, 18, 18, 17, 18, 18, 20, 21, 20, 20, 21, 18, 18, 18, 17, 17, 18, 17, 18, 18, 79, 17, 18, 18, 19] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1545,9 +1545,9 @@ "min": 70, "max": 113, "n": 32, - "samples": [72, 74, 113, 86, 71, 71, 70, 76, 80, 88, 73, 72, 70, 73, 72, 80, 84, 80, 80, 84, 70, 72, 70, 71, 70, 71, 71, 71, 70, 80, 70, 72] + "samples": [113, 86, 71, 71, 70, 76, 80, 88, 73, 72, 70, 73, 72, 80, 84, 80, 80, 84, 70, 72, 70, 71, 70, 71, 71, 71, 70, 80, 70, 72, 72, 77] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1677,9 +1677,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 5, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [5, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1801,9 +1801,9 @@ "min": 15, "max": 92, "n": 32, - "samples": [15, 16, 92, 19, 16, 16, 16, 16, 19, 19, 16, 17, 15, 16, 16, 18, 19, 18, 18, 18, 15, 16, 15, 16, 15, 16, 15, 16, 15, 16, 15, 15] + "samples": [92, 19, 16, 16, 16, 16, 19, 19, 16, 17, 15, 16, 16, 18, 19, 18, 18, 18, 15, 16, 15, 16, 15, 16, 15, 16, 15, 16, 15, 15, 17, 18] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -1920,14 +1920,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 63, + "p50": 64, "p95": 82, "min": 61, "max": 574, "n": 32, - "samples": [63, 67, 574, 74, 64, 62, 63, 82, 72, 74, 68, 69, 62, 63, 65, 71, 74, 70, 71, 74, 62, 64, 62, 63, 61, 62, 62, 63, 63, 63, 62, 62] + "samples": [574, 74, 64, 62, 63, 82, 72, 74, 68, 69, 62, 63, 65, 71, 74, 70, 71, 74, 62, 64, 62, 63, 61, 62, 62, 63, 63, 63, 62, 62, 67, 67] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -2049,9 +2049,9 @@ "min": 243, "max": 402, "n": 32, - "samples": [257, 264, 402, 298, 250, 259, 252, 285, 294, 301, 265, 277, 252, 257, 264, 281, 298, 285, 284, 298, 250, 257, 248, 250, 243, 252, 250, 252, 251, 251, 250, 252] + "samples": [402, 298, 250, 259, 252, 285, 294, 301, 265, 277, 252, 257, 264, 281, 298, 285, 284, 298, 250, 257, 248, 250, 243, 252, 250, 252, 251, 251, 250, 252, 265, 297] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -2208,9 +2208,9 @@ "min": 1, "max": 2, "n": 32, - "samples": [1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32": { "tick_us": { @@ -2332,9 +2332,9 @@ "min": 4, "max": 7, "n": 32, - "samples": [4, 4, 7, 5, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [7, 5, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32": { "tick_us": { @@ -2456,9 +2456,9 @@ "min": 15, "max": 27, "n": 32, - "samples": [16, 16, 27, 18, 15, 15, 15, 17, 18, 18, 16, 15, 15, 16, 16, 17, 18, 18, 18, 18, 15, 16, 15, 15, 15, 16, 15, 16, 17, 15, 15, 15] + "samples": [27, 18, 15, 15, 15, 17, 18, 18, 16, 15, 15, 16, 16, 17, 18, 18, 18, 18, 15, 16, 15, 15, 15, 16, 15, 16, 17, 15, 15, 15, 17, 18] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32": { "tick_us": { @@ -2580,9 +2580,9 @@ "min": 61, "max": 103, "n": 32, - "samples": [63, 67, 103, 74, 63, 63, 63, 68, 71, 79, 67, 62, 62, 62, 63, 70, 74, 71, 71, 72, 62, 64, 62, 61, 61, 62, 62, 63, 62, 62, 62, 63] + "samples": [103, 74, 63, 63, 63, 68, 71, 79, 67, 62, 62, 62, 63, 70, 74, 71, 71, 72, 62, 64, 62, 61, 61, 62, 62, 63, 62, 62, 62, 63, 68, 71] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32": { "tick_us": { diff --git a/test/scenarios/light/scenario_perf_light.json b/test/scenarios/light/scenario_perf_light.json index c1387a90..06a0c50d 100644 --- a/test/scenarios/light/scenario_perf_light.json +++ b/test/scenarios/light/scenario_perf_light.json @@ -106,9 +106,9 @@ "min": 1, "max": 2, "n": 32, - "samples": [1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -454,9 +454,9 @@ "min": 1, "max": 2, "n": 32, - "samples": [1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -578,9 +578,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 6, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { @@ -698,13 +698,13 @@ "desktop-macos": { "tick_us": { "p50": 16, - "p95": 19, + "p95": 24, "min": 15, - "max": 24, + "max": 27, "n": 32, - "samples": [16, 16, 24, 18, 15, 16, 15, 16, 18, 19, 16, 16, 16, 15, 16, 17, 19, 18, 17, 18, 15, 16, 16, 15, 15, 15, 15, 15, 15, 15, 15, 16] + "samples": [24, 18, 15, 16, 15, 16, 18, 19, 16, 16, 16, 15, 16, 17, 19, 18, 17, 18, 15, 16, 16, 15, 15, 15, 15, 15, 15, 15, 15, 16, 17, 27] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_peripheral_grid_sweep.json b/test/scenarios/light/scenario_peripheral_grid_sweep.json index 524b5ac3..9ef26777 100644 --- a/test/scenarios/light/scenario_peripheral_grid_sweep.json +++ b/test/scenarios/light/scenario_peripheral_grid_sweep.json @@ -174,13 +174,13 @@ "desktop-macos": { "tick_us": { "p50": 4, - "p95": 5, + "p95": 6, "min": 4, - "max": 6, + "max": 15, "n": 32, - "samples": [4, 4, 6, 5, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 5, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 15, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -301,13 +301,13 @@ "desktop-macos": { "tick_us": { "p50": 16, - "p95": 20, + "p95": 37, "min": 15, - "max": 37, + "max": 122, "n": 32, - "samples": [17, 16, 37, 19, 16, 16, 16, 16, 17, 20, 16, 16, 15, 15, 15, 17, 18, 18, 17, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 16] + "samples": [37, 19, 16, 16, 16, 16, 17, 20, 16, 16, 15, 15, 15, 17, 18, 18, 17, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 16, 122, 17] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -427,14 +427,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 64, + "p50": 65, "p95": 78, "min": 61, "max": 103, "n": 32, - "samples": [66, 64, 103, 74, 65, 63, 63, 69, 72, 78, 66, 72, 64, 62, 66, 71, 73, 71, 71, 71, 62, 67, 63, 61, 61, 62, 62, 62, 63, 63, 62, 64] + "samples": [103, 74, 65, 63, 63, 69, 72, 78, 66, 72, 64, 62, 66, 71, 73, 71, 71, 71, 62, 67, 63, 61, 61, 62, 62, 62, 63, 63, 62, 64, 71, 69] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -559,9 +559,9 @@ "min": 244, "max": 813, "n": 32, - "samples": [259, 272, 813, 298, 254, 251, 251, 278, 285, 299, 272, 277, 251, 250, 254, 282, 285, 282, 285, 285, 249, 260, 250, 245, 244, 251, 249, 251, 251, 252, 250, 249] + "samples": [813, 298, 254, 251, 251, 278, 285, 299, 272, 277, 251, 250, 254, 282, 285, 282, 285, 285, 249, 260, 250, 245, 244, 251, 249, 251, 251, 252, 250, 249, 286, 278] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -707,9 +707,9 @@ "min": 4, "max": 16, "n": 32, - "samples": [4, 4, 16, 5, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [16, 5, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -834,9 +834,9 @@ "min": 15, "max": 61, "n": 32, - "samples": [16, 16, 61, 18, 15, 16, 15, 18, 18, 18, 16, 16, 15, 15, 15, 17, 18, 17, 18, 18, 15, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 17] + "samples": [61, 18, 15, 16, 15, 18, 18, 18, 16, 16, 15, 15, 15, 17, 18, 17, 18, 18, 15, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 17, 17, 17] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -961,9 +961,9 @@ "min": 60, "max": 125, "n": 32, - "samples": [64, 63, 125, 74, 62, 63, 64, 69, 72, 75, 69, 67, 62, 62, 61, 70, 71, 69, 71, 71, 62, 64, 63, 61, 60, 63, 63, 63, 63, 62, 62, 62] + "samples": [125, 74, 62, 63, 64, 69, 72, 75, 69, 67, 62, 62, 61, 70, 71, 69, 71, 71, 62, 64, 63, 61, 60, 63, 63, 63, 63, 62, 62, 62, 67, 67] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1083,14 +1083,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 254, + "p50": 255, "p95": 309, "min": 243, "max": 643, "n": 32, - "samples": [260, 253, 643, 309, 254, 255, 252, 272, 285, 296, 274, 258, 248, 249, 251, 283, 286, 276, 276, 284, 248, 257, 248, 246, 243, 251, 256, 252, 252, 252, 249, 249] + "samples": [643, 309, 254, 255, 252, 272, 285, 296, 274, 258, 248, 249, 251, 283, 286, 276, 276, 284, 248, 257, 248, 246, 243, 251, 256, 252, 252, 252, 249, 249, 268, 267] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1236,9 +1236,9 @@ "min": 4, "max": 7, "n": 32, - "samples": [4, 4, 7, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [7, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1363,9 +1363,9 @@ "min": 15, "max": 27, "n": 32, - "samples": [16, 17, 27, 18, 16, 15, 15, 16, 18, 18, 19, 17, 15, 15, 16, 17, 18, 17, 17, 18, 15, 16, 15, 15, 15, 15, 16, 15, 15, 16, 15, 16] + "samples": [27, 18, 16, 15, 15, 16, 18, 18, 19, 17, 15, 15, 16, 17, 18, 17, 17, 18, 15, 16, 15, 15, 15, 15, 16, 15, 15, 16, 15, 16, 17, 16] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1490,9 +1490,9 @@ "min": 61, "max": 588, "n": 32, - "samples": [64, 67, 96, 588, 63, 64, 64, 68, 71, 74, 67, 70, 62, 62, 63, 70, 71, 68, 68, 78, 61, 64, 61, 61, 61, 62, 62, 62, 63, 62, 64, 62] + "samples": [96, 588, 63, 64, 64, 68, 71, 74, 67, 70, 62, 62, 63, 70, 71, 68, 68, 78, 61, 64, 61, 61, 61, 62, 62, 62, 63, 62, 64, 62, 66, 65] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1617,9 +1617,9 @@ "min": 243, "max": 384, "n": 32, - "samples": [255, 260, 384, 359, 252, 252, 251, 270, 292, 297, 266, 266, 248, 247, 251, 282, 287, 275, 276, 288, 249, 258, 243, 246, 243, 251, 251, 252, 253, 251, 249, 249] + "samples": [384, 359, 252, 252, 251, 270, 292, 297, 266, 266, 248, 247, 251, 282, 287, 275, 276, 288, 249, 258, 243, 246, 243, 251, 251, 252, 253, 251, 249, 249, 254, 263] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1765,9 +1765,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -1892,9 +1892,9 @@ "min": 15, "max": 25, "n": 32, - "samples": [15, 15, 25, 21, 15, 15, 15, 16, 17, 18, 16, 16, 15, 15, 15, 17, 18, 20, 18, 18, 15, 16, 15, 15, 15, 15, 16, 15, 15, 15, 15, 15] + "samples": [25, 21, 15, 15, 15, 16, 17, 18, 16, 16, 15, 15, 15, 17, 18, 20, 18, 18, 15, 16, 15, 15, 15, 15, 16, 15, 15, 15, 15, 15, 16, 16] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -2014,14 +2014,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 62, + "p50": 63, "p95": 87, "min": 60, "max": 117, "n": 32, - "samples": [64, 62, 117, 87, 64, 63, 63, 68, 71, 74, 65, 62, 62, 62, 62, 69, 71, 80, 71, 71, 62, 64, 60, 60, 61, 62, 62, 62, 62, 62, 62, 62] + "samples": [117, 87, 64, 63, 63, 68, 71, 74, 65, 62, 62, 62, 62, 69, 71, 80, 71, 71, 62, 64, 60, 60, 61, 62, 62, 62, 62, 62, 62, 62, 63, 68] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { @@ -2146,9 +2146,9 @@ "min": 245, "max": 391, "n": 32, - "samples": [251, 253, 385, 363, 252, 251, 250, 299, 287, 289, 265, 261, 248, 249, 250, 275, 287, 391, 279, 284, 249, 256, 245, 245, 247, 252, 249, 251, 258, 252, 247, 250] + "samples": [385, 363, 252, 251, 250, 299, 287, 289, 265, 261, 248, 249, 250, 275, 287, 391, 279, 284, 249, 256, 245, 245, 247, 252, 249, 251, 258, 252, 247, 250, 264, 260] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_peripheral_switch.json b/test/scenarios/light/scenario_peripheral_switch.json index 4cdb33de..c46cf639 100644 --- a/test/scenarios/light/scenario_peripheral_switch.json +++ b/test/scenarios/light/scenario_peripheral_switch.json @@ -177,9 +177,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -298,9 +298,9 @@ "min": 4, "max": 19, "n": 32, - "samples": [4, 4, 6, 19, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [6, 19, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -419,9 +419,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -539,9 +539,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -660,9 +660,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { @@ -797,9 +797,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-12" + "last_updated": "2026-09-13" }, "esp32p4rev1-eth": { "tick_us": { diff --git a/test/unit/core/unit_MoonStatsReport.cpp b/test/unit/core/unit_MoonStatsReport.cpp index 1de45481..8c4e4b51 100644 --- a/test/unit/core/unit_MoonStatsReport.cpp +++ b/test/unit/core/unit_MoonStatsReport.cpp @@ -47,6 +47,37 @@ class LeakyModule : public mm::MoonModule { char flash_[8] = {}; }; +/// A scripted module, the shape MoonLiveEffect and friends have: a `script` FilePath control +/// holding a file NAME, plus whatever that script declared. +class ScriptedModule : public mm::MoonModule { +public: + ScriptedModule(const char* name, const char* script, mm::ModuleRole role) + : role_(role) { + setName(name); + std::snprintf(script_, sizeof(script_), "%s", script); + } + + mm::ModuleRole role() const MM_NONBLOCKING override { return role_; } + + void defineControls() override { + controls_.addFilePath("script", script_, sizeof(script_), mm::moonlive::kEffectPick); + } + +private: + mm::ModuleRole role_; + char script_[48] = {}; +}; + +/// The report for a tree holding one scripted module. +std::string scriptedReport(const char* script, mm::ModuleRole role = mm::ModuleRole::Effect) { + ScriptedModule mod("MoonLive", script, role); + mod.defineControls(); + mm::MoonModule* tree[] = {&mod}; + mm::JsonSink sink; + mm::buildMoonStatsReport(sink, tree, 1, mm::MoonStatsEvent::Install, nullptr, "4.0.0", nullptr); + return std::string(sink.data(), sink.size()); +} + std::string report(mm::MoonStatsEvent event = mm::MoonStatsEvent::Install, const char* id = nullptr, const char* version = "4.0.0", @@ -109,6 +140,53 @@ TEST_CASE("an upgrade is distinguished from a fresh install by the previous vers CHECK(upgraded.find("\"previousVersion\":\"4.0.0\"") != std::string::npos); } +/// The button's event. Install and Upgrade are decided by a version comparison, which cannot see a +/// setup that changed without one: someone who reported a bare board and then wired up the fixtures +/// they actually run. Distinct from the other two so the install count stays a count of installs. +TEST_CASE("a user-triggered refresh is its own event, carrying the same payload") { + const std::string refreshed = report(mm::MoonStatsEvent::Refresh, nullptr, "4.0.0", nullptr); + CHECK(refreshed.find("\"event\":\"refresh\"") != std::string::npos); + // Same shape as any other report: the button re-sends, it does not send something smaller. + CHECK(refreshed.find("\"chip\"") != std::string::npos); + CHECK(refreshed.find("\"version\":\"4.0.0\"") != std::string::npos); + // And it is none of the other two, so a legend cannot show it as an install. + CHECK(refreshed.find("\"event\":\"install\"") == std::string::npos); + CHECK(refreshed.find("\"event\":\"upgrade\"") == std::string::npos); +} + +/// A refresh carries no previousVersion. The server reads that field's PRESENCE as what makes a +/// row an upgrade, and the value it would carry (`reportedVersion`) is non-empty whenever the +/// button is pressed: sending it would report every refresh as an upgrade from the version already +/// running. Caught by CodeRabbit on PR #104. +TEST_CASE("a refresh names no previous version, so it cannot read as an upgrade") { + // The GUARD is in MoonStatsModule::sendReport, which passes previousVersion() only on Upgrade: + // the value it would otherwise carry is `reportedVersion`, non-empty whenever the button is + // pressed, and the server reads that field's PRESENCE as what makes a row an upgrade. So a + // refresh would have reported as an upgrade from the version already running. + // + // This builder is a pure function over its arguments and rightly emits whatever it is handed, + // so passing a previous version here WOULD produce one. What it pins is the other half: with + // no predecessor supplied, a refresh carries none, and the field never appears by itself. + const std::string refreshed = report(mm::MoonStatsEvent::Refresh, nullptr, "4.0.0", nullptr); + CHECK(refreshed.find("\"event\":\"refresh\"") != std::string::npos); + CHECK(refreshed.find("previousVersion") == std::string::npos); + + // An upgrade carries it: that is the one event the field describes. + const std::string upgraded = report(mm::MoonStatsEvent::Upgrade, nullptr, "4.1.0", "4.0.0"); + CHECK(upgraded.find("\"previousVersion\":\"4.0.0\"") != std::string::npos); +} + +/// A failed BUTTON press must not consume the automatic report. Pressing it before the install +/// report has gone out, and having the send fail, used to mark the version reported anyway: the +/// install was then never counted, and the user had been told the press failed. The automatic path +/// still marks either way, because nobody is waiting for it. +TEST_CASE("a refresh that did not send leaves the automatic report still due") { + // Documents the rule the code encodes (MoonStatsModule::sendReport): the mark is conditional + // on Refresh, unconditional otherwise. A build asserting it end to end needs a server. + CHECK(mm::MoonStatsEvent::Refresh != mm::MoonStatsEvent::Install); + CHECK(mm::MoonStatsEvent::Refresh != mm::MoonStatsEvent::Upgrade); +} + /// A report built without consent carries no installation id at all, rather than an empty or /// placeholder one: nothing is generated until the user says yes. TEST_CASE("no installation id appears until one is supplied") { @@ -195,3 +273,29 @@ TEST_CASE("a module added under a wired parent is still reported") { CHECK(json.find("Effects") == std::string::npos); } +/// A scripted module reports WHICH script it runs, because "MoonLive" alone says nothing: the +/// interesting fact is that a device is running `aurora.mle`. +TEST_CASE("a scripted module reports the shipped script it runs") { + const std::string json = scriptedReport("aurora.mle"); + CHECK(json.find("effect:MoonLive/aurora.mle") != std::string::npos); +} + +/// The other half, and the one that matters: a script a USER wrote is a name they invented, which +/// is text they typed. The module still counts, under its bare type name. +/// +/// Without this the feature would be a privacy regression wearing a usage-statistics hat: a script +/// called "ewoud-bedroom-test.mle" would travel to the server exactly like a shipped name. +TEST_CASE("a script the user wrote is counted but never named") { + const std::string json = scriptedReport("ewoud-bedroom-test.mle"); + CHECK(json.find("ewoud-bedroom-test") == std::string::npos); + CHECK(json.find("effect:MoonLive") != std::string::npos); + CHECK(json.find("effect:MoonLive/") == std::string::npos); +} + +/// A shipped name under the WRONG extension is not a shipped script: the catalogs are per role, so +/// a lookup that scanned them all would let `aurora.mle` through on a layout and, worse, would make +/// "is this ours" depend on a name rather than a name plus its kind. +TEST_CASE("a catalog name is matched against its own role's catalog") { + const std::string json = scriptedReport("grid.mll", mm::ModuleRole::Layout); + CHECK(json.find("layout:MoonLive/grid.mll") != std::string::npos); +}