FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchRemoteBenchmarkMethods.cpp
Go to the documentation of this file.
1// Performance benchmark RPC bindings — the autoresearch perf framework
2// itself: perfProbeMemcpy (SRAM bandwidth), perfProbeNop (probe overhead
3// floor), perfProbeRepeat (probe variance), and wave2dPerf (Wave2D solver
4// benchmark). Sanity probes gate trust in subsequent perf numbers.
5// Extracted from AutoResearchRemote.cpp as part of #3132 / meta #3127.
6
8#if FL_PLATFORM_HAS_LARGE_MEMORY
9
10// Legacy debug macros (no-ops, kept for debugTest RPC function)
11#define DEBUG_PRINT(x) do {} while(0)
12#define DEBUG_PRINTLN(x) do {} while(0)
13
14#include "AutoResearchRemote.h"
15#include "AutoResearchBle.h"
16#include "AutoResearchNet.h"
17#include "AutoResearchOta.h"
19#include "fl/stl/vector.h"
20#include "fl/stl/singleton.h"
21#include "fl/system/heap.h"
22#include "fl/system/delay.h"
23#include "fl/system/pin.h"
24#include "fl/system/pins.h"
25#include "platforms/cpu_frequency.h"
26#include "Common.h"
27#include "AutoResearchTest.h"
28#include "AutoResearchHelpers.h"
30#include "fl/stl/sstream.h"
31#include "fl/stl/unique_ptr.h"
32#include "fl/stl/optional.h"
33#include "fl/stl/json.h"
34#include "fl/task/task.h"
35#include "fl/task/executor.h"
37#include "fl/stl/atomic.h"
38#include "fl/task/promise.h"
39#include "fl/math/simd.h"
40#include "AutoResearchSimd.h"
46#include "fl/chipsets/spi.h"
47#include "fl/channels/color_managed_source.h" // colorPipelinePerf (P9, #4043)
49#include "fl/gfx/pipeline.h"
50#include "fl/channels/config.h"
51#include "fl/channels/channel.h"
52#include "fl/channels/driver.h"
53#include "fl/channels/manager.h"
54#include "fl/channels/data.h"
57#include "fl/stl/scope_exit.h"
58#include <Arduino.h>
59
60#include "fl/net/ble.h"
61
62#include "fl/codec/h264.h"
63#include "fl/codec/mp4_parser.h"
65#include "fl/fx/frame.h"
66
67namespace {
68
69struct PerfProbeMemcpyState {
70 fl::vector<uint8_t> source;
71 fl::vector<uint8_t> destination;
72};
73
74PerfProbeMemcpyState& perfProbeMemcpyState() {
76}
77
78#if FL_COLOR_PIPELINE_SHARED
79class PowerBenchCaptureDriver : public fl::IChannelDriver {
80 public:
81 fl::ChannelDataPtr frames[3];
82 int count = 0;
83 bool canHandle(const fl::ChannelDataPtr&) const FL_NO_EXCEPT override { return true; }
84 void enqueue(fl::ChannelDataPtr data) FL_NO_EXCEPT override {
85 if (count < 3) frames[count++] = data;
86 }
87 void show() FL_NO_EXCEPT override {}
88 DriverState poll() FL_NO_EXCEPT override { return DriverState::READY; }
89 fl::string getName() const FL_NO_EXCEPT override { return "POWER_BENCH_CAPTURE"; }
90 Capabilities getCapabilities() const FL_NO_EXCEPT override {
91 return Capabilities(true, true);
92 }
93};
94
98 profile.topology = topology;
99 profile.xy_white1[0] = 0.39f;
100 profile.xy_white1[1] = 0.38f;
101 profile.xy_white2[0] = 0.28f;
102 profile.xy_white2[1] = 0.31f;
103 profile.lum_white1 = 0.8f;
104 profile.lum_white2 = 0.8f;
105 return profile;
106}
107#endif
108
109} // namespace
110
112 // ====== AutoResearch perf-instrumentation sanity probes ======
113 // Task 2 of meta #3113. Three probes verify autoresearch timing
114 // produces trustworthy data BEFORE we use it to gate optimizations.
115 // Fail-loud: any probe failing → host reporter MUST stamp result
116 // UNTRUSTED and refuse to feed wave2dPerf into comparison tables.
117
118 // Probe 2a: memcpy throughput. Caller invokes with { bytes,
119 // iterations }; returns MB/s on SRAM. Host compares vs per-platform
120 // vendor floor (e.g. ESP32-S3 ~175 MB/s) before trusting wave2dPerf.
121 remote.bind("perfProbeMemcpy", [](const fl::json& args) -> fl::json {
122 int bytes = 4096;
123 int iterations = 1000;
124 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
125 const fl::json &cfg = args[0];
126 if (cfg.contains("bytes") && cfg["bytes"].is_int()) {
127 bytes = static_cast<int>(cfg["bytes"].as_int().value());
128 }
129 if (cfg.contains("iterations") && cfg["iterations"].is_int()) {
130 iterations = static_cast<int>(cfg["iterations"].as_int().value());
131 }
132 }
133 fl::json response = fl::json::object();
134 response.set("bytes", static_cast<int64_t>(bytes));
135 response.set("iterations", static_cast<int64_t>(iterations));
136 if (bytes < 64 || bytes > 8192 || iterations < 1 || iterations > 100000) {
137 response.set("success", false);
138 response.set("error", "out_of_range");
139 return response;
140 }
141 // Heap-allocated on first use: 16 KB of static buffers
142 // overflowed ESP32-S2's dram0 bss by 3.7 KB (FastLED#3576
143 // Phase 4 compile matrix).
144 PerfProbeMemcpyState& state = perfProbeMemcpyState();
145 fl::vector<uint8_t>& src_vec = state.source;
146 fl::vector<uint8_t>& dst_vec = state.destination;
147 if (src_vec.size() < 8192) {
148 src_vec.resize(8192);
149 dst_vec.resize(8192);
150 }
151 uint8_t *src_buf = src_vec.data();
152 uint8_t *dst_buf = dst_vec.data();
153 for (int i = 0; i < bytes; ++i) {
154 src_buf[i] = static_cast<uint8_t>(i & 0xFF);
155 }
156 const fl::u32 t0 = fl::micros();
157 for (int i = 0; i < iterations; ++i) {
158 FL_BUILTIN_MEMCPY(dst_buf, src_buf, bytes);
159 }
160 const fl::u32 t1 = fl::micros();
161 const fl::u32 total_us = t1 - t0;
162 const double total_bytes = static_cast<double>(bytes) * iterations;
163 const double mb_per_s = (total_us > 0)
164 ? (total_bytes / total_us) * (1e6 / (1024.0 * 1024.0))
165 : 0.0;
166 response.set("success", true);
167 response.set("total_us", static_cast<int64_t>(total_us));
168 response.set("mb_per_s", mb_per_s);
169 return response;
170 });
171
172 // Probe 2b: nop-loop calibration. Volatile fence prevents the
173 // compiler optimizing the loop away. Host computes
174 // (total_us * cpu_mhz / iterations) and checks [1.0, 2.5]
175 // cycles-per-nop. Out-of-band → timer broken or IRQ jitter.
176 remote.bind("perfProbeNop", [](const fl::json& args) -> fl::json {
177 int iterations = 100000;
178 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
179 const fl::json &cfg = args[0];
180 if (cfg.contains("iterations") && cfg["iterations"].is_int()) {
181 iterations = static_cast<int>(cfg["iterations"].as_int().value());
182 }
183 }
184 fl::json response = fl::json::object();
185 response.set("iterations", static_cast<int64_t>(iterations));
186 if (iterations < 1000 || iterations > 10000000) {
187 response.set("success", false);
188 response.set("error", "out_of_range");
189 return response;
190 }
191 volatile int counter = 0;
192 const fl::u32 t0 = fl::micros();
193 for (int i = 0; i < iterations; ++i) {
194#if defined(__GNUC__) || defined(__clang__)
195 __asm__ volatile("nop");
196#else
197 counter += 1;
198#endif
199 }
200 const fl::u32 t1 = fl::micros();
201 (void)counter;
202 const fl::u32 total_us = t1 - t0;
203 const double us_per_iter = (iterations > 0)
204 ? (static_cast<double>(total_us) / iterations) : 0.0;
205 response.set("success", true);
206 response.set("total_us", static_cast<int64_t>(total_us));
207 response.set("us_per_iter", us_per_iter);
208 return response;
209 });
210
211 // Probe 2d: bit-bang cost attribution (FastLED#4203). The portable
212 // BIT_BANG driver stretches every WS2812 bit by a constant ~2-3us, which
213 // makes its output undecodable. The captures prove the cost is constant
214 // per bit but cannot say which call carries it, so measure the candidates
215 // against a common loop baseline instead of guessing:
216 // nop - empty-loop floor for this board
217 // delayNs - fl::delayNanoseconds(ns): runtime u64 divide, and on
218 // ESP32 an esp_clk_cpu_freq() query on every call
219 // delayNsHz - same with the clock frequency hoisted out; the gap
220 // against delayNs is exactly the per-call clock query
221 // writeByte - DigitalMultiWrite8::writeByte(): nibble LUT lookups plus
222 // two out-of-line applyNibble() calls
223 // A bit costs 3x delay + 3x writeByte, so these numbers reconstruct the
224 // measured per-bit overhead directly.
225 remote.bind("perfProbeBitBangCost", [](const fl::json& args) -> fl::json {
226 int iterations = 20000;
227 int ns = 400; // WS2812 T0H — the budget being blown
228 int pin = -1; // -1 keeps GPIO untouched (default, safe)
229 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
230 const fl::json &cfg = args[0];
231 if (cfg.contains("iterations") && cfg["iterations"].is_int()) {
232 iterations = static_cast<int>(cfg["iterations"].as_int().value());
233 }
234 if (cfg.contains("ns") && cfg["ns"].is_int()) {
235 ns = static_cast<int>(cfg["ns"].as_int().value());
236 }
237 if (cfg.contains("pin") && cfg["pin"].is_int()) {
238 pin = static_cast<int>(cfg["pin"].as_int().value());
239 }
240 }
241 fl::json response = fl::json::object();
242 response.set("iterations", static_cast<int64_t>(iterations));
243 response.set("ns", static_cast<int64_t>(ns));
244 response.set("pin", static_cast<int64_t>(pin));
245 if (iterations < 1000 || iterations > 1000000 || ns < 0) {
246 response.set("success", false);
247 response.set("error", "out_of_range");
248 return response;
249 }
250 // Six timed loops each run `iterations` times, so the wall time is
251 // roughly 6 * iterations * ns. AutoResearch's loop watchdog is 5 s;
252 // cap the estimate well under it so a large ns cannot turn this probe
253 // into a watchdog reset on an unattended bench.
254 constexpr fl::u64 kMaxProbeNs = 2000000000ULL; // 2 s
255 const fl::u64 estimated_ns =
256 static_cast<fl::u64>(iterations) * static_cast<fl::u64>(ns) * 6ULL;
257 if (estimated_ns > kMaxProbeNs) {
258 response.set("success", false);
259 response.set("error", "duration_budget_exceeded");
260 response.set("estimated_ns", static_cast<int64_t>(estimated_ns));
261 response.set("budget_ns", static_cast<int64_t>(kMaxProbeNs));
262 return response;
263 }
264
265 const fl::u32 requested_ns = static_cast<fl::u32>(ns);
266 const fl::u32 hz = static_cast<fl::u32>(FL_CPU_FREQUENCY());
267
268 // Baseline: same loop shape, no payload. An empty memory barrier
269 // keeps the loop alive without adding work of its own -- the previous
270 // `volatile int` read-modify-write costs a load, add and store to
271 // memory every iteration, which inflates nop_per and therefore
272 // under-reports every overhead computed by subtracting it.
273 const fl::u32 nop_t0 = fl::micros();
274 for (int i = 0; i < iterations; ++i) {
275 __asm__ __volatile__("" ::: "memory");
276 }
277 const fl::u32 nop_us = fl::micros() - nop_t0;
278
279 const fl::u32 delay_t0 = fl::micros();
280 for (int i = 0; i < iterations; ++i) {
281 fl::delayNanoseconds(requested_ns);
282 }
283 const fl::u32 delay_us = fl::micros() - delay_t0;
284
285 const fl::u32 delay_hz_t0 = fl::micros();
286 for (int i = 0; i < iterations; ++i) {
287 fl::delayNanoseconds(requested_ns, hz);
288 }
289 const fl::u32 delay_hz_us = fl::micros() - delay_hz_t0;
290
291 // Compile-time NS: cycles_from_ns_*() folds to a constant, so the
292 // runtime u64 divide disappears while the platform busy-wait still
293 // runs. If this is fast, the divide carried the cost; if it is still
294 // slow, the cost is inside the busy-wait. Fixed at 400ns (WS2812 T0H)
295 // because a template argument cannot come from the RPC payload -- the
296 // `ns` parameter only steers the runtime variants above.
297 // Runtime form pinned to 400 ns. ns_conversion_us subtracts the
298 // compile-time 400 ns loop, and the compile-time form cannot take its
299 // duration from the RPC payload -- so comparing it against the
300 // caller-supplied `ns` above would difference two different delays
301 // and misattribute the gap to conversion cost.
302 const fl::u32 delay_hz400_t0 = fl::micros();
303 for (int i = 0; i < iterations; ++i) {
304 fl::delayNanoseconds(400u, hz);
305 }
306 const fl::u32 delay_hz400_us = fl::micros() - delay_hz400_t0;
307
308 const fl::u32 delay_ct_t0 = fl::micros();
309 for (int i = 0; i < iterations; ++i) {
311 }
312 const fl::u32 delay_ct_us = fl::micros() - delay_ct_t0;
313
314 // Pure cycle-counted loop: no ns->cycles conversion and no platform
315 // busy-wait wrapper. This is the floor a hand-rolled bit loop could
316 // reach. The board reports cpu_hz=125000000, so 400ns is 50 cycles;
317 // 60 is kept as a small deliberate overshoot. delaycycles<> is specialized
318 // only up to 50 with no generic fallback, so 60 is composed from two
319 // specializations rather than written as delaycycles<60>(), which is
320 // an undefined reference at link time.
321 const fl::u32 delay_cyc_t0 = fl::micros();
322 for (int i = 0; i < iterations; ++i) {
325 }
326 const fl::u32 delay_cyc_us = fl::micros() - delay_cyc_t0;
327
328 // pin < 0 leaves every slot inactive, so applyNibble performs no GPIO
329 // write at all — that isolates the LUT and call overhead from the
330 // cost of the pin write itself.
331 fl::Pins8 pins8;
332 for (int i = 0; i < 8; ++i) {
333 pins8.pins[i] = -1;
334 }
335 if (pin >= 0) {
337 pins8.pins[0] = pin;
338 }
339 fl::DigitalMultiWrite8 writer;
340 writer.init(pins8);
341 const fl::u32 wb_t0 = fl::micros();
342 for (int i = 0; i < iterations; ++i) {
343 writer.writeByte(static_cast<fl::u8>(i & 0xFF));
344 }
345 const fl::u32 wb_us = fl::micros() - wb_t0;
346 if (pin >= 0) {
348 }
349
350 const double denom = static_cast<double>(iterations);
351 const double nop_per = static_cast<double>(nop_us) / denom;
352 const double delay_per = static_cast<double>(delay_us) / denom;
353 const double delay_hz_per = static_cast<double>(delay_hz_us) / denom;
354 const double wb_per = static_cast<double>(wb_us) / denom;
355
356 response.set("success", true);
357 response.set("cpu_hz", static_cast<int64_t>(hz));
358 response.set("nop_us_per_iter", nop_per);
359 response.set("delay_us_per_iter", delay_per);
360 response.set("delay_hz_us_per_iter", delay_hz_per);
361 response.set("write_byte_us_per_iter", wb_per);
362 // Cost above the loop floor, i.e. what the caller actually pays.
363 response.set("delay_overhead_us", delay_per - nop_per);
364 response.set("delay_hz_overhead_us", delay_hz_per - nop_per);
365 response.set("write_byte_overhead_us", wb_per - nop_per);
366 const double delay_ct_per = static_cast<double>(delay_ct_us) / denom;
367 const double delay_cyc_per = static_cast<double>(delay_cyc_us) / denom;
368 response.set("delay_ct_us_per_iter", delay_ct_per);
369 response.set("delay_cycles_us_per_iter", delay_cyc_per);
370 response.set("delay_ct_overhead_us", delay_ct_per - nop_per);
371 response.set("delay_cycles_overhead_us", delay_cyc_per - nop_per);
372 // Cost attributable to the runtime ns->cycles conversion: the gap
373 // between the runtime and compile-time forms, both at 400 ns.
374 const double delay_hz400_per =
375 static_cast<double>(delay_hz400_us) / iterations;
376 response.set("delay_hz400_us_per_iter", delay_hz400_per);
377 response.set("ns_conversion_us", delay_hz400_per - delay_ct_per);
378 response.set("clock_query_us", delay_per - delay_hz_per);
379 // One clockless bit issues three delays and three writeByte calls.
380 response.set("predicted_bit_overhead_us",
381 3.0 * ((delay_per - nop_per) + (wb_per - nop_per)));
382 return response;
383 });
384
385 // Probe 2c: repeat-stability check. Runs wave2dPerf's measurement
386 // N times at a fixed config, reports mean + std-dev. Host checks
387 // std_dev/mean < 5%; higher → IRQ noise or RPC framing jitter is
388 // contaminating wave2dPerf, mark UNTRUSTED.
389 remote.bind("perfProbeRepeat", [](const fl::json& args) -> fl::json {
390 int W = 16;
391 int H = 16;
392 int iterations = 50;
393 int repeats = 8;
394 fl::string stencil_name = "FivePoint";
395 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
396 const fl::json &cfg = args[0];
397 if (cfg.contains("W") && cfg["W"].is_int()) {
398 W = static_cast<int>(cfg["W"].as_int().value());
399 }
400 if (cfg.contains("H") && cfg["H"].is_int()) {
401 H = static_cast<int>(cfg["H"].as_int().value());
402 }
403 if (cfg.contains("iterations") && cfg["iterations"].is_int()) {
404 iterations = static_cast<int>(cfg["iterations"].as_int().value());
405 }
406 if (cfg.contains("repeats") && cfg["repeats"].is_int()) {
407 repeats = static_cast<int>(cfg["repeats"].as_int().value());
408 }
409 if (cfg.contains("stencil") && cfg["stencil"].is_string()) {
410 stencil_name = cfg["stencil"].as_string().value();
411 }
412 }
413 fl::json response = fl::json::object();
414 response.set("W", static_cast<int64_t>(W));
415 response.set("H", static_cast<int64_t>(H));
416 response.set("iterations", static_cast<int64_t>(iterations));
417 response.set("repeats", static_cast<int64_t>(repeats));
418 if (W < 4 || H < 4 || W > 128 || H > 128 ||
419 iterations < 1 || iterations > 1000 ||
420 repeats < 2 || repeats > 64) {
421 response.set("success", false);
422 response.set("error", "out_of_range");
423 return response;
424 }
425 const fl::LaplacianStencil stencil =
426 (stencil_name == "NinePointIsotropic")
429 // External-binder pattern: simulator-side code (in
430 // src/fl/math/wave/wave_perf_bench.h) owns the wave PDE setup,
431 // checkerboard seed, warm-up, timing and statistics. This RPC
432 // is the thin JSON-RPC translation layer.
433 const fl::wave_perf::WavePerfRepeatResult bench =
435 static_cast<fl::u32>(W),
436 static_cast<fl::u32>(H),
437 static_cast<fl::u32>(iterations),
438 static_cast<fl::u32>(repeats),
439 stencil);
440 response.set("success", bench.success);
441 response.set("stencil", stencil_name.c_str());
442 response.set("mean_us_per_update", bench.mean_us_per_update);
443 response.set("std_dev_us_per_update", bench.std_dev_us_per_update);
444 response.set("std_dev_pct", bench.std_dev_pct);
445 return response;
446 });
447
448 // Wave2D perf benchmark — first task from meta #3113.
449 //
450 // Args: { W, H, iterations, stencil ("FivePoint" | "NinePointIsotropic"),
451 // loads_only (bool, optional) }
452 //
453 // Builds a WaveSimulation2D_Real, seeds it with a deterministic
454 // checkerboard, optionally warms up, then times `iterations` calls to
455 // update() using fl::micros(). Returns timing dict including us/update
456 // and us/cell/update so we can compare scaling across platforms.
457 //
458 // `loads_only=true` swaps update() for a memory-bound baseline (just
459 // sum every cell into a sink) so the gap between the two reveals
460 // compute vs memory cost — critical for the ESP32-S3 PSRAM-vs-SRAM
461 // analysis in sibling issue #3114.
462 remote.bind("wave2dPerf", [](const fl::json& args) -> fl::json {
463 // Defaults sized so the test runs in a sensible budget on small
464 // platforms (32x32 / 100 iters ~= 100-500 ms on M4-class).
465 int W = 32;
466 int H = 32;
467 int iterations = 100;
468 fl::string stencil_name = "NinePointIsotropic";
469 bool loads_only = false;
470 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
471 const fl::json &cfg = args[0];
472 if (cfg.contains("W") && cfg["W"].is_int()) {
473 W = static_cast<int>(cfg["W"].as_int().value());
474 }
475 if (cfg.contains("H") && cfg["H"].is_int()) {
476 H = static_cast<int>(cfg["H"].as_int().value());
477 }
478 if (cfg.contains("iterations") && cfg["iterations"].is_int()) {
479 iterations = static_cast<int>(cfg["iterations"].as_int().value());
480 }
481 if (cfg.contains("stencil") && cfg["stencil"].is_string()) {
482 stencil_name = cfg["stencil"].as_string().value();
483 }
484 if (cfg.contains("loads_only") && cfg["loads_only"].is_bool()) {
485 loads_only = cfg["loads_only"].as_bool().value();
486 }
487 }
488
489 fl::json response = fl::json::object();
490 response.set("W", static_cast<int64_t>(W));
491 response.set("H", static_cast<int64_t>(H));
492 response.set("iterations", static_cast<int64_t>(iterations));
493 response.set("stencil", stencil_name.c_str());
494 response.set("loads_only", loads_only);
495
496 // Refuse silly inputs. 1024 cap protects us from accidentally
497 // requesting a multi-MB grid via a bad RPC payload.
498 if (W < 4 || H < 4 || W > 1024 || H > 1024 || iterations < 1 || iterations > 100000) {
499 response.set("success", false);
500 response.set("error", "out_of_range");
501 return response;
502 }
503
504 const fl::LaplacianStencil stencil =
505 (stencil_name == "FivePoint")
508
509 // External-binder pattern: simulator-side code (in
510 // src/fl/math/wave/wave_perf_bench.h) owns the wave PDE setup,
511 // checkerboard seed, warm-up, and timing. This RPC is the thin
512 // JSON-RPC translation layer.
513 const fl::wave_perf::WavePerfResult bench = loads_only
515 static_cast<fl::u32>(W), static_cast<fl::u32>(H),
516 static_cast<fl::u32>(iterations), stencil)
517 : fl::wave_perf::runWavePerf(
518 static_cast<fl::u32>(W), static_cast<fl::u32>(H),
519 static_cast<fl::u32>(iterations), stencil);
520
521 response.set("success", bench.success);
522 response.set("total_us", static_cast<int64_t>(bench.total_us));
523 response.set("us_per_update", bench.us_per_update);
524 response.set("us_per_cell_per_update", bench.us_per_cell_per_update);
525 response.set("fps_at_one_update_per_frame",
526 bench.fps_at_one_update_per_frame);
527 return response;
528 });
529
530#if FL_COLOR_PIPELINE_SHARED
531 // colorPipelinePerf below stops at the pixel source. This measures the
532 // complete show -> power plan -> channel encode path on three channels.
533 // A capture driver consumes encoded frames without requiring LED wiring;
534 // physical transmission time is deliberately outside this measurement.
535 remote.bind("mixedPowerShowPerf", [](const fl::json& args) -> fl::json {
536 int pixels = 64;
537 int frames = 4;
538 int budget_mw = 2500;
539 if (args.is_array() && args.size() && args[0].is_object()) {
540 const fl::json& cfg = args[0];
541 if (cfg.contains("pixels") && cfg["pixels"].is_int())
542 pixels = static_cast<int>(cfg["pixels"].as_int().value());
543 if (cfg.contains("frames") && cfg["frames"].is_int())
544 frames = static_cast<int>(cfg["frames"].as_int().value());
545 if (cfg.contains("budget_mw") && cfg["budget_mw"].is_int())
546 budget_mw = static_cast<int>(cfg["budget_mw"].as_int().value());
547 }
548 fl::json response = fl::json::object();
549 response.set("pixels_per_channel", pixels);
550 response.set("frames_per_sample", frames);
551 response.set("budget_mw", budget_mw);
552 if (pixels < 1 || pixels > 128 || frames < 1 || frames > 8 ||
553 budget_mw < 625 || budget_mw > 50000) {
554 response.set("success", false);
555 response.set("error", "out_of_range");
556 return response;
557 }
558 if (FastLED.count() != 0) {
559 response.set("success", false);
560 response.set("error", "existing_channels");
561 return response;
562 }
563
564 fl::vector<CRGB> leds(static_cast<fl::size>(pixels * 3));
565 if (leds.size() != static_cast<fl::size>(pixels * 3)) {
566 response.set("success", false);
567 response.set("error", "allocation_failed");
568 return response;
569 }
570 for (int i = 0; i < pixels * 3; ++i)
571 leds[static_cast<fl::size>(i)] = CRGB(170 + i * 7, 220 + i * 3, 190 + i * 11);
572 fl::ChannelOptions legacy_options;
573 fl::ChannelOptions rgbw_options;
574 fl::ChannelOptions rgbww_options;
575 legacy_options.mDitherMode = DISABLE_DITHER;
576 rgbw_options.mDitherMode = DISABLE_DITHER;
577 rgbww_options.mDitherMode = DISABLE_DITHER;
578 rgbw_options.mWhiteCfg = fl::Rgbw(6000, fl::RGBW_MODE::kRGBWNullWhitePixel,
579 fl::EOrderW::W0);
580 rgbww_options.mWhiteCfg = fl::Rgbww(2700, 6500,
582 if (!rgbw_options.setColorProfile(powerBenchProfile(
585 !rgbww_options.setColorProfile(powerBenchProfile(
588 response.set("success", false);
589 response.set("error", "profile_build_failed");
590 return response;
591 }
593 auto legacy = fl::Channel::create(fl::ChannelConfig(101, timing,
594 fl::span<CRGB>(leds.data(), pixels), RGB, legacy_options));
595 auto rgbw = fl::Channel::create(fl::ChannelConfig(102, timing,
596 fl::span<CRGB>(leds.data() + pixels, pixels), RGB, rgbw_options));
597 auto rgbww = fl::Channel::create(fl::ChannelConfig(103, timing,
598 fl::span<CRGB>(leds.data() + pixels * 2, pixels), RGB, rgbww_options));
599 if (!legacy || !rgbw || !rgbww || !rgbw->isColorManaged() ||
600 !rgbww->isColorManaged()) {
601 response.set("success", false);
602 response.set("error", "channel_create_failed");
603 return response;
604 }
606 const fl::u8 prior_brightness = FastLED.getBrightness();
608 auto cleanup = fl::make_scope_exit([&]() {
609 FastLED.remove(legacy);
611 FastLED.remove(rgbww);
613 FastLED.setBrightness(prior_brightness);
615 });
616 // This is a dedicated fixture: leave the harness at its default
617 // unlimited power setting after each run, even if a caller configured
618 // a limiter before invoking this benchmark.
619 FastLED.add(legacy);
621 FastLED.add(rgbww);
623 const fl::FramePowerPlan on_plan =
624 fl::calculateFramePowerPlan(255, static_cast<fl::u32>(budget_mw));
625 const fl::FramePowerPlan off_plan =
626 fl::calculateFramePowerPlan(255, 0xffffffffu);
627
628 bool captured_all = true;
629 auto show_sample = [&](bool limited, fl::u32& hash) -> fl::u32 {
630 if (limited) FastLED.setMaxPowerInMilliWatts(budget_mw);
632 capture->count = 0;
633 FastLED.show(); // untimed warm-up after each setting change
634 const fl::u32 start = fl::micros();
635 for (int f = 0; f < frames; ++f) {
636 capture->count = 0;
637 FastLED.show();
638 }
639 const fl::u32 elapsed = fl::micros() - start;
640 captured_all = captured_all && capture->count == 3;
641 if (capture->count == 3) {
642 for (int c = 0; c < 3; ++c) {
643 const auto& bytes = capture->frames[c]->getData();
644 for (fl::size i = 0; i < bytes.size(); ++i)
645 hash = (hash ^ bytes[i]) * 16777619u;
646 capture->frames[c].reset();
647 }
648 }
649 return elapsed;
650 };
651 fl::u32 on_hash = 2166136261u, off_hash = 2166136261u;
652 fl::u32 on_us[4], off_us[4];
653 // ABBA / BAAB: each condition occupies every position once.
654 const bool order[8] = {true, false, false, true,
655 false, true, true, false};
656 int on_count = 0, off_count = 0;
657 for (int i = 0; i < 8; ++i) {
658 if (order[i]) on_us[on_count++] = show_sample(true, on_hash);
659 else off_us[off_count++] = show_sample(false, off_hash);
660 }
661 if (!captured_all) {
662 response.set("success", false);
663 response.set("error", "capture_failed");
664 return response;
665 }
666 auto median4 = [](fl::u32* values) -> fl::u32 {
667 for (int i = 1; i < 4; ++i)
668 for (int j = i; j > 0 && values[j] < values[j - 1]; --j) {
669 const fl::u32 tmp = values[j]; values[j] = values[j - 1]; values[j - 1] = tmp;
670 }
671 return (values[1] + values[2]) / 2;
672 };
673 response.set("success", true);
674 response.set("on_median_us", static_cast<int64_t>(median4(on_us)));
675 response.set("off_median_us", static_cast<int64_t>(median4(off_us)));
676 response.set("on_plan_modeled_mw", static_cast<int64_t>(on_plan.modeled_mW));
677 response.set("off_plan_modeled_mw", static_cast<int64_t>(off_plan.modeled_mW));
678 response.set("on_plan_flux_q16", static_cast<int64_t>(on_plan.flux_q16));
679 response.set("off_plan_flux_q16", static_cast<int64_t>(off_plan.flux_q16));
680 response.set("on_fnv1a", static_cast<int64_t>(on_hash));
681 response.set("off_fnv1a", static_cast<int64_t>(off_hash));
682 return response;
683 });
684#endif
685
686#if FL_COLOR_PROFILE_RUNTIME
687 // P9 (#4043): throughput of the colour pipeline on the real per-pixel
688 // path. Times `ColorManagedPixelSource::loadAndScaleRGB` -- decode, gamut
689 // map, device solve, flux and the final quantize, exactly what an encoder
690 // pulls per pixel on a colour-managed channel -- against the legacy
691 // `loadAndScale0/1/2` over the same buffer, so the ratio is the pipeline's
692 // cost over what an unmanaged channel pays. Args: { pixels, frames,
693 // dither, response_curve }. `dither` selects temporal dither (C5);
694 // `response_curve` selects a synthetic three-sample response (#4497).
695 remote.bind("colorPipelinePerf", [](const fl::json& args) -> fl::json {
696 int pixels = 256;
697 int frames = 20;
698 bool dither = false;
699 bool response_curve = false;
700 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
701 const fl::json& cfg = args[0];
702 if (cfg.contains("pixels") && cfg["pixels"].is_int()) {
703 pixels = static_cast<int>(cfg["pixels"].as_int().value());
704 }
705 if (cfg.contains("frames") && cfg["frames"].is_int()) {
706 frames = static_cast<int>(cfg["frames"].as_int().value());
707 }
708 if (cfg.contains("dither") && cfg["dither"].is_bool()) {
709 dither = cfg["dither"].as_bool().value();
710 }
711 if (cfg.contains("response_curve") && cfg["response_curve"].is_bool()) {
712 response_curve = cfg["response_curve"].as_bool().value();
713 }
714 }
715 fl::json response = fl::json::object();
716 response.set("pixels", static_cast<int64_t>(pixels));
717 response.set("frames", static_cast<int64_t>(frames));
718 response.set("dither", dither);
719 response.set("response_curve", response_curve);
720 // Each path is measured in both complementary orders. Bound all eight
721 // intervals so the synchronous RPC returns before the watchdog window.
722 if (pixels < 1 || pixels > 4096 || frames < 1 || frames > 10000 ||
723 static_cast<long>(pixels) * frames > 50000L) {
724 response.set("success", false);
725 response.set("error", "out_of_range");
726 return response;
727 }
728 fl::StreamingPipelineQ16 pipeline;
729 fl::colorimetric_response::EmitterProfile device = fl::profiles::WS2812B;
730 const fl::u16 linear_response[] = {0, 32768, 65535};
731 const fl::u16 nonlinear_response[] = {0, 16384, 65535};
732 if (response_curve) {
733 device.response_lut_r = linear_response;
734 device.response_lut_g = nonlinear_response;
735 device.response_lut_b = linear_response;
736 device.response_lut_size = 3;
737 }
739 device,
741 &pipeline)) {
742 response.set("success", false);
743 response.set("error", "pipeline_build_failed");
744 return response;
745 }
746 fl::vector<CRGB> buffer(static_cast<fl::size>(pixels));
747 for (int i = 0; i < pixels; ++i) {
748 buffer[static_cast<fl::size>(i)] = CRGB(
749 static_cast<uint8_t>(i * 7), static_cast<uint8_t>(i * 13 + 40),
750 static_cast<uint8_t>(255 - i * 3));
751 }
752 const EDitherMode mode = dither ? BINARY_DITHER : DISABLE_DITHER;
753 ColorAdjustment adjustment = ColorAdjustment::noAdjustment();
754 // One order-sensitive FNV-1a per path over every emitted byte, so a
755 // checksum match between boards means the same byte sequence.
756 uint32_t managed_hash = 2166136261u;
757 uint32_t legacy_hash = 2166136261u;
758 uint32_t managed_second_hash = 2166136261u;
759 uint32_t legacy_second_hash = 2166136261u;
760 uint32_t baab_managed_hash = 2166136261u;
761 uint32_t baab_legacy_hash = 2166136261u;
762
763 auto measure_managed = [&](uint32_t& hash) -> uint32_t {
764 const uint32_t start = fl::micros();
765 for (int f = 0; f < frames; ++f) {
766 PixelController<RGB> controller(buffer.data(), pixels, adjustment, mode);
767 fl::ColorManagedPixelSource source(controller, GRB, pipeline);
768 while (source.has(1)) {
769 uint8_t b0, b1, b2;
770 source.loadAndScaleRGB(&b0, &b1, &b2);
771 hash = (hash ^ b0) * 16777619u;
772 hash = (hash ^ b1) * 16777619u;
773 hash = (hash ^ b2) * 16777619u;
774 source.advanceData();
775 }
776 }
777 return fl::micros() - start;
778 };
779
780 auto measure_legacy = [&](uint32_t& hash) -> uint32_t {
781 const uint32_t start = fl::micros();
782 for (int f = 0; f < frames; ++f) {
783 PixelController<GRB> controller(buffer.data(), pixels, adjustment, mode);
784 while (controller.has(1)) {
785 hash = (hash ^ controller.loadAndScale0()) * 16777619u;
786 hash = (hash ^ controller.loadAndScale1()) * 16777619u;
787 hash = (hash ^ controller.loadAndScale2()) * 16777619u;
788 controller.stepDithering();
789 controller.advanceData();
790 }
791 }
792 return fl::micros() - start;
793 };
794
795 // ABBA and BAAB place each implementation in every position across
796 // the pair, reducing both linear drift and order-specific warm-up bias.
797 const uint32_t abba_m1_us = measure_managed(managed_hash);
798 const uint32_t abba_l2_us = measure_legacy(legacy_hash);
799 const uint32_t abba_l3_us = measure_legacy(legacy_second_hash);
800 const uint32_t abba_m4_us = measure_managed(managed_second_hash);
801 const uint32_t baab_l1_us = measure_legacy(baab_legacy_hash);
802 const uint32_t baab_m2_us = measure_managed(baab_managed_hash);
803 const uint32_t baab_m3_us = measure_managed(baab_managed_hash);
804 const uint32_t baab_l4_us = measure_legacy(baab_legacy_hash);
805 const uint32_t managed_us = abba_m1_us + abba_m4_us + baab_m2_us + baab_m3_us;
806 const uint32_t legacy_us = abba_l2_us + abba_l3_us + baab_l1_us + baab_l4_us;
807
808 const double n = 4.0 * static_cast<double>(pixels) *
809 static_cast<double>(frames);
810 const double managed_per = static_cast<double>(managed_us) / n;
811 const double legacy_per = static_cast<double>(legacy_us) / n;
812 response.set("success", true);
813 response.set("managed_us", static_cast<int64_t>(managed_us));
814 response.set("legacy_us", static_cast<int64_t>(legacy_us));
815 response.set("abba_m1_us", static_cast<int64_t>(abba_m1_us));
816 response.set("abba_l2_us", static_cast<int64_t>(abba_l2_us));
817 response.set("abba_l3_us", static_cast<int64_t>(abba_l3_us));
818 response.set("abba_m4_us", static_cast<int64_t>(abba_m4_us));
819 response.set("baab_l1_us", static_cast<int64_t>(baab_l1_us));
820 response.set("baab_m2_us", static_cast<int64_t>(baab_m2_us));
821 response.set("baab_m3_us", static_cast<int64_t>(baab_m3_us));
822 response.set("baab_l4_us", static_cast<int64_t>(baab_l4_us));
823 response.set("managed_us_per_pixel", managed_per);
824 response.set("legacy_us_per_pixel", legacy_per);
825 response.set("managed_pixels_per_second",
826 managed_per > 0.0 ? 1.0e6 / managed_per : 0.0);
827 response.set("managed_fnv1a", static_cast<int64_t>(managed_hash));
828 response.set("legacy_fnv1a", static_cast<int64_t>(legacy_hash));
829 response.set("managed_second_fnv1a", static_cast<int64_t>(managed_second_hash));
830 response.set("legacy_second_fnv1a", static_cast<int64_t>(legacy_second_hash));
831 response.set("baab_managed_fnv1a", static_cast<int64_t>(baab_managed_hash));
832 response.set("baab_legacy_fnv1a", static_cast<int64_t>(baab_legacy_hash));
833 return response;
834 });
835#endif
836}
837
838#endif // FL_PLATFORM_HAS_LARGE_MEMORY
CRGB leds[1]
Full PARLIO encode bench for ESP32-P4 (post-byte-LUT, #2526 landed).
PARLIO ISR-streaming functional validation (#2548 deep-dive follow-up).
size_t capture(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, size_t expected_data_bytes, int tx_pin, const fl::ChipsetTimingConfig &timing, const char *driver_name, fl::RunResult *diagnostics=nullptr)
Issue #2994 repro: compounded per-sequence timing drift vs 3.10.3.
Wave8 expansion micro-benchmark for #2526.
static void bench(const char *label)
TestState state
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
CLEDController * controller
static const int H
Definition PerfDisc.ino:21
static const int W
Definition PerfDisc.ino:20
int counter
Definition Pintest.h:190
Rgbw rgbw
fl::unique_ptr< fl::Remote > remote
Definition RpcClient.ino:43
fl::net::ble — BLE GATT transport layer for JSON-RPC
ESP32-P4 Parallel IO (PARLIO) LED channel.
Runtime chipset timing configuration for clockless LED drivers.
SPI encoder configuration for clocked LED chipsets.
void bindBenchmarkMethods(fl::Remote &remote)
fl::u8 getBrightness()
Get the current global brightness setting.
Definition FastLED.h:1605
static void remove(fl::ChannelPtr channel)
Remove a channel from the LED controller list.
void setMaxPowerInMilliWatts(fl::u32 milliwatts)
Set the maximum power to be used, given in milliwatts.
Definition FastLED.h:1697
void show(fl::u8 scale)
Update all our controllers with the current led colors, using the passed in brightness.
void setBrightness(fl::u8 scale)
Set the global brightness scaling.
Definition FastLED.h:1601
static void add(fl::ChannelPtr channel)
Add a Channel-based LED controller (from ChannelPtr)
int count()
Get how many controllers have been registered.
void clear(bool writeData=false)
Clear the leds, wiping the local array of data.
static ChannelPtr create(const ChannelConfig &config)
Create a new channel with optional mBus driver pinning.
static ChannelManager & instance() FL_NO_EXCEPT
Get the global singleton instance.
void addDriver(int priority, fl::shared_ptr< IChannelDriver > driver) FL_NO_EXCEPT
Add a driver with priority (higher priority = preferred)
bool removeDriver(fl::shared_ptr< IChannelDriver > driver) FL_NO_EXCEPT
Remove a driver from the manager.
void init(const Pins8 &pins)
Initialize from 8 pin numbers. -1 means "skip this bit position".
Definition pins.cpp.hpp:157
void writeByte(u8 byte) const
Write a single bitmask byte to the 8 pins.
Definition pins.h:46
virtual fl::string getName() const FL_NO_EXCEPT
Get the driver name for affinity binding.
Definition driver.h:217
virtual Capabilities getCapabilities() const FL_NO_EXCEPT=0
Get driver capabilities (clockless, SPI, or both)
virtual bool canHandle(const ChannelDataPtr &data) const FL_NO_EXCEPT=0
Check if this driver can handle the given channel data.
virtual void enqueue(ChannelDataPtr channelData) FL_NO_EXCEPT=0
Enqueue channel data for transmission.
virtual void show() FL_NO_EXCEPT=0
Trigger transmission of enqueued data.
virtual DriverState poll() FL_NO_EXCEPT=0
Query driver state and perform maintenance.
Minimal interface for LED channel transmission drivers.
Definition driver.h:52
JSON-RPC server with scheduling support.
Definition remote.h:42
static T & instance() FL_NO_EXCEPT
Definition singleton.h:41
const char * c_str() const FL_NO_EXCEPT
fl::optional< fl::string > as_string() const FL_NO_EXCEPT
Definition json.h:443
fl::optional< i64 > as_int() const FL_NO_EXCEPT
Definition json.h:398
fl::optional< bool > as_bool() const FL_NO_EXCEPT
Definition json.h:397
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json object() FL_NO_EXCEPT
Definition json.h:857
fl::size size() const FL_NO_EXCEPT
void resize(fl::size n) FL_NO_EXCEPT
Definition vector.h:593
T * data() FL_NO_EXCEPT
Definition vector.h:619
Shared colorimetric primitives reusable by the RGBW and (future) RGB colorimetric paths (issue #3255)...
#define FL_BUILTIN_MEMCPY(dest, src, n)
Channel transmission data - lightweight DTO for driver transmission.
Delay utilities for FastLED Includes nanosecond-precision delays, cycle counting, and microsecond del...
#define BINARY_DITHER
Enable dithering using binary dithering (only option)
Definition dither_mode.h:16
#define DISABLE_DITHER
Disable dithering.
Definition dither_mode.h:14
fl::u8 EDitherMode
The dither setting, either DISABLE_DITHER or BINARY_DITHER.
Definition dither_mode.h:19
constexpr EOrder GRB
Definition eorder.h:19
constexpr EOrder RGB
Definition eorder.h:17
Task executor — runs registered task runners and manages the run loop.
@ POWER_SETTINGS
Reset power management (setMaxPowerInMilliWatts)
Definition FastLED.h:669
fl::CRGB CRGB
Definition crgb.h:25
Platform-abstracted heap memory query functions.
FastLED's Elegant JSON Library: fl::json
Unified manager for channel drivers with priority-based fallback.
static constexpr EmitterProfile WS2812B
WavePerfRepeatResult runWavePerfRepeat(u32 W, u32 H, u32 iterations, u32 repeats, LaplacianStencil stencil) FL_NO_EXCEPT
WavePerfResult runWavePerf(u32 W, u32 H, u32 iterations, LaplacianStencil stencil) FL_NO_EXCEPT
WavePerfResult runWavePerfLoadsOnly(u32 W, u32 H, u32 iterations, LaplacianStencil stencil) FL_NO_EXCEPT
unsigned char u8
Definition stdint.h:131
constexpr ChipsetTimingConfig makeTimingConfig() FL_NO_EXCEPT
Convert compile-time CHIPSET type to runtime timing config.
void delaycycles< 50 >() FL_NO_EXCEPT
fl::u32 micros()
Universal microsecond timer - returns microseconds since system startup.
@ kRGBWNullWhitePixel
Definition rgbw.h:17
@ Low
Logic low (0V / GND)
Definition pin.h:50
@ WwWcStart
warm-W, cool-W, then RGB
Definition rgbww.h:30
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:414
scope_exit< decay_t< EF > > make_scope_exit(EF &&f)
Factory for C++11/14 (no CTAD).
Definition scope_exit.h:48
void pinMode(int pin, PinMode mode)
Set pin mode (input, output, pull-up, pull-down)
Definition pin.cpp.hpp:378
void delaycycles< 10 >() FL_NO_EXCEPT
void digitalWrite(int pin, PinValue val)
Write digital value to pin.
Definition pin.cpp.hpp:51
bool buildStreamingPipelineQ16(const SourceProfile &source, const colorimetric_response::EmitterProfile &device, GamutPolicy policy, StreamingPipelineQ16 *out, const Chromaticity *target_white) FL_NO_EXCEPT
Bind a source declaration and a device profile into a pipeline.
@ kRGBWWColorimetric
Definition rgbww.h:44
@ Output
Digital output (push-pull)
Definition pin.h:43
FASTLED_FORCE_INLINE void delayNanoseconds() FL_NO_EXCEPT
Delay for a compile-time constant number of nanoseconds.
Definition delay.h:47
int pins[8]
Definition pins.h:19
corkscrew_args args
Definition old.h:149
Promise-based fluent API for FastLED - standalone async primitives.
#define FL_NO_EXCEPT
RAII scope guard — executes a callable on scope exit.
Umbrella header for SIMD subsystem.
unsigned char uint8_t
Definition stdint.h:208
fl::u32 uint32_t
Definition stdint.h:218
static ColorAdjustment noAdjustment()
the per-channel scale values premixed with brightness.
bool setColorProfile(const EmitterProfile &profile) FL_NO_EXCEPT
Definition options.h:99
fl::variant< fl::Empty, Rgbw, Rgbww > mWhiteCfg
White-channel selection (variant): Empty = plain RGB, Rgbw = 4-channel, Rgbww = 5-channel.
Definition options.h:91
fl::u8 mDitherMode
Definition options.h:88
static constexpr SourceProfile linearSrgb() FL_NO_EXCEPT