Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

recurrent scheduled functions max delay update #8949

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2757aa2
Revert `compute_scheduled_recurrent_grain`
dok-net Jul 1, 2023
a81e544
wip
dok-net Jul 1, 2023
588e153
Add function to retrieve the time remaining until the (next) timeout …
dok-net Nov 26, 2019
a435fa5
Rename and reformat according to review.
dok-net Nov 27, 2019
232961e
Revert _current as member change.
dok-net Nov 27, 2019
210c3d4
Fix expiredOneShot to properly stay expired forever unless reset.
dok-net Nov 27, 2019
a6456d0
Fix "reverse notation return statement"
dok-net Nov 27, 2019
9863f71
Reset() was wrecked by resetting _timeout. Reverted.
dok-net Nov 27, 2019
9c83f9a
Expired check ALWAYS executes yield policy BEFORE checking expiration…
dok-net Dec 2, 2019
4cbe06b
Reformat
dok-net Dec 3, 2019
20e6378
Fix comment, remove redundant parentheses
dok-net Dec 4, 2019
b0eb345
expired() is not const qualified in PolledTmeout.
dok-net Mar 15, 2021
a3612dd
expiredOneShot() qualifed as const, despite asymmetry with expired() …
dok-net Mar 16, 2021
c88af7b
Remove alwaysExpired, it is identical to 0 and obfuscates the code.
dok-net Apr 5, 2021
4eab7e4
Somewhat easier on the human reader.
dok-net Apr 5, 2021
b5285c3
Avoid possible unnecessary time conversion.
dok-net Apr 5, 2021
b54576f
Fix one-shot mode in resetToNeverExpires(). Add convenient stop() syn…
dok-net Apr 5, 2021
100d2c4
Due to review and discussion, _oneShotExpired removed again.
dok-net Apr 5, 2021
81b90c9
Revert removal of PolledTimeout::alwaysExpired
dok-net Jul 1, 2023
eaf05e9
Finalize scheduled recurrent function delay logic.
dok-net Jul 1, 2023
a09726d
Fix millis/micros mismatch.
dok-net Jul 1, 2023
4d3655b
ISR safety.
dok-net Jul 1, 2023
55d465a
Returning max delay value that is uint-wrap around safe.
dok-net Jul 2, 2023
c41a2c2
Add pseudo-get delay function for scheduled functions
dok-net Jul 2, 2023
4e654d1
Worked out input from PR review.
dok-net Jul 20, 2023
717f4ad
use single constexpr definition instead of repeated expression.
dok-net Jul 27, 2023
538025a
Simplified roll-over logic
dok-net Jul 30, 2023
861cf02
Add rationale for HALF_MAX_MICROS
dok-net Jul 30, 2023
b3b4aa0
Use CAS to minimize critical sections.
dok-net Sep 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions cores/esp8266/PolledTimeout.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ class timeoutTemplate
IRAM_ATTR // fast
bool expired()
{
YieldPolicyT::execute(); //in case of DoNothing: gets optimized away
if(PeriodicT) //in case of false: gets optimized away
return expiredRetrigger();
return expiredOneShot();
bool hasExpired = PeriodicT ? expiredRetrigger() : expiredOneShot();
if (!hasExpired) //in case of DoNothing: gets optimized away
YieldPolicyT::execute();
return hasExpired;
}

IRAM_ATTR // fast
Expand All @@ -186,7 +186,7 @@ class timeoutTemplate
{
reset();
_timeout = TimePolicyT::toTimeTypeUnit(newUserTimeout);
_neverExpires = (newUserTimeout < 0) || (newUserTimeout > timeMax());
_neverExpires = newUserTimeout > timeMax();
}

// Resets, will trigger after the timeout previously set.
Expand Down Expand Up @@ -219,11 +219,27 @@ class timeoutTemplate
_neverExpires = true;
}

void stop()
{
resetToNeverExpires();
}

timeType getTimeout() const
{
return TimePolicyT::toUserUnit(_timeout);
}

IRAM_ATTR // fast
timeType remaining() const
{
if (_neverExpires)
return timeMax();
timeType current = TimePolicyT::time();
if (checkExpired(current))
return TimePolicyT::toUserUnit(0);
return TimePolicyT::toUserUnit(_timeout - (current - _start));
}

