Skip to content

Commit a4bcf24

Browse files
committed
add workload compare suite
1 parent 31c4743 commit a4bcf24

13 files changed

Lines changed: 1528 additions & 0 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,37 @@ Current best module tradeoff reference (ESP32, `LEAN_NET`, manual pattern vs mod
117117

118118
In other words: the modules do cost memory, but the current tuned path keeps that cost bounded and pays it back with better transport throughput and less queue buildup under the same synthetic workload window.
119119

120+
## General-Purpose Validation Workloads
121+
122+
To avoid overfitting the runtime to a single project, ZeroKernel now also ships with three reusable compare workloads:
123+
124+
- `EnvMonitor`: sensor sampling + filtering + threshold alarm
125+
- `TelemetryGateway`: queue-heavy transport orchestration without tying the example to real Wi-Fi credentials
126+
- `IndustrialLoop`: fast control loop + command handling + diagnostics + safe-mode recovery
127+
128+
Current ESP32 references from the compare runners:
129+
130+
- `EnvMonitor`
131+
- `sample_runs`: `49 -> 50`
132+
- `fast_avg_lag_us`: `871 -> 0`
133+
- `fast_max_lag_us`: `1780 -> 0`
134+
- `fast_miss`: `24 -> 0`
135+
- `TelemetryGateway`
136+
- `http_ok`: `23 -> 27`
137+
- `mqtt_ok`: `23 -> 27`
138+
- `http_rate`: `88 -> 87`
139+
- `mqtt_rate`: `92 -> 90`
140+
- `http_queue`: `0 -> 0`
141+
- `mqtt_queue`: `0 -> 0`
142+
- `IndustrialLoop`
143+
- `control_runs`: `99 -> 101`
144+
- `fast_avg_lag_us`: `241 -> 0`
145+
- `fast_max_lag_us`: `3983 -> 0`
146+
- `fast_miss`: `9 -> 0`
147+
- `recoveries`: `0 -> 1`
148+
149+
These workloads are intentionally broader than the micro-benchmarks: they show the runtime under sensor, transport, and control-loop pressure without depending on a single application codebase.
150+
120151
## Field Validation: ESP8266 Seismic Node
121152

122153
Measured on a real `ESP8266` direct-AP seismic node using:

docs/testing.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,40 @@ This compares the fault-oriented workload against a manual-loop baseline and rep
9797
- RAM deltas
9898
- ending runtime state
9999

100+
## General-purpose workload compares
101+
102+
- `scripts/run_esp32_env_monitor_compare.sh [/dev/ttyUSB1]`
103+
104+
This compares a simple sensor-monitor workload against a manual-loop baseline and reports:
105+
106+
- sample cadence
107+
- filter cadence
108+
- fast-cycle lag and misses
109+
- RAM deltas
110+
111+
- `scripts/run_esp32_gateway_compare.sh [/dev/ttyUSB1]`
112+
113+
This compares a credential-free transport gateway workload against a manual-loop baseline and reports:
114+
115+
- Wi-Fi maintenance attempts and reconnects
116+
- HTTP and MQTT success/failure counts
117+
- effective success rates
118+
- queue depth and RAM deltas
119+
120+
- `scripts/run_esp32_industrial_compare.sh [/dev/ttyUSB1]`
121+
122+
This compares a control-loop workload against a manual-loop baseline and reports:
123+
124+
- control-loop cadence
125+
- command handling cadence
126+
- fast-cycle lag and misses
127+
- safe-mode entries and recoveries
128+
- RAM deltas
129+
130+
- `scripts/run_workload_matrix.sh`
131+
132+
This compiles the three workload pairs across the installed Arduino cores (ESP8266, ESP32, RP2040, STM32) and prints per-target PASS lines with RAM and flash usage.
133+
100134
## Cross-board command smoke
101135

102136
- `examples/CommandQueue`
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#include <Arduino.h>
2+
3+
namespace {
4+
5+
const unsigned long kSamplePeriodUs = 100000UL;
6+
const unsigned long kFilterPeriodMs = 200UL;
7+
const unsigned long kAlarmPeriodMs = 150UL;
8+
const unsigned long kReportPeriodMs = 5000UL;
9+
const unsigned long kFastMissThresholdUs = 1500UL;
10+
11+
unsigned long gNextExpectedUs = 0;
12+
unsigned long gLagAccumUs = 0;
13+
unsigned long gMaxLagUs = 0;
14+
unsigned long gFastMisses = 0;
15+
unsigned long gSampleRuns = 0;
16+
unsigned long gFilterRuns = 0;
17+
unsigned long gAlarmTrips = 0;
18+
unsigned long gLastSampleAtUs = 0;
19+
unsigned long gLastFilterAtMs = 0;
20+
unsigned long gLastAlarmAtMs = 0;
21+
unsigned long gWindowStartedAtMs = 0;
22+
unsigned long gSensorValue = 420;
23+
unsigned long gFilteredValue = 420;
24+
25+
void busyWaitUs(unsigned long durationUs) {
26+
const unsigned long startedAtUs = micros();
27+
while ((micros() - startedAtUs) < durationUs) {
28+
}
29+
}
30+
31+
void resetWindow(unsigned long nowMs) {
32+
gWindowStartedAtMs = nowMs;
33+
gLagAccumUs = 0;
34+
gMaxLagUs = 0;
35+
gFastMisses = 0;
36+
gSampleRuns = 0;
37+
gFilterRuns = 0;
38+
gAlarmTrips = 0;
39+
}
40+
41+
void runFilter() {
42+
++gFilterRuns;
43+
busyWaitUs(1800UL);
44+
gFilteredValue = ((gFilteredValue * 3UL) + gSensorValue) / 4UL;
45+
}
46+
47+
void runAlarm() {
48+
if (gFilteredValue > 470UL) {
49+
++gAlarmTrips;
50+
}
51+
}
52+
53+
void runSample(unsigned long nowUs) {
54+
if (gNextExpectedUs == 0UL) {
55+
gNextExpectedUs = nowUs;
56+
}
57+
58+
const unsigned long lagUs = nowUs > gNextExpectedUs ? (nowUs - gNextExpectedUs) : 0UL;
59+
gLagAccumUs += lagUs;
60+
if (lagUs > gMaxLagUs) {
61+
gMaxLagUs = lagUs;
62+
}
63+
if (lagUs > kFastMissThresholdUs) {
64+
++gFastMisses;
65+
}
66+
67+
++gSampleRuns;
68+
gSensorValue = 420UL + ((millis() / 23UL) % 90UL);
69+
gNextExpectedUs += kSamplePeriodUs;
70+
if (nowUs > (gNextExpectedUs + kSamplePeriodUs)) {
71+
gNextExpectedUs = nowUs + kSamplePeriodUs;
72+
}
73+
}
74+
75+
void maybeReport(unsigned long nowMs) {
76+
if ((nowMs - gWindowStartedAtMs) < kReportPeriodMs) {
77+
return;
78+
}
79+
80+
const unsigned long avgLagUs = gSampleRuns == 0 ? 0 : (gLagAccumUs / gSampleRuns);
81+
Serial.print("BASELINE_ENVMONITOR window_ms=");
82+
Serial.print(nowMs - gWindowStartedAtMs);
83+
Serial.print(" sample_runs=");
84+
Serial.print(gSampleRuns);
85+
Serial.print(" filter_runs=");
86+
Serial.print(gFilterRuns);
87+
Serial.print(" fast_avg_lag_us=");
88+
Serial.print(avgLagUs);
89+
Serial.print(" fast_max_lag_us=");
90+
Serial.print(gMaxLagUs);
91+
Serial.print(" fast_miss=");
92+
Serial.print(gFastMisses);
93+
Serial.print(" alarm_trips=");
94+
Serial.println(gAlarmTrips);
95+
96+
resetWindow(nowMs);
97+
}
98+
99+
} // namespace
100+
101+
void setup() {
102+
Serial.begin(115200);
103+
delay(50);
104+
105+
const unsigned long nowMs = millis();
106+
gLastSampleAtUs = micros();
107+
gLastFilterAtMs = nowMs;
108+
gLastAlarmAtMs = nowMs;
109+
resetWindow(nowMs);
110+
}
111+
112+
void loop() {
113+
const unsigned long nowMs = millis();
114+
const unsigned long nowUs = micros();
115+
116+
if ((nowMs - gLastFilterAtMs) >= kFilterPeriodMs) {
117+
gLastFilterAtMs += kFilterPeriodMs;
118+
runFilter();
119+
}
120+
121+
if ((nowMs - gLastAlarmAtMs) >= kAlarmPeriodMs) {
122+
gLastAlarmAtMs += kAlarmPeriodMs;
123+
runAlarm();
124+
}
125+
126+
if ((nowUs - gLastSampleAtUs) >= kSamplePeriodUs) {
127+
gLastSampleAtUs += kSamplePeriodUs;
128+
runSample(nowUs);
129+
}
130+
131+
maybeReport(nowMs);
132+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#include <ZeroKernel.h>
2+
#include <adapters/PowerSaveLoopAdapter.h>
3+
4+
namespace {
5+
6+
const unsigned long kReportPeriodMs = 5000UL;
7+
const unsigned long kFastMissThresholdUs = 1500UL;
8+
9+
unsigned long gNextExpectedUs = 0;
10+
unsigned long gLagAccumUs = 0;
11+
unsigned long gMaxLagUs = 0;
12+
unsigned long gFastMisses = 0;
13+
unsigned long gSampleRuns = 0;
14+
unsigned long gFilterRuns = 0;
15+
unsigned long gAlarmTrips = 0;
16+
unsigned long gWindowStartedAtMs = 0;
17+
unsigned long gSensorValue = 420;
18+
unsigned long gFilteredValue = 420;
19+
20+
unsigned long boardMillis() {
21+
return millis();
22+
}
23+
24+
void busyWaitUs(unsigned long durationUs) {
25+
const unsigned long startedAtUs = micros();
26+
while ((micros() - startedAtUs) < durationUs) {
27+
}
28+
}
29+
30+
void resetWindow(unsigned long nowMs) {
31+
gWindowStartedAtMs = nowMs;
32+
gLagAccumUs = 0;
33+
gMaxLagUs = 0;
34+
gFastMisses = 0;
35+
gSampleRuns = 0;
36+
gFilterRuns = 0;
37+
gAlarmTrips = 0;
38+
}
39+
40+
void sampleTask() {
41+
const unsigned long nowUs = micros();
42+
if (gNextExpectedUs == 0UL) {
43+
gNextExpectedUs = nowUs;
44+
}
45+
46+
const unsigned long lagUs = nowUs > gNextExpectedUs ? (nowUs - gNextExpectedUs) : 0UL;
47+
gLagAccumUs += lagUs;
48+
if (lagUs > gMaxLagUs) {
49+
gMaxLagUs = lagUs;
50+
}
51+
if (lagUs > kFastMissThresholdUs) {
52+
++gFastMisses;
53+
}
54+
55+
++gSampleRuns;
56+
gSensorValue = 420UL + ((millis() / 23UL) % 90UL);
57+
gNextExpectedUs += 100000UL;
58+
if (nowUs > (gNextExpectedUs + 100000UL)) {
59+
gNextExpectedUs = nowUs + 100000UL;
60+
}
61+
}
62+
63+
void filterTask() {
64+
++gFilterRuns;
65+
busyWaitUs(1800UL);
66+
gFilteredValue = ((gFilteredValue * 3UL) + gSensorValue) / 4UL;
67+
}
68+
69+
void alarmTask() {
70+
if (gFilteredValue > 470UL) {
71+
++gAlarmTrips;
72+
}
73+
}
74+
75+
void reportTask() {
76+
const unsigned long nowMs = millis();
77+
if ((nowMs - gWindowStartedAtMs) < kReportPeriodMs) {
78+
return;
79+
}
80+
81+
const unsigned long avgLagUs = gSampleRuns == 0 ? 0 : (gLagAccumUs / gSampleRuns);
82+
Serial.print("ZEROKERNEL_ENVMONITOR window_ms=");
83+
Serial.print(nowMs - gWindowStartedAtMs);
84+
Serial.print(" sample_runs=");
85+
Serial.print(gSampleRuns);
86+
Serial.print(" filter_runs=");
87+
Serial.print(gFilterRuns);
88+
Serial.print(" fast_avg_lag_us=");
89+
Serial.print(avgLagUs);
90+
Serial.print(" fast_max_lag_us=");
91+
Serial.print(gMaxLagUs);
92+
Serial.print(" fast_miss=");
93+
Serial.print(gFastMisses);
94+
Serial.print(" alarm_trips=");
95+
Serial.println(gAlarmTrips);
96+
97+
resetWindow(nowMs);
98+
}
99+
100+
} // namespace
101+
102+
void setup() {
103+
Serial.begin(115200);
104+
delay(50);
105+
106+
ZeroKernel.begin(boardMillis);
107+
ZeroKernel.setIdleStrategy(zerokernel::Kernel::kIdleSleep);
108+
109+
zerokernel::Kernel::ExecutionContract sampleContract = {};
110+
sampleContract.flags = zerokernel::Kernel::kContractRunImmediate |
111+
zerokernel::Kernel::kContractDropIfLate;
112+
zerokernel::Kernel::TaskConfig sampleConfig = {
113+
"EnvSample",
114+
sampleTask,
115+
100,
116+
0,
117+
0,
118+
zerokernel::Kernel::kPriorityCritical,
119+
true,
120+
sampleContract,
121+
zerokernel::Kernel::kCapNone};
122+
123+
ZeroKernel.addTask(sampleConfig);
124+
ZeroKernel.addTask("EnvFilter", filterTask, 200, 0, true);
125+
ZeroKernel.setTaskPriority("EnvFilter", zerokernel::Kernel::kPriorityNormal);
126+
ZeroKernel.addTask("EnvAlarm", alarmTask, 150, 0, true);
127+
ZeroKernel.setTaskPriority("EnvAlarm", zerokernel::Kernel::kPriorityHigh);
128+
ZeroKernel.addTask("EnvReport", reportTask, 250, 0, true);
129+
130+
resetWindow(millis());
131+
}
132+
133+
void loop() {
134+
zerokernel::adapters::powerSaveTick(ZeroKernel);
135+
}

0 commit comments

Comments
 (0)