Skip to content

Commit d99fddc

Browse files
committed
token-based high-precision measurement
1 parent 50ed3ed commit d99fddc

5 files changed

Lines changed: 301 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ All notable changes to this project will be documented in this file.
99
- Optional average smoothing in `CpuUsageSample` via `smoothedAverage`, with configurable `smoothingMode` (`None`, `RollingMean`, `Ewma`), rolling window size, and EWMA alpha.
1010
- `getLastSmoothedAverage()` helper for quickly reading the latest trend/baseline value when smoothing is enabled.
1111
- `CpuMonitorConfig::usePSRAMBuffers` toggle to prefer PSRAM-backed internal history and callback container/snapshot storage through `ESPBufferManager`, with automatic fallback to normal heap.
12+
- Token-based measurement API via `startMeasure()` / `stopMeasure(token)` with precise timing fields (`durationUs`, `durationMs`, `durationSec`) and optional per-window CPU usage in `CpuMeasure`.
1213

1314
### Changed
1415
- Removed the library-provided global `cpuMonitor`; create and manage your own `ESPCpuMonitor` instance (only one active monitor at a time) and updated examples/docs accordingly.
1516
- Made `ESPCpuMonitor` non-copyable/movable to prevent accidental double-free of FreeRTOS handles.
1617
- `toJson()` now exports `avgSmoothed` when smoothing is enabled and a smoothed sample is available.
18+
- Baseline calibration now tracks idle rates per microsecond so code-path measurements can estimate CPU usage across arbitrary measurement durations.
1719

1820
## [1.0.1] - 2025-12-03
1921
### Fixed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ESPCpuMonitor is a tiny C++17 helper that estimates per-core CPU usage on ESP32
1414
- Optional smoothing helpers for average CPU usage (`RollingMean` or `EWMA`) so you can track baseline/trend load without forcing smoothing globally (disabled by default, fixed-size buffer when enabled).
1515
- Thread-safe per-sample callbacks for logging, telemetry, or UI updates; swap `enablePerCore` off to collapse cores to an overall average.
1616
- Manual `sampleNow()` path for users who already have their own schedulers (set `sampleIntervalMs` to `0`).
17+
- Token-based manual measurement API (`startMeasure()` / `stopMeasure(token)`) with precise microsecond durations and optional per-window CPU usage.
1718
- Optional CPU temperature readings (current + running average) using the ESP-IDF temperature sensor driver, gracefully disabled when unsupported.
1819
- Optional ArduinoJson export helper to stream samples over HTTP/MQTT/WebSockets alongside the rest of ESPToolKit.
1920

@@ -75,6 +76,25 @@ When you set `sampleIntervalMs` to `0`, call `sampleNow()` on your monitor insta
7576
If temperature is enabled, `getLastTemperature(current, average)` returns the latest reading and running mean (returns `false` when unsupported or not ready).
7677
When your feature shuts down (task exit, OTA handoff, mode switch), call `cpuMonitor.deinit()` to release idle hooks and timer resources.
7778
79+
Measure a specific code path:
80+
81+
```cpp
82+
CpuMeasureToken token = cpuMonitor.startMeasure();
83+
// do work...
84+
CpuMeasure result = cpuMonitor.stopMeasure(token);
85+
86+
if (result.valid) {
87+
// precise timings
88+
double ms = result.durationMs;
89+
double sec = result.durationSec;
90+
91+
// CPU usage for this window (available after calibration)
92+
if (result.hasCpuData) {
93+
float avg = result.averageUsage;
94+
}
95+
}
96+
```
97+
7898
## Gotchas
7999
- CPU usage numbers are floats in percent so you can see tiny changes; `1.0` means ~1% busy, not 100%. Feel free to round (`%.0f%%`) in your logs/UI if you prefer whole numbers.
80100
- Allow the calibration window (`calibrationSamples`) to finish before trusting numbers; keep the device as idle as possible during that phase.
@@ -91,6 +111,7 @@ When your feature shuts down (task exit, OTA handoff, mode switch), call `cpuMon
91111
- `bool getLastSample(CpuUsageSample &out) const` / `float getLastAverage() const` / `float getLastSmoothedAverage() const` – read the latest sample; average/smoothed values return `-1.0f` until ready (or when smoothing is disabled).
92112
- `bool getLastTemperature(float &currentC, float &averageC) const` – latest temperature and running average; returns `false` if disabled, unsupported, or not yet sampled.
93113
- `std::vector<CpuUsageSample> history() const` – copy of the ring buffer (size capped by `historySize`, `0` disables storage).
114+
- `CpuMeasureToken startMeasure() const` / `CpuMeasure stopMeasure(const CpuMeasureToken &token) const` – token-based code-path timing using `esp_timer_get_time()`. Requires `init()`. `stopMeasure()` returns `valid=false` for invalid/stale tokens. `hasCpuData` becomes true once baseline calibration is available.
94115
- `bool sampleNow(CpuUsageSample &out)` – immediate sampling, useful when periodic timer is disabled.
95116
- `void onSample(CpuSampleCallback cb)` – subscribe to every stored sample.
96117
- `void toJson(const CpuUsageSample&, JsonDocument &doc)` – ArduinoJson export helper (compiled only when ArduinoJson is available).
@@ -107,6 +128,7 @@ When your feature shuts down (task exit, OTA handoff, mode switch), call `cpuMon
107128
- `smoothingAlpha` (default `0.2`) – EWMA alpha (used when `smoothingMode = Ewma`, clamped to `(0.0, 1.0]`).
108129

