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)
5  2023, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 */
13 
14 
76 #ifndef SRC_VAULT_GEAR_BLOCK_FLOW_H_
77 #define SRC_VAULT_GEAR_BLOCK_FLOW_H_
78 
79 
80 #include "vault/common.hpp"
81 #include "vault/gear/activity.hpp"
83 #include "lib/time/timevalue.hpp"
84 #include "lib/iter-explorer.hpp"
85 #include "lib/format-util.hpp"
86 #include "lib/nocopy.hpp"
87 #include "lib/util.hpp"
88 
89 #include <utility>
90 
91 
92 namespace vault{
93 namespace gear {
94 
95  using util::isnil;
96  using lib::time::Time;
97  using lib::time::FSecs;
98  using lib::time::TimeVar;
99  using lib::time::Duration;
100  using lib::time::FrameRate;
101 
102  namespace err = lumiera::error;
103 
104 
105  namespace blockFlow {
106 
114  const size_t BLOCK_EXPAND_SAFETY_LIMIT = 3000;
115 
116 
122  {
123  /* === characteristic parameters === */
124  const static size_t EPOCH_SIZ = 100;
125  const Duration DUTY_CYCLE{FSecs(1)};
126  const size_t INITIAL_STREAMS = 2;
127 
128  /* === algorithm tuning settings === */
129  const double TARGET_FILL = 0.90;
130  const double BOOST_FACTOR = 0.85;
131  const double DAMP_THRESHOLD = 0.08;
132 
133  /* === contextual assumptions === */
134  const size_t ACTIVITIES_PER_FRAME = 10;
135  const size_t REFERENCE_FPS = 25;
136  const size_t OVERLOAD_LIMIT = 60;
137  };
138 
143  : DefaultConfig
144  {
145  const static size_t EPOCH_SIZ = 500;
146  const size_t INITIAL_STREAMS = 5;
147  };
148 
153  template<class CONF>
154  struct Strategy
155  {
156  CONF const&
157  config() const
158  { // Meyers Singleton
159  static const CONF configInstance;
160  return configInstance;
161  }
162 
163  size_t
164  framesPerEpoch() const
165  {
166  return config().EPOCH_SIZ / config().ACTIVITIES_PER_FRAME;
167  }
168 
169  size_t
170  initialFrameRate() const
171  {
172  return config().INITIAL_STREAMS * config().REFERENCE_FPS;
173  }
174 
175  Duration
176  initialEpochStep() const
177  {
178  return Duration{TimeValue(framesPerEpoch() * TimeValue::SCALE / initialFrameRate())};
179  }
180 
181  size_t
183  {
184  return util::max(2*_raw(config().DUTY_CYCLE) / _raw(initialEpochStep()), 2u);
185  }
186 
187  size_t
188  averageEpochs() const
189  {
190  return util::max (initialEpochCnt(), 6u);
191  }
192 
193  double
194  boostFactor() const
195  {
196  return config().BOOST_FACTOR;
197  }
198 
199  double
201  {
202  return pow(config().BOOST_FACTOR, 5.0/config().EPOCH_SIZ);
203  }
204 
205  Duration
207  {
208  return Duration{TimeValue(_raw(initialEpochStep()) / config().OVERLOAD_LIMIT)};
209  }
210  };
211 
212 
213 
214 
223  template<class ALO>
224  class Epoch
225  : public ALO::Extent
226  {
227  using RawIter = typename ALO::iterator;
228  using SIZ = typename ALO::Extent::SIZ;
229 
231  Epoch() = delete;
232 
233  public:
241  struct EpochGate
242  : Activity
243  {
252  : Activity{int(0), Time::ANYTIME}
253  {
254  // initialise allocation usage marker: start at last usable slot
255  next = this + (Epoch::SIZ() - 1);
256  ENSURE (next != this);
257  }
258  // default copyable
259 
261  deadline()
262  {
263  return data_.condition.dead;
264  }
265 
266  bool
267  isAlive (Time deadline)
268  {
269  return data_.condition.isHold() // NOTE: expected callback keeps alive
270  or not data_.condition.isDead (deadline);
271  }
272 
273  size_t
274  filledSlots() const
275  {
276  const Activity* firstAllocPoint{this + (Epoch::SIZ()-1)};
277  return firstAllocPoint - next;
278  }
279 
280  bool
281  hasFreeSlot() const
282  { // see C++ § 5.9 : comparison of pointers within same array
283  return next > this;
284  }
285 
286  Activity*
287  claimNextSlot()
288  {
289  REQUIRE (hasFreeSlot());
290  return next--;
291  }
292  };
293 
294 
295  EpochGate& gate() { return static_cast<EpochGate&> ((*this)[0]); }
296  Time deadline() { return Time{gate().deadline()}; }
297 
298  double
299  getFillFactor()
300  {
301  return double(gate().filledSlots()) / (SIZ()-1);
302  }
303 
304 
305  static Epoch&
306  implantInto (RawIter storageSlot)
307  {
308  Epoch& target = static_cast<Epoch&> (*storageSlot);
309  new(&target[0]) EpochGate{};
310  return target;
311  }
312 
313  static Epoch&
314  setup (RawIter storageSlot, Time deadline)
315  {
316  Epoch& newEpoch{implantInto (storageSlot)};
317  newEpoch.gate().deadline() = deadline;
318  return newEpoch;
319  }
320  };
321 
322 
323  }//(End)namespace blockFlow
324 
325  template<class CONF>
327 
328 
329 
330 
331  /******************************************************/
340  template<class CONF = blockFlow::DefaultConfig>
341  class BlockFlow
342  : public blockFlow::Strategy<CONF>
344  {
345  constexpr static size_t EPOCH_SIZ = CONF::EPOCH_SIZ;
346 
347  public:
350  using RawIter = typename Allocator::iterator;
351  using Extent = typename Allocator::Extent;
353 
354  using Strategy::config;
355 
356  private:
357  Allocator alloc_;
358  TimeVar epochStep_;
359 
360 
362  static Epoch&
363  asEpoch (Extent& extent)
364  {
365  return static_cast<Epoch&> (extent);
366  }
367 
373  : public RawIter
374  {
375  Epoch* curr_{nullptr};
376 
377  Epoch*
378  accessEpoch()
379  {
380  return RawIter::checkPoint()? & asEpoch (RawIter::yield())
381  : nullptr;
382  }
383 
384  public:
385  StorageAdaptor() = default;
386  StorageAdaptor(RawIter it)
387  : RawIter{it}
388  , curr_{accessEpoch()}
389  { }
390 
391  bool
392  checkPoint() const
393  {
394  return bool(curr_);
395  }
396 
397  Epoch&
398  yield() const
399  {
400  return *curr_;
401  }
402 
403  void
404  iterNext()
405  {
406  RawIter::validatePos(curr_);
407  RawIter::iterNext();
408  curr_ = accessEpoch();
409  }
410 
411  void
412  expandAlloc (size_t cnt =1)
413  {
414  RawIter::expandAlloc(cnt);
415  curr_ = accessEpoch();
416  }
417  };
418 
419 
420 
421  public:
422  BlockFlow()
423  : alloc_{Strategy::initialEpochCnt()}
424  , epochStep_{Strategy::initialEpochStep()}
425  { }
426 
427  Duration
428  getEpochStep() const
429  {
430  return Duration{epochStep_};
431  }
432 
433  void
434  adjustEpochStep (double factor)
435  {
436  double stretched = _raw(epochStep_) * factor;
437  gavl_time_t microTicks(floor (stretched));
438  epochStep_ = TimeValue{microTicks};
439  }
440 
441 
442 
445 
446 
457  {
458  EpochIter epoch_;
459  BlockFlow* flow_;
460 
461  public:
462  AllocatorHandle(RawIter slot, BlockFlow* parent)
463  : epoch_{slot}
464  , flow_{parent}
465  { }
466 
467  /*************************************************/
470  template<typename...ARGS>
471  Activity&
472  create (ARGS&& ...args)
473  {
474  return *new(claimSlot()) Activity {std::forward<ARGS> (args)...};
475  }
476 
477  Time currDeadline() const { return epoch_->deadline(); }
478  bool hasFreeSlot() const { return epoch_->gate().hasFreeSlot(); }
479 
480 
481  private:
482  void*
483  claimSlot()
484  {
485  while (not (epoch_ and
486  epoch_->gate().hasFreeSlot()))
487  // Epoch overflow...
488  {// shift to following Epoch; possibly allocate
489  if (not epoch_)
490  {
491  auto lastDeadline = flow_->lastEpoch().deadline();
492  epoch_.expandAlloc(); // may throw out-of-memory..
493  ENSURE (epoch_);
494  Epoch::setup (epoch_, lastDeadline + flow_->getEpochStep());
495  }
496  else
497  {
498  flow_->markEpochOverflow();
499  ++epoch_;
500  }
501  }
502  return epoch_->gate().claimNextSlot();
503  }
504  };
505 
506 
507  /* ===== public BlockFlow API ===== */
508 
516  until (Time deadline)
517  {
518  if (isnil (alloc_))
519  {//just create new Epoch one epochStep ahead
520  alloc_.openNew();
521  Epoch::setup (alloc_.begin(), deadline + Time{epochStep_});
522  return AllocatorHandle{alloc_.begin(), this};
523  }
524  else
525  {//find out how the given time relates to existing Epochs
526  if (firstEpoch().deadline() >= deadline)
527  // way into the past ... put it in the first available Epoch
528  return AllocatorHandle{alloc_.begin(), this};
529  else
530  if (lastEpoch().deadline() < deadline)
531  { // a deadline beyond the established Epochs...
532  // create a grid of new epochs up to the requested point
533  TimeVar lastDeadline = lastEpoch().deadline();
534  auto distance = _raw(deadline) - _raw(lastDeadline);
535  EpochIter nextEpoch{alloc_.end()};
536  ENSURE (not nextEpoch); // not valid yet, but we will allocate starting there...
537  auto requiredNew = distance / _raw(epochStep_);
538  ___sanityCheckAlloc(requiredNew);
539  if (distance % _raw(epochStep_) > 0)
540  ++requiredNew; // fractional: requested deadline lies within last epoch
541  nextEpoch.expandAlloc (requiredNew);
542  // nextEpoch now points to the first new Epoch
543  for ( ; 0 < requiredNew; --requiredNew)
544  {
545  REQUIRE (nextEpoch);
546  lastDeadline += epochStep_;
547  Epoch::setup (nextEpoch, lastDeadline);
548  if (deadline <= lastDeadline)
549  {
550  ENSURE (requiredNew == 1);
551  return AllocatorHandle{nextEpoch, this};
552  } // break out and return handle to allocate into the matching Epoch
553  ++nextEpoch;
554  }
555  NOTREACHED ("Logic of counting new Epochs");
556  }
557  else
558  for (EpochIter epochIt{alloc_.begin()}; epochIt; ++epochIt)
559  if (epochIt->deadline() >= deadline)
560  return AllocatorHandle{epochIt, this};
561 
562  NOTREACHED ("Inconsistency in BlockFlow Epoch deadline organisation");
563  }
564  }
565 
573  void
574  discardBefore (Time deadline)
575  {
576  if (isnil (alloc_)
577  or firstEpoch().deadline() > deadline)
578  return;
579 
580  size_t toDiscard{0};
581  for (Epoch& epoch : allEpochs())
582  {
583  if (epoch.gate().isAlive (deadline))
584  break;
585  ++toDiscard;
586  auto currDeadline = epoch.deadline();
587  auto epochDuration = currDeadline - updatePastDeadline(currDeadline);
588  markEpochUnderflow (epochDuration, epoch.getFillFactor());
589  }
590  // ask to discard the enumerated Extents
591  alloc_.dropOld (toDiscard);
592  }
593 
594 
601  void
603  {
604  if (epochStep_ > _cache_timeStep_cutOff)
605  adjustEpochStep (_cache_boostFactorOverflow);
606  }
607  // caching access to the config saves 15-30% per allocation
608  Duration _cache_timeStep_cutOff = Strategy::timeStep_cutOff();
609  double _cache_boostFactorOverflow = Strategy::boostFactorOverflow();
610 
621  void
622  markEpochUnderflow (TimeVar actualLen, double fillFactor)
623  {
624  auto interpolate = [&](auto f, auto v1, auto v2) { return f*v2 + (1-f)*v1; };
625 
626  // use actual fill as signal, set desired fill-level as goal
627  fillFactor /= config().TARGET_FILL;
628  auto THRESH = config().DAMP_THRESHOLD;
629  double adjust =
630  fillFactor > THRESH? fillFactor // limit signal for almost empty Epochs to avoid overshooting
631  : interpolate (1 - fillFactor/THRESH, fillFactor, Strategy::boostFactor());
632 
633  // damped adjustment towards ideal size
634  double contribution = double(_raw(actualLen)) / _raw(epochStep_) / adjust;
635 
636  // Exponential MA: mean ≔ mean · (N-1)/N + newVal/N
637  auto N = Strategy::averageEpochs();
638  double avgFactor = (contribution + N-1) / N; // contribution = newVal / mean => can extract factor
639  adjustEpochStep (avgFactor);
640  }
641 
642 
651  void
653  {
654  FrameRate currFps{Strategy::framesPerEpoch(), Duration{epochStep_}};
655  currFps += additionalFps;
656  TimeVar adaptedSpacing = Strategy::framesPerEpoch() / currFps;
657  epochStep_ = util::max (adaptedSpacing, _cache_timeStep_cutOff);
658  }
659 
660 
661  private:
662  Epoch&
663  firstEpoch()
664  {
665  REQUIRE (not isnil (alloc_));
666  return asEpoch(*alloc_.begin());
667  }
668  Epoch&
669  lastEpoch()
670  {
671  REQUIRE (not isnil (alloc_));
672  return asEpoch(*alloc_.last());
673  }
674 
675  EpochIter
676  allEpochs()
677  {
678  return alloc_.begin();
679  }
680 
688  Time
690  {
691  if (pastDeadline_ == Time::ANYTIME)
692  pastDeadline_ = newDeadline - epochStep_;
693  TimeVar previous = pastDeadline_;
694  pastDeadline_ = newDeadline;
695  return previous;
696  }
697  TimeVar pastDeadline_{Time::ANYTIME};
698 
699  void
700  ___sanityCheckAlloc (size_t newBlockCnt)
701  {
702  if (newBlockCnt > blockFlow::BLOCK_EXPAND_SAFETY_LIMIT)
703  throw err::Fatal{"Deadline expansion causes allocation of "
704  +util::showSize(newBlockCnt) +"blocks > "
705  +util::showSize(blockFlow::BLOCK_EXPAND_SAFETY_LIMIT)
706  , err::LUMIERA_ERROR_CAPACITY};
707  }
708 
709 
711  friend class FlowDiagnostic<CONF>;
712  };
713 
714 
715 
716 
717 
718 
719 
720 
721  /* ===== Test / Diagnostic ===== */
722 
723  template<class CONF>
724  class FlowDiagnostic
725  {
726  using Epoch = typename BlockFlow<CONF>::Epoch;
727 
728  BlockFlow<CONF>& flow_;
729 
730  public:
732  : flow_{theFlow}
733  { }
734 
735  Time first() { return flow_.firstEpoch().deadline();}
736  Time last() { return flow_.lastEpoch().deadline(); }
737  size_t cntEpochs() { return watch(flow_.alloc_).active(); }
738  size_t poolSize() { return watch(flow_.alloc_).size(); }
739 
741  TimeValue
742  find (Activity& someActivity)
743  {
744  for (Epoch& epoch : flow_.allEpochs())
745  for (Activity& act : epoch)
746  if (util::isSameObject (act, someActivity))
747  return epoch.deadline();
748  return Time::NEVER;
749  }
750 
752  std::string
754  {
755  if (isnil (flow_.alloc_)) return "";
756  auto deadlines = lib::explore (flow_.allEpochs())
757  .transform([](Epoch& a){ return TimeValue{a.deadline()}; });
758  return util::join(deadlines, "|");
759  }
760 
762  size_t
764  {
765  size_t cnt{0};
766  for (auto& epoch : flow_.allEpochs())
767  cnt += epoch.gate().filledSlots();
768  return cnt;
769  }
770  };
771 
772  template<class CONF>
773  inline FlowDiagnostic<CONF>
774  watch (BlockFlow<CONF>& theFlow)
775  {
776  return FlowDiagnostic{theFlow};
777  }
778 
779 
780 
781 }} // namespace vault::gear
782 #endif /*SRC_VAULT_GEAR_BLOCK_FLOW_H_*/
static const Time ANYTIME
border condition marker value. ANYTIME <= any time value
Definition: timevalue.hpp:313
Allocation scheme for the Scheduler, based on Epoch(s).
Definition: block-flow.hpp:341
a mutable time value, behaving like a plain number, allowing copy and re-accessing ...
Definition: timevalue.hpp:232
Record to describe an Activity, to happen within the Scheduler&#39;s control flow.
Definition: activity.hpp:226
iterator begin()
iterate over all the currently active Extents
< special definitions for the Scheduler activity language
Definition: activity.hpp:115
specifically rigged GATE Activity, used for managing Epoch metadata
Definition: block-flow.hpp:241
const Duration DUTY_CYCLE
typical relaxation time or average pre-roll to deadline
Definition: block-flow.hpp:125
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:154
Framerate specified as frames per second.
Definition: timevalue.hpp:655
const size_t OVERLOAD_LIMIT
load factor over normal use where to assume saturation and limit throughput
Definition: block-flow.hpp:136
const size_t BLOCK_EXPAND_SAFETY_LIMIT
< Parametrisation of Scheduler memory management scheme
Definition: block-flow.hpp:114
Any copy and copy construction prohibited.
Definition: nocopy.hpp:37
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:224
static const gavl_time_t SCALE
Number of micro ticks (µs) per second as basic time scale.
Definition: timevalue.hpp:167
Local handle to allow allocating a collection of Activities, all sharing a common deadline...
Definition: block-flow.hpp:456
void markEpochOverflow()
Notify and adjust Epoch capacity as consequence of exhausting an Epoch.
Definition: block-flow.hpp:602
Time updatePastDeadline(TimeVar newDeadline)
Definition: block-flow.hpp:689
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:299
const double BOOST_FACTOR
adjust capacity by this factor on Epoch overflow/underflow events
Definition: block-flow.hpp:130
Lightweight yet safe parametrisation of memory management.
Definition: block-flow.hpp:121
TimeValue find(Activity &someActivity)
find out in which Epoch the given Activity was placed
Definition: block-flow.hpp:742
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:190
static const size_t EPOCH_SIZ
Number of storage slots to fit into one »Epoch«
Definition: block-flow.hpp:124
const double DAMP_THRESHOLD
do not account for (almost) empty Epochs to avoid overshooting regulation
Definition: block-flow.hpp:131
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:135
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:652
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:142
Activity & create(ARGS &&...args)
Main API operation: allocate a new Activity record.
Definition: block-flow.hpp:472
const double TARGET_FILL
aim at using this fraction of Epoch space on average (slightly below 100%)
Definition: block-flow.hpp:129
Duration timeStep_cutOff() const
< prevent stalling Epoch progression when reaching saturation
Definition: block-flow.hpp:206
boost::rational< int64_t > FSecs
rational representation of fractional seconds
Definition: timevalue.hpp:220
static Epoch & asEpoch(Extent &extent)
Definition: block-flow.hpp:363
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:622
Memory management scheme for cyclically used memory extents.
size_t initialEpochCnt() const
< reserve allocation headroom for two duty cycles
Definition: block-flow.hpp:182
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:134
static const Time NEVER
border condition marker value. NEVER >= any time value
Definition: timevalue.hpp:314
Adapt the access to the raw storage to present the Extents as Epoch; also caches the address resoluti...
Definition: block-flow.hpp:372
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:468
void discardBefore(Time deadline)
Clean-up all storage related to activities before the given deadline.
Definition: block-flow.hpp:574
const size_t INITIAL_STREAMS
Number of streams with REFERENCE_FPS to expect for normal use.
Definition: block-flow.hpp:126
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:200
a family of time value like entities and their relationships.
basic constant internal time value.
Definition: timevalue.hpp:133
size_t cntElm()
count all currently active allocated elements
Definition: block-flow.hpp:763
Vault-Layer implementation namespace root.
std::string allEpochs()
render deadlines of all currently active Epochs
Definition: block-flow.hpp:753
AllocatorHandle until(Time deadline)
initiate allocations for activities to happen until some deadline
Definition: block-flow.hpp:516
Descriptor for a piece of operational logic performed by the scheduler.