static constexpr timeType timeMax()
{
return TimePolicyT::timeMax;
Expand All @@ -235,7 +251,7 @@ class timeoutTemplate
bool checkExpired(const timeType internalUnit) const
{
// canWait() is not checked here
// returns "can expire" and "time expired"
// returns "can expire" and "time has expired"
return (!_neverExpires) && ((internalUnit - _start) >= _timeout);
}

Expand All @@ -250,7 +266,7 @@ class timeoutTemplate
timeType current = TimePolicyT::time();
if(checkExpired(current))
{
unsigned long n = (current - _start) / _timeout; //how many _timeouts periods have elapsed, will usually be 1 (current - _start >= _timeout)
timeType n = (current - _start) / _timeout; //how many _timeouts periods have elapsed, will usually be 1 (current - _start >= _timeout)
_start += n * _timeout;
return true;
}
Expand Down
92 changes: 57 additions & 35 deletions cores/esp8266/Schedule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
*/

#include <assert.h>
#include <numeric>

#include "Schedule.h"
#include "PolledTimeout.h"
#include "interrupts.h"
#include "coredecls.h"
#include <atomic>

typedef std::function<void(void)> mSchedFuncT;
struct scheduled_fn_t
Expand All @@ -35,7 +34,6 @@ static scheduled_fn_t* sFirst = nullptr;
static scheduled_fn_t* sLast = nullptr;
static scheduled_fn_t* sUnused = nullptr;
static int sCount = 0;
static uint32_t recurrent_max_grain_mS = 0;

typedef std::function<bool(void)> mRecFuncT;
struct recurrent_fn_t
Expand All @@ -49,6 +47,20 @@ struct recurrent_fn_t

static recurrent_fn_t* rFirst = nullptr;
static recurrent_fn_t* rLast = nullptr;
// The target time for scheduling the next timed recurrent function
static std::atomic<decltype(micros())> rTarget;

// As 32 bit unsigned integer, micros() rolls over every 71.6 minutes.
// For unambiguous earlier/later order between two timestamps,
// despite roll over, there is a limit on the maximum duration
// that can be requested, if full expiration must be observable:
// later - earlier >= 0 for both later >= earlier or (rolled over) later <= earlier
// Also, expiration should remain observable for a useful duration of time:
// now - (start + period) >= 0 for now - start >= 0 despite (start + period) >= now
// A well-balanced solution, not breaking on two's compliment signed arithmetic,
// is limiting durations to the maximum signed value of the same word size
// as the original unsigned word.
constexpr decltype(micros()) HALF_MAX_MICROS = ~static_cast<decltype(micros())>(0) >> 1;

// Returns a pointer to an unused sched_fn_t,
// or if none are available allocates a new one,
Expand Down Expand Up @@ -106,7 +118,7 @@ bool schedule_function(const std::function<void(void)>& fn)

IRAM_ATTR // (not only) called from ISR
bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
uint32_t repeat_us, const std::function<bool(void)>& alarm)
decltype(micros()) repeat_us, const std::function<bool(void)>& alarm)
{
assert(repeat_us < decltype(recurrent_fn_t::callNow)::neverExpires); //~26800000us (26.8s)

Expand All @@ -122,6 +134,19 @@ bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,

esp8266::InterruptLock lockAllInterruptsInThisScope;

const auto now = micros();
const auto itemRemaining = item->callNow.remaining();
for (auto _rTarget = rTarget.load(); ;)
{
const auto remaining = _rTarget - now;
if (!rFirst || (remaining <= HALF_MAX_MICROS && remaining > itemRemaining))
{
// if (!rTarget.compare_exchange_weak(_rTarget, now + itemRemaining)) continue;
rTarget = now + itemRemaining; // interrupt lock is active, no ABA issue
}
break;
}

d-a-v marked this conversation as resolved.
Show resolved Hide resolved
if (rLast)
{
rLast->mNext = item;
Expand All @@ -132,37 +157,20 @@ bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
}
rLast = item;

// grain needs to be recomputed
recurrent_max_grain_mS = 0;

return true;
}

uint32_t compute_scheduled_recurrent_grain ()
decltype(micros()) get_scheduled_recurrent_delay_us()
{
if (recurrent_max_grain_mS == 0)
{
if (rFirst)
{
uint32_t recurrent_max_grain_uS = rFirst->callNow.getTimeout();
for (auto it = rFirst->mNext; it; it = it->mNext)
recurrent_max_grain_uS = std::gcd(recurrent_max_grain_uS, it->callNow.getTimeout());
if (recurrent_max_grain_uS)
// round to the upper millis
recurrent_max_grain_mS = recurrent_max_grain_uS <= 1000? 1: (recurrent_max_grain_uS + 999) / 1000;
}

#ifdef DEBUG_ESP_CORE
static uint32_t last_grain = 0;
if (recurrent_max_grain_mS != last_grain)
{
::printf(":rsf %u->%u\n", last_grain, recurrent_max_grain_mS);
last_grain = recurrent_max_grain_mS;
}
#endif
}
if (!rFirst) return HALF_MAX_MICROS;
const auto now = micros();
const auto remaining = rTarget.load() - now;
return (remaining <= HALF_MAX_MICROS) ? remaining : 0;
}