109130
`CpuUsageSample` contains `timestampUs`, `perCore[portNUM_PROCESSORS]`, `average` (%), `smoothedAverage` (%/NaN when smoothing is disabled), `temperatureC`, and `temperatureAvgC` (NaN when unsupported).
131+
`CpuMeasure` contains `startedUs`, `endedUs`, `durationUs`, convenience `durationMs`/`durationSec`, start/end core IDs, `idleDelta[]`, and per-window usage (`perCoreUsage[]` + `averageUsage`).
110132

111133
## Restrictions
112134
- ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with `esp_freertos_hooks.h` available.

src/esp_cpu_monitor/cpu_monitor.cpp

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ static bool isValidSmoothingMode(CpuSmoothingMode mode) {
4040
mode == CpuSmoothingMode::Ewma;
4141
}
4242

43+
static float clampUsagePercent(float usage) {
44+
if (usage < 0.0f) {
45+
return 0.0f;
46+
}
47+
if (usage > 100.0f) {
48+
return 100.0f;
49+
}
50+
return usage;
51+
}
52+
4353
void ESPCpuMonitor::resetState(const CpuMonitorConfig &cfg) {
4454
config_ = cfg;
4555
if (!isValidSmoothingMode(config_.smoothingMode)) {
@@ -56,6 +66,8 @@ void ESPCpuMonitor::resetState(const CpuMonitorConfig &cfg) {
5666
}
5767
calibrationSamplesNeeded_ = config_.calibrationSamples == 0 ? 1 : config_.calibrationSamples;
5868
calibrationSamplesDone_ = 0;
69+
calibrationWindowUs_ = 0;
70+
calibrationLastSampleUs_ = static_cast<uint64_t>(esp_timer_get_time());
5971
hasSample_ = false;
6072
calibrated_ = false;
6173
history_ = CpuMonitorDeque<CpuUsageSample>(
@@ -76,6 +88,8 @@ void ESPCpuMonitor::resetState(const CpuMonitorConfig &cfg) {
7688
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
7789
prevIdle_[i] = 0;
7890
idleBaseline_[i] = 0.0f;
91+
idleBaselineRatePerUs_[i] = 0.0f;
92+
calibrationIdleTotal_[i] = 0;
7993
lastSample_.perCore[i] = 0.0f;
8094
s_idleCount[i] = 0;
8195
}
@@ -104,6 +118,10 @@ bool ESPCpuMonitor::init(const CpuMonitorConfig &cfg) {
104118
}
105119

106120
resetState(cfg);
121+
measureEpoch_++;
122+
if (measureEpoch_ == 0) {
123+
measureEpoch_ = 1;
124+
}
107125

108126
if (!mutex_) {
109127
mutex_ = xSemaphoreCreateMutex();
@@ -280,6 +298,84 @@ std::vector<CpuUsageSample> ESPCpuMonitor::history() const {
280298
return out;
281299
}
282300

301+
CpuMeasureToken ESPCpuMonitor::startMeasure() const {
302+
CpuMeasureToken token{};
303+
if (!isInitialized()) {
304+
return token;
305+
}
306+
307+
token.valid = true;
308+
token.startedUs = static_cast<uint64_t>(esp_timer_get_time());
309+
token.startedCore = xPortGetCoreID();
310+
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
311+
token.idleStart[i] = s_idleCount[i];
312+
}
313+
314+
lock();
315+
token.ownerMarker = this;
316+
token.epoch = measureEpoch_;
317+
token.hasCpuData = calibrated_;
318+
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
319+
token.idleBaselineRatePerUs[i] = idleBaselineRatePerUs_[i];
320+
}
321+
unlock();
322+
323+
return token;
324+
}
325+
326+
CpuMeasure ESPCpuMonitor::stopMeasure(const CpuMeasureToken &token) const {
327+
CpuMeasure result{};
328+
if (!isInitialized()) {
329+
return result;
330+
}
331+
if (!token.valid || token.ownerMarker != this) {
332+
return result;
333+
}
334+
335+
lock();
336+
const uint32_t currentEpoch = measureEpoch_;
337+
unlock();
338+
if (token.epoch != currentEpoch) {
339+
return result;
340+
}
341+
342+
result.valid = true;
343+
result.startedUs = token.startedUs;
344+
result.endedUs = static_cast<uint64_t>(esp_timer_get_time());
345+
result.durationUs = result.endedUs >= result.startedUs ? result.endedUs - result.startedUs : 0;
346+
result.durationMs = static_cast<double>(result.durationUs) / 1000.0;
347+
result.durationSec = static_cast<double>(result.durationUs) / 1000000.0;
348+
result.startedCore = token.startedCore;
349+
result.endedCore = xPortGetCoreID();
350+
351+
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
352+
const uint64_t currentIdle = s_idleCount[i];
353+
result.idleDelta[i] = currentIdle - token.idleStart[i];
354+
}
355+
356+
if (!token.hasCpuData || result.durationUs == 0) {
357+
return result;
358+
}
359+
360+
const float durationUs = static_cast<float>(result.durationUs);
361+
float usageSum = 0.0f;
362+
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
363+
const float baselineRate = token.idleBaselineRatePerUs[i];
364+
const float expectedIdle = baselineRate * durationUs;
365+
if (expectedIdle <= 0.0f) {
366+
return result;
367+
}
368+
const float idleRatio = static_cast<float>(result.idleDelta[i]) / expectedIdle;
369+
const float usage = clampUsagePercent(100.0f * (1.0f - idleRatio));
370+
result.perCoreUsage[i] = usage;
371+
usageSum += usage;
372+
}
373+
374+
result.hasCpuData = true;
375+
result.averageUsage = usageSum / static_cast<float>(portNUM_PROCESSORS);
376+
return result;
377+
}
378+
283379
bool ESPCpuMonitor::sampleNow(CpuUsageSample &out) {
284380
if (!isInitialized()) {
285381
ESP_LOGE(TAG, "Call init() before sampleNow()");
@@ -364,21 +460,30 @@ bool ESPCpuMonitor::computeSampleLocked(CpuUsageSample &out) {
364460
float perCoreUsage[portNUM_PROCESSORS] = {};
365461
float avg = 0.0f;
366462
uint64_t nowUs = esp_timer_get_time();
463+
uint64_t deltaUs = 0;
464+
if (nowUs > calibrationLastSampleUs_) {
465+
deltaUs = nowUs - calibrationLastSampleUs_;
466+
}
467+
calibrationLastSampleUs_ = nowUs;
468+
const float deltaUsFloat = static_cast<float>(deltaUs);
367469

368470
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
369471
uint64_t currentIdle = s_idleCount[i];
370472
uint64_t deltaIdle = currentIdle - prevIdle_[i];
371473

372474
if (!calibrated_) {
373475
idleBaseline_[i] += static_cast<float>(deltaIdle);
476+
calibrationIdleTotal_[i] += deltaIdle;
374477
} else {
375478
float baseline = idleBaseline_[i] <= 0.0f ? 1.0f : idleBaseline_[i];
376-
float idleRatio = baseline > 0.0f ? static_cast<float>(deltaIdle) / baseline : 0.0f;
377-
float usage = 100.0f * (1.0f - idleRatio);
378-
if (usage < 0.0f)
379-
usage = 0.0f;
380-
if (usage > 100.0f)
381-
usage = 100.0f;
479+
if (idleBaselineRatePerUs_[i] > 0.0f && deltaUs > 0) {
480+
const float expectedIdle = idleBaselineRatePerUs_[i] * deltaUsFloat;
481+
if (expectedIdle > 0.0f) {
482+
baseline = expectedIdle;
483+
}
484+
}
485+
const float idleRatio = baseline > 0.0f ? static_cast<float>(deltaIdle) / baseline : 0.0f;
486+
const float usage = clampUsagePercent(100.0f * (1.0f - idleRatio));
382487
perCoreUsage[i] = usage;
383488
avg += usage;
384489
}
@@ -387,13 +492,25 @@ bool ESPCpuMonitor::computeSampleLocked(CpuUsageSample &out) {
387492
}
388493

389494
if (!calibrated_) {
495+
calibrationWindowUs_ += deltaUs;
390496
calibrationSamplesDone_++;
391497
if (calibrationSamplesDone_ >= calibrationSamplesNeeded_) {
498+
const float calibrationWindowUs = static_cast<float>(calibrationWindowUs_);
499+
const float fallbackWindowUs =
500+
static_cast<float>(config_.sampleIntervalMs) * 1000.0f;
392501
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
393502
idleBaseline_[i] /= static_cast<float>(calibrationSamplesNeeded_);
394503
if (idleBaseline_[i] < 1.0f) {
395504
idleBaseline_[i] = 1.0f;
396505
}
506+
if (calibrationWindowUs > 0.0f) {
507+
idleBaselineRatePerUs_[i] =
508+
static_cast<float>(calibrationIdleTotal_[i]) / calibrationWindowUs;
509+
} else if (fallbackWindowUs > 0.0f) {
510+
idleBaselineRatePerUs_[i] = idleBaseline_[i] / fallbackWindowUs;
511+
} else {
512+
idleBaselineRatePerUs_[i] = 0.0f;
513+
}
397514
}
398515
calibrated_ = true;
399516
ESP_LOGI(

src/esp_cpu_monitor/cpu_monitor.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,39 @@ struct CpuUsageSample {
111111
float temperatureAvgC = std::numeric_limits<float>::quiet_NaN();
112112
};
113113

114+
struct CpuMeasureToken {
115+
bool valid = false;
116+
uint64_t startedUs = 0;
117+
int startedCore = -1;
118+
uint64_t idleStart[portNUM_PROCESSORS]{};
119+
float idleBaselineRatePerUs[portNUM_PROCESSORS]{};
120+
const void *ownerMarker = nullptr;
121+
uint32_t epoch = 0;
122+
bool hasCpuData = false;
123+
};
124+
125+
struct CpuMeasure {
126+
CpuMeasure() {
127+
for (int i = 0; i < portNUM_PROCESSORS; ++i) {
128+
perCoreUsage[i] = std::numeric_limits<float>::quiet_NaN();
129+
}
130+
averageUsage = std::numeric_limits<float>::quiet_NaN();
131+
}
132+
133+
bool valid = false;
134+
bool hasCpuData = false;
135+
uint64_t startedUs = 0;
136+
uint64_t endedUs = 0;
137+
uint64_t durationUs = 0;
138+
double durationMs = 0.0;
139+
double durationSec = 0.0;
140+
int startedCore = -1;
141+
int endedCore = -1;
142+
uint64_t idleDelta[portNUM_PROCESSORS]{};
143+
float perCoreUsage[portNUM_PROCESSORS]{};
144+
float averageUsage = std::numeric_limits<float>::quiet_NaN();
145+
};
146+
114147
using CpuSampleCallback = std::function<void(const CpuUsageSample &)>;
115148

116149
class ESPCpuMonitor {
@@ -136,6 +169,9 @@ class ESPCpuMonitor {
136169
bool getLastTemperature(float &currentC, float &averageC) const;
137170
std::vector<CpuUsageSample> history() const;
138171

172+
CpuMeasureToken startMeasure() const;
173+
CpuMeasure stopMeasure(const CpuMeasureToken &token) const;
174+
139175
// Trigger sampling immediately (useful when sampleIntervalMs == 0)
140176
bool sampleNow(CpuUsageSample &out);
141177

@@ -170,6 +206,11 @@ class ESPCpuMonitor {
170206
CpuMonitorConfig config_{};
171207
uint64_t prevIdle_[portNUM_PROCESSORS];
172208
float idleBaseline_[portNUM_PROCESSORS];
209+
float idleBaselineRatePerUs_[portNUM_PROCESSORS];
210+
uint64_t calibrationIdleTotal_[portNUM_PROCESSORS];
211+
uint64_t calibrationWindowUs_ = 0;
212+
uint64_t calibrationLastSampleUs_ = 0;
213+
uint32_t measureEpoch_ = 0;
173214
CpuUsageSample lastSample_{};
174215
bool hasSample_ = false;
175216
bool calibrated_ = false;

0 commit comments

Comments
 (0)