Lumiera  0.pre.03
»edit your freedom«
block-flow.hpp
Go to the documentation of this file.
1 /*
2  BLOCK-FLOW.hpp - specialised custom allocator to manage scheduler data
3 
4  Copyright (C) Lumiera.org
5  2023, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 */
22 
23 
85 #ifndef SRC_VAULT_GEAR_BLOCK_FLOW_H_
86 #define SRC_VAULT_GEAR_BLOCK_FLOW_H_
87 
88 
89 #include "vault/common.hpp"
90 #include "vault/gear/activity.hpp"
92 #include "lib/time/timevalue.hpp"
93 #include "lib/iter-explorer.hpp"
94 #include "lib/format-util.hpp"
95 #include "lib/nocopy.hpp"
96 #include "lib/util.hpp"
97 
98 #include <utility>
99 
100 
101 namespace vault{
102 namespace gear {
103 
104  using util::isnil;
105  using lib::time::Time;
106  using lib::time::FSecs;
107  using lib::time::TimeVar;
108  using lib::time::Duration;
109  using lib::time::FrameRate;
110 
111  namespace err = lumiera::error;
112 
113 
114  namespace blockFlow {
115 
123  const size_t BLOCK_EXPAND_SAFETY_LIMIT = 3000;
124 
125 
131  {
132  /* === characteristic parameters === */
133  const static size_t EPOCH_SIZ = 100;
134  const Duration DUTY_CYCLE{FSecs(1)};
135  const size_t INITIAL_STREAMS = 2;
136 
137  /* === algorithm tuning settings === */
138  const double TARGET_FILL = 0.90;
139  const double BOOST_FACTOR = 0.85;
140  const double DAMP_THRESHOLD = 0.08;
141 
142  /* === contextual assumptions === */
143  const size_t ACTIVITIES_PER_FRAME = 10;
144  const size_t REFERENCE_FPS = 25;
145  const size_t OVERLOAD_LIMIT = 60;
146  };
147 
152  : DefaultConfig
153  {
154  const static size_t EPOCH_SIZ = 500;
155  const size_t INITIAL_STREAMS = 5;
156  };
157 
162  template<class CONF>
163  struct Strategy
164  {
165  CONF const&
166  config() const
167  { // Meyers Singleton
168  static const CONF configInstance;
169  return configInstance;
170  }
171 
172  size_t
173  framesPerEpoch() const
174  {
175  return config().EPOCH_SIZ / config().ACTIVITIES_PER_FRAME;
176  }
177 
178  size_t
179  initialFrameRate() const
180  {
181  return config().INITIAL_STREAMS * config().REFERENCE_FPS;
182  }
183 
184  Duration
185  initialEpochStep() const
186  {
187  return Duration{TimeValue(framesPerEpoch() * TimeValue::SCALE / initialFrameRate())};
188  }
189 
190  size_t
192  {
193  return util::max(2*_raw(config().DUTY_CYCLE) / _raw(initialEpochStep()), 2u);
194  }
195 
196  size_t
197  averageEpochs() const
198  {
199  return util::max (initialEpochCnt(), 6u);
200  }
201 
202  double
203  boostFactor() const
204  {
205  return config().BOOST_FACTOR;
206  }
207 
208  double
210  {
211  return pow(config().BOOST_FACTOR, 5.0/config().EPOCH_SIZ);
212  }
213 
214  Duration
216  {
217  return Duration{TimeValue(_raw(initialEpochStep()) / config().OVERLOAD_LIMIT)};
218  }
219  };
220 
221 
222 
223 
232  template<class ALO>
233  class Epoch
234  : public ALO::Extent
235  {
236  using RawIter = typename ALO::iterator;
237  using SIZ = typename ALO::Extent::SIZ;
238 
240  Epoch() = delete;
241 
242  public:
250  struct EpochGate
251  : Activity
252  {
261  : Activity{int(0), Time::ANYTIME}
262  {
263  // initialise allocation usage marker: start at last usable slot
264  next = this + (Epoch::SIZ() - 1);
265  ENSURE (next != this);
266  }
267  // default copyable
268 
270  deadline()
271  {
272  return data_.condition.dead;
273  }
274 
275  bool
276  isAlive (Time deadline)
277  {
278  return data_.condition.isHold() // NOTE: expected callback keeps alive
279  or not data_.condition.isDead (deadline);
280  }
281 
282  size_t
283  filledSlots() const
284  {
285  const Activity* firstAllocPoint{this + (Epoch::SIZ()-1)};
286  return firstAllocPoint - next;
287  }
288 
289  bool
290  hasFreeSlot() const
291  { // see C++ § 5.9 : comparison of pointers within same array
292  return next > this;
293  }
294 
295  Activity*
296  claimNextSlot()
297  {
298  REQUIRE (hasFreeSlot());
299  return next--;
300  }
301  };
302 
303 
304  EpochGate& gate() { return static_cast<EpochGate&> ((*this)[0]); }
305  Time deadline() { return Time{gate().deadline()}; }
306 
307  double
308  getFillFactor()
309  {
310  return double(gate().filledSlots()) / (SIZ()-1);
311  }
312 
313 
314  static Epoch&
315  implantInto (RawIter storageSlot)
316  {
317  Epoch& target = static_cast<Epoch&> (*storageSlot);
318  new(&target[0]) EpochGate{};
319  return target;
320  }
321 
322  static Epoch&
323  setup (RawIter storageSlot, Time deadline)
324  {
325  Epoch& newEpoch{implantInto (storageSlot)};
326  newEpoch.gate().deadline() = deadline;
327  return newEpoch;
328  }
329  };
330 
331 
332  }//(End)namespace blockFlow
333 
334  template<class CONF>
336 
337 
338 
339 
340  /******************************************************/
349  template<class CONF = blockFlow::DefaultConfig>
350  class BlockFlow
351  : public blockFlow::Strategy<CONF>
353  {
354  constexpr static size_t EPOCH_SIZ = CONF::EPOCH_SIZ;
355 
356  public:
359  using RawIter = typename Allocator::iterator;
360  using Extent = typename Allocator::Extent;
362 
363  using Strategy::config;
364 
365  private:
366  Allocator alloc_;
367  TimeVar epochStep_;
368 
369 
371  static Epoch&
372  asEpoch (Extent& extent)
373  {
374  return static_cast<Epoch&> (extent);
375  }
376 
382  : public RawIter
383  {
384  Epoch* curr_{nullptr};
385 
386  Epoch*
387  accessEpoch()
388  {
389  return RawIter::checkPoint()? & asEpoch (RawIter::yield())
390  : nullptr;
391  }
392 
393  public:
394  StorageAdaptor() = default;
395  StorageAdaptor(RawIter it)
396  : RawIter{it}
397  , curr_{accessEpoch()}
398  { }
399 
400  bool
401  checkPoint() const
402  {
403  return bool(curr_);
404  }
405 
406  Epoch&
407  yield() const
408  {
409  return *curr_;
410  }
411 
412  void
413  iterNext()
414  {
415  RawIter::validatePos(curr_);
416  RawIter::iterNext();
417  curr_ = accessEpoch();
418  }
419 
420  void
421  expandAlloc (size_t cnt =1)
422  {
423  RawIter::expandAlloc(cnt);
424  curr_ = accessEpoch();
425  }
426  };
427 
428 
429 
430  public:
431  BlockFlow()
432  : alloc_{Strategy::initialEpochCnt()}
433  , epochStep_{Strategy::initialEpochStep()}
434  { }
435 
436  Duration
437  getEpochStep() const
438  {
439  return Duration{epochStep_};
440  }
441 
442  void
443  adjustEpochStep (double factor)
444  {
445  double stretched = _raw(epochStep_) * factor;
446  gavl_time_t microTicks(floor (stretched));
447  epochStep_ = TimeValue{microTicks};
448  }
449 
450 
451 
454 
455 
466  {
467  EpochIter epoch_;
468  BlockFlow* flow_;
469 
470  public:
471  AllocatorHandle(RawIter slot, BlockFlow* parent)
472  : epoch_{slot}
473  , flow_{parent}
474  { }
475 
476  /*************************************************/
479  template<typename...ARGS>
480  Activity&
481  create (ARGS&& ...args)
482  {
483  return *new(claimSlot()) Activity {std::forward<ARGS> (args)...};
484  }
485 
486  Time currDeadline() const { return epoch_->deadline(); }
487  bool hasFreeSlot() const { return epoch_->gate().hasFreeSlot(); }
488 
489 
490  private:
491  void*
492  claimSlot()
493  {
494  while (not (epoch_ and
495  epoch_->gate().hasFreeSlot()))
496  // Epoch overflow...
497  {// shift to following Epoch; possibly allocate
498  if (not epoch_)
499  {
500  auto lastDeadline = flow_->lastEpoch().deadline();
501  epoch_.expandAlloc(); // may throw out-of-memory..
502  ENSURE (epoch_);
503  Epoch::setup (epoch_, lastDeadline + flow_->getEpochStep());
504  }
505  else
506  {
507  flow_->markEpochOverflow();
508  ++epoch_;
509  }
510  }
511  return epoch_->gate().claimNextSlot();
512  }
513  };
514 
515 
516  /* ===== public BlockFlow API ===== */
517 
525  until (Time deadline)
526  {
527  if (isnil (alloc_))
528  {//just create new Epoch one epochStep ahead
529  alloc_.openNew();
530  Epoch::setup (alloc_.begin(), deadline + Time{epochStep_});
531  return AllocatorHandle{alloc_.begin(), this};
532  }
533  else
534  {//find out how the given time relates to existing Epochs
535  if (firstEpoch().deadline() >= deadline)
536  // way into the past ... put it in the first available Epoch
537  return AllocatorHandle{alloc_.begin(), this};
538  else
539  if (lastEpoch().deadline() < deadline)
540  { // a deadline beyond the established Epochs...
541  // create a grid of new epochs up to the requested point
542  TimeVar lastDeadline = lastEpoch().deadline();
543  auto distance = _raw(deadline) - _raw(lastDeadline);
544  EpochIter nextEpoch{alloc_.end()};
545  ENSURE (not nextEpoch); // not valid yet, but we will allocate starting there...
546  auto requiredNew = distance / _raw(epochStep_);
547  ___sanityCheckAlloc(requiredNew);
548  if (distance % _raw(epochStep_) > 0)
549  ++requiredNew; // fractional: requested deadline lies within last epoch
550  nextEpoch.expandAlloc (requiredNew);
551  // nextEpoch now points to the first new Epoch
552  for ( ; 0 < requiredNew; --requiredNew)
553  {
554  REQUIRE (nextEpoch);
555  lastDeadline += epochStep_;
556  Epoch::setup (nextEpoch, lastDeadline);
557  if (deadline <= lastDeadline)
558  {
559  ENSURE (requiredNew == 1);
560  return AllocatorHandle{nextEpoch, this};
561  } // break out and return handle to allocate into the matching Epoch
562  ++nextEpoch;
563  }
564  NOTREACHED ("Logic of counting new Epochs");
565  }
566  else
567  for (EpochIter epochIt{alloc_.begin()}; epochIt; ++epochIt)
568  if (epochIt->deadline() >= deadline)
569  return AllocatorHandle{epochIt, this};
570 
571  NOTREACHED ("Inconsistency in BlockFlow Epoch deadline organisation");
572  }
573  }
574 
582  void
583  discardBefore (Time deadline)
584  {
585  if (isnil (alloc_)
586  or firstEpoch().deadline() > deadline)
587  return;
588 
589  size_t toDiscard{0};
590  for (Epoch& epoch : allEpochs())
591  {
592  if (epoch.gate().isAlive (deadline))
593  break;
594  ++toDiscard;
595  auto currDeadline = epoch.deadline();
596  auto epochDuration = currDeadline - updatePastDeadline(currDeadline);
597  markEpochUnderflow (epochDuration, epoch.getFillFactor());
598  }
599  // ask to discard the enumerated Extents
600  alloc_.dropOld (toDiscard);
601  }
602 
603 
610  void
612  {
613  if (epochStep_ > _cache_timeStep_cutOff)
614  adjustEpochStep (_cache_boostFactorOverflow);
615  }
616  // caching access to the config saves 15-30% per allocation
617  Duration _cache_timeStep_cutOff = Strategy::timeStep_cutOff();
618  double _cache_boostFactorOverflow = Strategy::boostFactorOverflow();
619 
630  void
631  markEpochUnderflow (TimeVar actualLen, double fillFactor)
632  {
633  auto interpolate = [&](auto f, auto v1, auto v2) { return f*v2 + (1-f)*v1; };
634 
635  // use actual fill as signal, set desired fill-level as goal
636  fillFactor /= config().TARGET_FILL;
637  auto THRESH = config().DAMP_THRESHOLD;
638  double adjust =
639  fillFactor > THRESH? fillFactor // limit signal for almost empty Epochs to avoid overshooting
640  : interpolate (1 - fillFactor/THRESH, fillFactor, Strategy::boostFactor());
641 
642  // damped adjustment towards ideal size
643  double contribution = double(_raw(actualLen)) / _raw(epochStep_) / adjust;
644 
645  // Exponential MA: mean ≔ mean · (N-1)/N + newVal/N
646  auto N = Strategy::averageEpochs();
647  double avgFactor = (contribution + N-1) / N; // contribution = newVal / mean => can extract factor
648  adjustEpochStep (avgFactor);
649  }
650 
651 
660  void
662  {
663  FrameRate currFps{Strategy::framesPerEpoch(), Duration{epochStep_}};
664  currFps += additionalFps;
665  TimeVar adaptedSpacing = Strategy::framesPerEpoch() / currFps;
666  epochStep_ = util::max (adaptedSpacing, _cache_timeStep_cutOff);
667  }
668 
669 
670  private:
671  Epoch&
672  firstEpoch()
673  {
674  REQUIRE (not isnil (alloc_));
675  return asEpoch(*alloc_.begin());
676  }
677  Epoch&
678  lastEpoch()
679  {
680  REQUIRE (not isnil (alloc_));
681  return asEpoch(*alloc_.last());
682  }
683 
684  EpochIter
685  allEpochs()
686  {
687  return alloc_.begin();
688  }
689 
697  Time
699  {
700  if (pastDeadline_ == Time::ANYTIME)
701  pastDeadline_ = newDeadline - epochStep_;
702  TimeVar previous = pastDeadline_;
703  pastDeadline_ = newDeadline;
704  return previous;
705  }
706  TimeVar pastDeadline_{Time::ANYTIME};
707 
708  void
709  ___sanityCheckAlloc (size_t newBlockCnt)
710  {
711  if (newBlockCnt > blockFlow::BLOCK_EXPAND_SAFETY_LIMIT)
712  throw err::Fatal{"Deadline expansion causes allocation of "
713  +util::showSize(newBlockCnt) +"blocks > "
714  +util::showSize(blockFlow::BLOCK_EXPAND_SAFETY_LIMIT)
715  , err::LUMIERA_ERROR_CAPACITY};
716  }
717 
718 
720  friend class FlowDiagnostic<CONF>;
721  };
722 
723 
724 
725 
726 
727 
728 
729 
730  /* ===== Test / Diagnostic ===== */
731 
732  template<class CONF>
733  class FlowDiagnostic
734  {
735  using Epoch = typename BlockFlow<CONF>::Epoch;
736 
737  BlockFlow<CONF>& flow_;
738 
739  public:
741  : flow_{theFlow}
742  { }
743 
744  Time first() { return flow_.firstEpoch().deadline();}
745  Time last() { return flow_.lastEpoch().deadline(); }
746  size_t cntEpochs() { return watch(flow_.alloc_).active(); }
747  size_t poolSize() { return watch(flow_.alloc_).size(); }
748 
750  TimeValue
751  find (Activity& someActivity)
752  {
753  for (Epoch& epoch : flow_.allEpochs())
754  for (Activity& act : epoch)
755  if (util::isSameObject (act, someActivity))
756  return epoch.deadline();
757  return Time::NEVER;
758  }
759 
761  std::string
763  {
764  if (isnil (flow_.alloc_)) return "";
765  auto deadlines = lib::explore (flow_.allEpochs())
766  .transform([](Epoch& a){ return TimeValue{a.deadline()}; });
767  return util::join(deadlines, "|");
768  }
769 
771  size_t
773  {
774  size_t cnt{0};
775  for (auto& epoch : flow_.allEpochs())
776  cnt += epoch.gate().filledSlots();
777  return cnt;
778  }
779  };
780 
781  template<class CONF>
782  inline FlowDiagnostic<CONF>
783  watch (BlockFlow<CONF>& theFlow)
784  {
785  return FlowDiagnostic{theFlow};
786  }
787 
788 
789 
790 }} // namespace vault::gear
791 #endif /*SRC_VAULT_GEAR_BLOCK_FLOW_H_*/
static const Time ANYTIME
border condition marker value. ANYTIME <= any time value
Definition: timevalue.hpp:322
Allocation scheme for the Scheduler, based on Epoch(s).
Definition: block-flow.hpp:350
a mutable time value, behaving like a plain number, allowing copy and re-accessing ...
Definition: timevalue.hpp:241
Record to describe an Activity, to happen within the Scheduler&#39;s control flow.
Definition: activity.hpp:235
iterator begin()
iterate over all the currently active Extents
< special definitions for the Scheduler activity language
Definition: activity.hpp:124
specifically rigged GATE Activity, used for managing Epoch metadata
Definition: block-flow.hpp:250
const Duration DUTY_CYCLE
typical relaxation time or average pre-roll to deadline
Definition: block-flow.hpp:134
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
void openNew(size_t cnt=1)
claim next cnt extents, possibly allocate.
Policy template to mix into the BlockFlow allocator, providing the parametrisation for self-regulatio...
Definition: block-flow.hpp:163
Framerate specified as frames per second.
Definition: timevalue.hpp:664
const size_t OVERLOAD_LIMIT
load factor over normal use where to assume saturation and limit throughput
Definition: block-flow.hpp:145
const size_t BLOCK_EXPAND_SAFETY_LIMIT
< Parametrisation of Scheduler memory management scheme
Definition: block-flow.hpp:123
Any copy and copy construction prohibited.
Definition: nocopy.hpp:46
iterator last()
positioned to the last / latest storage extent opened
Allocation Extent holding scheduler Activities to be performed altogether before a common deadline...
Definition: block-flow.hpp:233
static const gavl_time_t SCALE
Number of micro ticks (µs) per second as basic time scale.
Definition: timevalue.hpp:176
Local handle to allow allocating a collection of Activities, all sharing a common deadline...
Definition: block-flow.hpp:465
void markEpochOverflow()
Notify and adjust Epoch capacity as consequence of exhausting an Epoch.
Definition: block-flow.hpp:611
Time updatePastDeadline(TimeVar newDeadline)
Definition: block-flow.hpp:698
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
const double BOOST_FACTOR
adjust capacity by this factor on Epoch overflow/underflow events
Definition: block-flow.hpp:139
Lightweight yet safe parametrisation of memory management.
Definition: block-flow.hpp:130
TimeValue find(Activity &someActivity)
find out in which Epoch the given Activity was placed
Definition: block-flow.hpp:751
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:199
static const size_t EPOCH_SIZ
Number of storage slots to fit into one »Epoch«
Definition: block-flow.hpp:133
const double DAMP_THRESHOLD
do not account for (almost) empty Epochs to avoid overshooting regulation
Definition: block-flow.hpp:140
Mix-Ins to allow or prohibit various degrees of copying and cloning.
const size_t REFERENCE_FPS
frame rate to use as reference point to relate DUTY_CYCLE and default counts
Definition: block-flow.hpp:144
void dropOld(size_t cnt)
discard oldest cnt extents
void announceAdditionalFlow(FrameRate additionalFps)
provide a hint to the self-regulating allocation scheme.
Definition: block-flow.hpp:661
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
Parametrisation tuned for Render Engine performance.
Definition: block-flow.hpp:151
Activity & create(ARGS &&...args)
Main API operation: allocate a new Activity record.
Definition: block-flow.hpp:481
const double TARGET_FILL
aim at using this fraction of Epoch space on average (slightly below 100%)
Definition: block-flow.hpp:138
Duration timeStep_cutOff() const
< prevent stalling Epoch progression when reaching saturation
Definition: block-flow.hpp:215
boost::rational< int64_t > FSecs
rational representation of fractional seconds
Definition: timevalue.hpp:229
static Epoch & asEpoch(Extent &extent)
Definition: block-flow.hpp:372
void markEpochUnderflow(TimeVar actualLen, double fillFactor)
On clean-up of past Epochs, the actual fill factor is checked to guess an Epoch duration to make opti...
Definition: block-flow.hpp:631
Memory management scheme for cyclically used memory extents.
size_t initialEpochCnt() const
< reserve allocation headroom for two duty cycles
Definition: block-flow.hpp:191
Basic set of definitions and includes commonly used together (Vault).
const size_t ACTIVITIES_PER_FRAME
how many Activity records are typically used to implement a single frame
Definition: block-flow.hpp:143
static const Time NEVER
border condition marker value. NEVER >= any time value
Definition: timevalue.hpp:323
Adapt the access to the raw storage to present the Extents as Epoch; also caches the address resoluti...
Definition: block-flow.hpp:381
auto setup(FUN &&workFun)
Helper: setup a Worker-Pool configuration for the test.
Collection of small helpers and convenience shortcuts for diagnostics & formatting.
Decorator-Adapter to make a »state core« iterable as Lumiera Forward Iterator.
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:477
void discardBefore(Time deadline)
Clean-up all storage related to activities before the given deadline.
Definition: block-flow.hpp:583
const size_t INITIAL_STREAMS
Number of streams with REFERENCE_FPS to expect for normal use.
Definition: block-flow.hpp:135
Building tree expanding and backtracking evaluations within hierarchical scopes.
double boostFactorOverflow() const
< reduced logarithmically, since overflow is detected on individual allocations
Definition: block-flow.hpp:209
a family of time value like entities and their relationships.
basic constant internal time value.
Definition: timevalue.hpp:142
size_t cntElm()
count all currently active allocated elements
Definition: block-flow.hpp:772
Vault-Layer implementation namespace root.
std::string allEpochs()
render deadlines of all currently active Epochs
Definition: block-flow.hpp:762
AllocatorHandle until(Time deadline)
initiate allocations for activities to happen until some deadline
Definition: block-flow.hpp:525
Descriptor for a piece of operational logic performed by the scheduler.