return recurrent_max_grain_mS;
decltype(micros()) get_scheduled_delay_us()
{
return sFirst ? 0 : HALF_MAX_MICROS;
}

void run_scheduled_functions()
Expand Down Expand Up @@ -225,11 +233,14 @@ void run_scheduled_recurrent_functions()
fence = true;
}

decltype(rLast) stop;
recurrent_fn_t* prev = nullptr;
bool done;

rTarget.store(micros() + HALF_MAX_MICROS);
// prevent scheduling of new functions during this run
auto stop = rLast;
stop = rLast;

bool done;
do
{
done = current == stop;
Expand Down Expand Up @@ -258,12 +269,23 @@ void run_scheduled_recurrent_functions()
}

delete(to_ditch);

// grain needs to be recomputed
recurrent_max_grain_mS = 0;
}
else
{
const auto now = micros();
const auto currentRemaining = current->callNow.remaining();
for (auto _rTarget = rTarget.load(); ;)
{
const auto remaining = _rTarget - now;
if (remaining <= HALF_MAX_MICROS && remaining > currentRemaining)
{
// if (!rTarget.compare_exchange_weak(_rTarget, now + currentRemaining)) continue;
esp8266::InterruptLock lockAllInterruptsInThisScope;
if (rTarget != _rTarget) { _rTarget = rTarget; continue; }
rTarget = now + currentRemaining;
}
break;
}
prev = current;
current = current->mNext;
}
Expand Down
15 changes: 12 additions & 3 deletions cores/esp8266/Schedule.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include <functional>
#include <stdint.h>

#include "coredecls.h"

#define SCHEDULED_FN_MAX_COUNT 32

// The purpose of scheduled functions is to trigger, from SYS stack (like in
Expand All @@ -39,10 +41,10 @@
// scheduled function happen more often: every yield() (vs every loop()),
// and time resolution is microsecond (vs millisecond). Details are below.

// compute_scheduled_recurrent_grain() is used by delay() to give a chance to
// get_scheduled_recurrent_delay_us() is used by delay() to give a chance to
// all recurrent functions to run per their timing requirement.

uint32_t compute_scheduled_recurrent_grain ();
decltype(micros()) get_scheduled_recurrent_delay_us();

// scheduled functions called once:
//
Expand All @@ -60,6 +62,13 @@ uint32_t compute_scheduled_recurrent_grain ();
// * Run the lambda only once next time.
// * A scheduled function can schedule a function.

// get_scheduled_delay_us() is named for symmetry to get_scheduled_recurrent_delay_us,
// despite the lack of specific delay times. Therefore it can return only one of two
// values, viz. 0 in case of any pending scheduled functions, or a large delay time if
// there is no function in the queue.

decltype(micros()) get_scheduled_delay_us();

bool schedule_function (const std::function<void(void)>& fn);

// Run all scheduled functions.
Expand All @@ -86,7 +95,7 @@ void run_scheduled_functions();
// any remaining delay from repeat_us is disregarded, and fn is executed.

bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
uint32_t repeat_us, const std::function<bool(void)>& alarm = nullptr);
decltype(micros()) repeat_us, const std::function<bool(void)>& alarm = nullptr);

// Test recurrence and run recurrent scheduled functions.
// (internally called at every `yield()` and `loop()`)
Expand Down
12 changes: 4 additions & 8 deletions cores/esp8266/core_esp8266_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@

//This may be used to change user task stack size:
//#define CONT_STACKSIZE 4096

#include <numeric>

#include <Arduino.h>
#include "Schedule.h"
extern "C" {
Expand Down Expand Up @@ -176,13 +173,12 @@ bool esp_try_delay(const uint32_t start_ms, const uint32_t timeout_ms, const uin
return true; // expired
}

// compute greatest chunked delay with respect to scheduled recurrent functions
uint32_t grain_ms = std::gcd(intvl_ms, compute_scheduled_recurrent_grain());
// compute greatest delay interval with respect to scheduled recurrent functions
const uint32_t scheduled_recurrent_delay_ms = get_scheduled_recurrent_delay_us() / 1000UL;
const uint32_t max_delay_ms = std::min(intvl_ms, scheduled_recurrent_delay_ms);

// recurrent scheduled functions will be called from esp_delay()->esp_suspend()
esp_delay(grain_ms > 0 ?
std::min((timeout_ms - expired), grain_ms):
(timeout_ms - expired));
esp_delay(std::min((timeout_ms - expired), max_delay_ms));

return false; // expiration must be checked again
}
Expand Down
Loading