Lumiera  0.pre.03
»edit your freedom«
test-chain-load.hpp
Go to the documentation of this file.
1 /*
2  TEST-CHAIN-LOAD.hpp - produce a configurable synthetic computation load
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 
118 #ifndef VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H
119 #define VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H
120 
121 
122 #include "vault/common.hpp"
123 #include "lib/test/transiently.hpp"
124 
125 #include "vault/gear/job.h"
126 #include "vault/gear/scheduler.hpp"
130 #include "lib/incidence-count.hpp"
131 #include "lib/time/timevalue.hpp"
132 #include "lib/time/quantiser.hpp"
133 #include "lib/iter-explorer.hpp"
134 #include "lib/format-string.hpp"
135 #include "lib/format-cout.hpp"
136 #include "lib/random-draw.hpp"
137 #include "lib/dot-gen.hpp"
138 #include "lib/util.hpp"
139 
140 #include <boost/functional/hash.hpp>
141 #include <functional>
142 #include <utility>
143 #include <future>
144 #include <memory>
145 #include <string>
146 #include <vector>
147 #include <tuple>
148 #include <array>
149 
150 
151 namespace vault{
152 namespace gear {
153 namespace test {
154 
155  using util::_Fmt;
156  using util::min;
157  using util::max;
158  using util::isnil;
159  using util::limited;
160  using util::unConst;
161  using util::toString;
162  using util::isLimited;
163  using util::showHashLSB;
164  using lib::time::Time;
165  using lib::time::TimeValue;
166  using lib::time::FrameRate;
167  using lib::time::Duration;
171  using lib::meta::_FunRet;
172 
173  using std::string;
174  using std::function;
175  using std::make_pair;
176  using std::forward;
177  using std::string;
178  using std::swap;
179  using std::move;
180  using std::chrono_literals::operator ""s;
181 
182  namespace err = lumiera::error;
183  namespace dot = lib::dot_gen;
184 
185  namespace { // Default definitions for structured load testing
186 
187  const size_t DEFAULT_FAN = 16;
188  const size_t DEFAULT_SIZ = 256;
189 
190  const auto SAFETY_TIMEOUT = 5s;
191  const auto STANDARD_DEADLINE = 30ms;
192  const size_t DEFAULT_CHUNKSIZE = 64;
193  const double UPFRONT_PLANNING_BOOST = 2.6;
194  const size_t GRAPH_BENCHMARK_RUNS = 5;
195  const size_t LOAD_BENCHMARK_RUNS = 500;
196  const double LOAD_SPEED_BASELINE = 100;
197  const microseconds LOAD_DEFAULT_TIME = 100us;
198  const size_t LOAD_DEFAULT_MEM_SIZE = 1000;
199  const Duration SCHEDULE_LEVEL_STEP{_uTicks(1ms)};
201  const Duration SCHEDULE_PLAN_STEP{_uTicks(100us)};
202  const Offset SCHEDULE_WAKE_UP{_uTicks(10us)};
203  const bool SCHED_DEPENDS = false;
204  const bool SCHED_NOTIFY = true;
205 
206  inline uint
207  defaultConcurrency()
208  {
210  }
211 
212  inline double
213  _uSec (microseconds ticks)
214  {
215  return std::chrono::duration<double, std::micro>{ticks}.count();
216  }
217  }
218 
219  struct LevelWeight
220  {
221  size_t level{0};
222  size_t nodes{0};
223  size_t endidx{0};
224  size_t weight{0};
225  };
226 
236  inline double
237  computeWeightFactor (LevelWeight const& lw, uint concurrency)
238  {
239  REQUIRE (0 < concurrency);
240  double speedUp = lw.nodes? lw.nodes / std::ceil (double(lw.nodes)/concurrency)
241  : 1.0;
242  ENSURE (1.0 <= speedUp);
243  return lw.weight / speedUp;
244  }
245 
246  struct Statistic;
247 
248 
249 
250 
251  /***********************************************************************/
257  template<size_t maxFan =DEFAULT_FAN>
260  {
261 
262  public:
264  struct Node
266  {
267  using _Arr = std::array<Node*, maxFan>;
268  using Iter = typename _Arr::iterator;
269  using CIter = typename _Arr::const_iterator;
270 
272  struct Tab : _Arr
273  {
274  Iter after = _Arr::begin();
275 
276  Iter end() { return after; }
277  CIter end() const{ return after; }
278  friend Iter end (Tab & tab){ return tab.end(); }
279  friend CIter end (Tab const& tab){ return tab.end(); }
280 
281  Node* front() { return empty()? nullptr : _Arr::front(); }
282  Node* back() { return empty()? nullptr : *(after-1); }
283 
284  void clear() { after = _Arr::begin(); }
285 
286  size_t size() const { return unConst(this)->end()-_Arr::begin(); }
287  bool empty() const { return 0 == size(); }
288 
289  Iter
290  add(Node* n)
291  {
292  if (after != _Arr::end())
293  {
294  *after = n;
295  return after++;
296  }
297  NOTREACHED ("excess node linkage");
298  }
299 
300  };
301 
302  size_t hash;
303  size_t level{0}, weight{0};
304  Tab pred{0}, succ{0};
305 
306  Node(size_t seed =0)
307  : hash{seed}
308  { }
309 
310  void
311  clear()
312  {
313  hash = 0;
314  level = weight = 0;
315  pred.clear();
316  succ.clear();
317  }
318 
319  Node&
320  addPred (Node* other)
321  {
322  REQUIRE (other);
323  pred.add (other);
324  other->succ.add (this);
325  return *this;
326  }
327 
328  Node&
329  addSucc (Node* other)
330  {
331  REQUIRE (other);
332  succ.add (other);
333  other->pred.add (this);
334  return *this;
335  }
336  Node& addPred(Node& other) { return addPred(&other); }
337  Node& addSucc(Node& other) { return addSucc(&other); }
338 
339  size_t
340  calculate()
341  {
342  for (Node* entry: pred)
343  boost::hash_combine (hash, entry->hash);
344  return hash;
345  }
346 
347  friend bool isStart (Node const& n) { return isnil (n.pred); };
348  friend bool isExit (Node const& n) { return isnil (n.succ); };
349  friend bool isInner (Node const& n) { return not (isStart(n) or isExit(n)); }
350  friend bool isFork (Node const& n) { return 1 < n.succ.size(); }
351  friend bool isJoin (Node const& n) { return 1 < n.pred.size(); }
352  friend bool isLink (Node const& n) { return 1 == n.pred.size() and 1 == n.succ.size(); }
353  friend bool isKnot (Node const& n) { return isFork(n) and isJoin(n); }
354 
355 
356  friend bool isStart (Node const* n) { return n and isStart(*n); };
357  friend bool isExit (Node const* n) { return n and isExit (*n); };
358  friend bool isInner (Node const* n) { return n and isInner(*n); };
359  friend bool isFork (Node const* n) { return n and isFork (*n); };
360  friend bool isJoin (Node const* n) { return n and isJoin (*n); };
361  friend bool isLink (Node const* n) { return n and isLink (*n); };
362  friend bool isKnot (Node const* n) { return n and isKnot (*n); };
363  };
364 
365 
367  class NodeControlBinding;
368 
371 
374 
375  private:
376  using NodeTab = typename Node::Tab;
378 
379  std::unique_ptr<Node[]> nodes_;
380  size_t numNodes_;
381 
382  Rule seedingRule_ {};
383  Rule expansionRule_{};
384  Rule reductionRule_{};
385  Rule pruningRule_ {};
386  Rule weightRule_ {};
387 
388  Node* frontNode() { return &nodes_[0]; }
389  Node* afterNode() { return &nodes_[numNodes_]; }
390  Node* backNode() { return &nodes_[numNodes_-1];}
391 
392  public:
393  explicit
394  TestChainLoad(size_t nodeCnt =DEFAULT_SIZ)
395  : nodes_{new Node[nodeCnt]}
396  , numNodes_{nodeCnt}
397  {
398  REQUIRE (1 < nodeCnt);
399  }
400 
401 
402  size_t size() const { return numNodes_; }
403  size_t topLevel() const { return unConst(this)->backNode()->level; }
404  size_t getSeed() const { return unConst(this)->frontNode()->hash; }
405 
406 
407  auto
408  allNodes()
409  {
410  return lib::explore (NodeIT{frontNode(),afterNode()});
411  }
412  auto
413  allNodePtr()
414  {
415  return allNodes().asPtr();
416  }
417 
418  auto
419  allExitNodes()
420  {
421  return allNodes().filter([](Node& n){ return isExit(n); });
422  }
423  auto
424  allExitHashes() const
425  {
426  return unConst(this)->allExitNodes().transform([](Node& n){ return n.hash; });
427  }
428 
430  size_t
431  getHash() const
432  {
433  auto combineBoostHashes = [](size_t h, size_t hx){ boost::hash_combine(h,hx); return h;};
434  return allExitHashes()
435  .filter([](size_t h){ return h != 0; })
436  .reduce(lib::iter_explorer::IDENTITY
437  ,combineBoostHashes
438  );
439  }
440 
441 
443  size_t nodeID(Node const* n){ return n - frontNode(); };
444  size_t nodeID(Node const& n){ return nodeID (&n); };
445 
446 
447 
448  /* ===== topology control ===== */
449 
450  TestChainLoad&&
451  seedingRule (Rule r)
452  {
453  seedingRule_ = move(r);
454  return move(*this);
455  }
456 
457  TestChainLoad&&
458  expansionRule (Rule r)
459  {
460  expansionRule_ = move(r);
461  return move(*this);
462  }
463 
464  TestChainLoad&&
465  reductionRule (Rule r)
466  {
467  reductionRule_ = move(r);
468  return move(*this);
469  }
470 
471  TestChainLoad&&
472  pruningRule (Rule r)
473  {
474  pruningRule_ = move(r);
475  return move(*this);
476  }
477 
478  TestChainLoad&&
479  weightRule (Rule r)
480  {
481  weightRule_ = move(r);
482  return move(*this);
483  }
484 
485 
487  static Rule rule() { return Rule(); }
488  static Rule value(size_t v) { return Rule().fixedVal(v); }
489 
490  static Rule
491  rule_atStart (uint v)
492  {
493  return Rule().mapping([v](Node* n)
494  {
495  return isStart(n)? Rule().fixedVal(v)
496  : Rule();
497  });
498  }
499 
500  static Rule
501  rule_atJoin (uint v)
502  {
503  return Rule().mapping([v](Node* n)
504  {
505  return isJoin(n) ? Rule().fixedVal(v)
506  : Rule();
507  });
508  }
509 
510  static Rule
511  rule_atLink (uint v)
512  {
513  return Rule().mapping([v](Node* n)
514  { // NOTE: when applying these rules,
515  // successors are not yet wired...
516  return not (isJoin(n) or isStart(n))
517  ? Rule().fixedVal(v)
518  : Rule();
519  });
520  }
521 
522  static Rule
523  rule_atJoin_else (double p1, double p2, uint v=1)
524  {
525  return Rule().mapping([p1,p2,v](Node* n)
526  {
527  return isJoin(n) ? Rule().probability(p1).maxVal(v)
528  : Rule().probability(p2).maxVal(v);
529  });
530  }
531 
532 
534  TestChainLoad&&
536  {
537  pruningRule(value(1));
538  weightRule(value(1));
539  return move(*this);
540  }
541 
543  TestChainLoad&&
545  {
546  pruningRule(rule().probability(0.8));
547  weightRule(value(1));
548  return move(*this);
549  }
550 
552  TestChainLoad&&
554  {
555  pruningRule(rule().probability(0.6));
556  seedingRule(rule_atStart(1));
557  weightRule(value(1));
558  return move(*this);
559  }
560 
562  TestChainLoad&&
564  {
565  seedingRule(rule().probability(0.8).maxVal(1));
566  reductionRule(rule().probability(0.75).maxVal(3));
567  pruningRule(rule_atJoin(1));
568  weightRule(value(1));
569  return move(*this);
570  }
571 
574  TestChainLoad&&
576  {
577  expansionRule(rule().probability(0.27).maxVal(4));
578  reductionRule(rule().probability(0.44).maxVal(6).minVal(2));
579  weightRule (rule().probability(0.66).maxVal(3));
580  setSeed(55); // ◁─────── produces a prelude with parallel chains,
581  return move(*this);// then fork at level 17 followed by bursts of load.
582  }
583 
584 
585 
592  TestChainLoad&&
594  {
595  NodeTab a,b, // working data for generation
596  *curr{&a}, // the current set of nodes to carry on
597  *next{&b}; // the next set of nodes connected to current
598  Node* node = frontNode();
599  size_t level{0};
600 
601  // transient snapshot of rules (non-copyable, once engaged)
602  Transiently originalExpansionRule{expansionRule_};
603  Transiently originalReductionRule{reductionRule_};
604  Transiently originalseedingRule {seedingRule_};
605  Transiently originalPruningRule {pruningRule_};
606  Transiently originalWeightRule {weightRule_};
607 
608  // prepare building blocks for the topology generation...
609  auto moreNext = [&]{ return next->size() < maxFan; };
610  auto moreNodes = [&]{ return node <= backNode(); };
611  auto spaceLeft = [&]{ return moreNext() and moreNodes(); };
612  auto addNode = [&](size_t seed =0)
613  {
614  Node* n = *next->add (node++);
615  n->clear();
616  n->level = level;
617  n->hash = seed;
618  return n;
619  };
620  auto apply = [&](Rule& rule, Node* n)
621  {
622  return rule(n);
623  };
624  auto calcNode = [&](Node* n)
625  {
626  n->calculate();
627  n->weight = apply(weightRule_,n);
628  };
629 
630  // visit all further nodes and establish links
631  while (moreNodes())
632  {
633  curr->clear();
634  swap (next, curr);
635  size_t toReduce{0};
636  Node* r = nullptr;
637  REQUIRE (spaceLeft());
638  for (Node* o : *curr)
639  { // follow-up on all Nodes in current level...
640  calcNode(o);
641  if (apply (pruningRule_,o))
642  continue; // discontinue
643  size_t toSeed = apply (seedingRule_, o);
644  size_t toExpand = apply (expansionRule_,o);
645  while (0 < toSeed and spaceLeft())
646  { // start a new chain from seed
647  addNode(this->getSeed());
648  --toSeed;
649  }
650  while (0 < toExpand and spaceLeft())
651  { // fork out secondary chain from o
652  Node* n = addNode();
653  o->addSucc(n);
654  --toExpand;
655  }
656  if (not toReduce)
657  { // carry-on chain from o
658  r = spaceLeft()? addNode():nullptr;
659  toReduce = apply (reductionRule_, o);
660  }
661  else
662  --toReduce;
663  if (r) // connect chain from o...
664  r->addPred(o);
665  else // space for successors is already exhausted
666  { // can not carry-on, but must ensure no chain is broken
667  ENSURE (not next->empty());
668  if (o->succ.empty())
669  o->addSucc (next->back());
670  }
671  }
672  ENSURE (not isnil(next) or spaceLeft());
673  if (isnil(next)) // ensure graph continues
674  addNode(this->getSeed());
675  ENSURE (not next->empty());
676  ++level;
677  }
678  ENSURE (node > backNode());
679  // all open nodes on last level become exit nodes
680  for (Node* o : *next)
681  calcNode(o);
682  //
683  return move(*this);
684  }
685 
686 
691  TestChainLoad&&
692  setSeed (size_t seed = rand())
693  {
694  frontNode()->hash = seed;
695  return move(*this);
696  }
697 
698 
703  TestChainLoad&&
704  setWeight (size_t fixedNodeWeight =1)
705  {
706  for (Node& n : allNodes())
707  n.weight = fixedNodeWeight;
708  return move(*this);
709  }
710 
711 
715  TestChainLoad&&
717  {
718  size_t seed = this->getSeed();
719  for (Node& n : allNodes())
720  {
721  n.hash = isStart(n)? seed : 0;
722  n.calculate();
723  }
724  return move(*this);
725  }
726 
727 
731  TestChainLoad&&
733  {
734  size_t seed = this->getSeed();
735  for (Node& n : allNodes())
736  n.hash = isStart(n)? seed : 0;
737  return move(*this);
738  }
739 
740 
741 
742  /* ===== Operators ===== */
743 
744  std::string
745  generateTopologyDOT()
746  {
747  using namespace dot;
748 
749  Section nodes("Nodes");
750  Section layers("Layers");
751  Section topology("Topology");
752 
753  // Styles to distinguish the computation nodes
754  Code BOTTOM{"shape=doublecircle"};
755  Code SEED {"shape=circle"};
756  Code TOP {"shape=box, style=rounded"};
757  Code DEFAULT{};
758 
759  // prepare time-level zero
760  size_t level(0);
761  auto timeLevel = scope(level).rank("min ");
762 
763  for (Node& n : allNodes())
764  {
765  size_t i = nodeID(n);
766  string tag{toString(i)+": "+showHashLSB(n.hash)};
767  if (n.weight) tag +="."+toString(n.weight);
768  nodes += node(i).label(tag)
769  .style(i==0 ? BOTTOM
770  :isnil(n.pred)? SEED
771  :isnil(n.succ)? TOP
772  : DEFAULT);
773  for (Node* suc : n.succ)
774  topology += connect (i, nodeID(*suc));
775 
776  if (level != n.level)
777  {// switch to next time-level
778  layers += timeLevel;
779  ++level;
780  ENSURE (level == n.level);
781  timeLevel = scope(level).rank("same");
782  }
783  timeLevel.add (node(i));
784  }
785  layers += timeLevel; // close last layer
786 
787  // combine and render collected definitions as DOT-code
788  return digraph (nodes, layers, topology);
789  }
790 
791  TestChainLoad&&
792  printTopologyDOT()
793  {
794  cout << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───\n"
795  << generateTopologyDOT()
796  << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
797  << endl;
798  return move(*this);
799  }
800 
801 
809  double
811  ,size_t sizeBase =0
812  ,size_t repeatCnt=GRAPH_BENCHMARK_RUNS
813  )
814  {
815  return microBenchmark ([&]{ performGraphSynchronously(timeBase,sizeBase); }
816  ,repeatCnt)
817  .first; // ∅ runtime in µs
818  }
819 
821  TestChainLoad&& performGraphSynchronously(microseconds timeBase =LOAD_DEFAULT_TIME
822  ,size_t sizeBase =0);
823 
824  TestChainLoad&&
825  printRuntimeReference(microseconds timeBase =LOAD_DEFAULT_TIME
826  ,size_t sizeBase =0
827  ,size_t repeatCnt=GRAPH_BENCHMARK_RUNS
828  )
829  {
830  cout << _Fmt{"runtime ∅(%d) = %6.2fms (single-threaded)\n"}
831  % repeatCnt
832  % (1e-3 * calcRuntimeReference(timeBase,sizeBase,repeatCnt))
833  << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
834  << endl;
835  return move(*this);
836  }
837 
838 
840  size_t
842  {
843  return allNodes()
844  .transform([](Node& n){ return n.weight; })
845  .resultSum();
846  }
847 
849  auto
851  {
852  return allNodes()
853  .groupedBy([](Node& n){ return n.level; }
854  ,[this](LevelWeight& lw, Node const& n)
855  {
856  lw.level = n.level;
857  lw.weight += n.weight;
858  lw.endidx = nodeID(n);
859  ++lw.nodes;
860  }
861  );
862  }
863 
865  auto
866  levelScheduleSequence (uint concurrency =1)
867  {
868  return allLevelWeights()
869  .transform([schedule=0.0, concurrency]
870  (LevelWeight const& lw) mutable
871  {
872  schedule += computeWeightFactor (lw, concurrency);
873  return schedule;
874  });
875  }
876 
878  double
879  calcExpectedCompoundedWeight (uint concurrency =1)
880  {
881  return allLevelWeights()
882  .transform([concurrency](LevelWeight const& lw){ return computeWeightFactor (lw, concurrency); })
883  .resultSum();
884  }
885 
886 
887 
888  Statistic computeGraphStatistics();
889  TestChainLoad&& printTopologyStatistics();
890 
891  class ScheduleCtx;
892  friend class ScheduleCtx; // accesses raw storage array
893 
894  ScheduleCtx setupSchedule (Scheduler& scheduler);
895  };
896 
897 
898 
907  template<size_t maxFan>
909  : public std::function<Param(Node*)>
910  {
911  protected:
913  static size_t
914  defaultSrc (Node* node)
915  {
916  return node? node->hash:0;
917  }
918 
919  static size_t
920  level (Node* node)
921  {
922  return node? node->level:0;
923  }
924 
925  static double
926  guessHeight (size_t level)
927  { // heuristic guess for a »fully stable state«
928  double expectedHeight = 2*maxFan;
929  return level / expectedHeight;
930  }
931 
932 
933 
935  template<class SIG>
936  struct Adaptor
937  {
938  static_assert (not sizeof(SIG), "Unable to adapt given functor.");
939  };
940 
942  template<typename RES>
943  struct Adaptor<RES(size_t)>
944  {
945  template<typename FUN>
946  static auto
947  build (FUN&& fun)
948  {
949  return [functor=std::forward<FUN>(fun)]
950  (Node* node) -> _FunRet<FUN>
951  {
952  return functor (defaultSrc (node));
953  };
954  }
955  };
956 
960  template<typename RES>
961  struct Adaptor<RES(size_t,double)>
962  {
963 
964  template<typename FUN>
965  static auto
966  build (FUN&& fun)
967  {
968  return [functor=std::forward<FUN>(fun)]
969  (Node* node) -> _FunRet<FUN>
970  {
971  return functor (defaultSrc (node)
972  ,guessHeight(level(node)));
973  };
974  }
975  };
976 
978  template<typename RES>
979  struct Adaptor<RES(double)>
980  {
981 
982  template<typename FUN>
983  static auto
984  build (FUN&& fun)
985  {
986  return [functor=std::forward<FUN>(fun)]
987  (Node* node) -> _FunRet<FUN>
988  {
989  return functor (guessHeight(level(node)));
990  };
991  }
992  };
993  };
994 
995 
996 
997 
998  /* ========= Graph Statistics Evaluation ========= */
999 
1000  struct StatKey
1001  : std::pair<size_t,string>
1002  {
1003  using std::pair<size_t,string>::pair;
1004  operator size_t const&() const { return this->first; }
1005  operator string const&() const { return this->second;}
1006  };
1007  const StatKey STAT_NODE{0,"node"};
1008  const StatKey STAT_SEED{1,"seed"};
1009  const StatKey STAT_EXIT{2,"exit"};
1010  const StatKey STAT_INNR{3,"innr"};
1011  const StatKey STAT_FORK{4,"fork"};
1012  const StatKey STAT_JOIN{5,"join"};
1013  const StatKey STAT_LINK{6,"link"};
1014  const StatKey STAT_KNOT{7,"knot"};
1015  const StatKey STAT_WGHT{8,"wght"};
1016 
1018  const uint CAT = KEYS.size();
1019  const uint IDX_SEED = 1; // index of STAT_SEED
1020 
1021  namespace {
1022  template<class NOD>
1023  inline auto
1024  prepareEvaluations()
1025  {
1026  return std::array<std::function<uint(NOD&)>, CAT>
1027  { [](NOD& ){ return 1; }
1028  , [](NOD& n){ return isStart(n);}
1029  , [](NOD& n){ return isExit(n); }
1030  , [](NOD& n){ return isInner(n);}
1031  , [](NOD& n){ return isFork(n); }
1032  , [](NOD& n){ return isJoin(n); }
1033  , [](NOD& n){ return isLink(n); }
1034  , [](NOD& n){ return isKnot(n); }
1035  , [](NOD& n){ return n.weight; }
1036  };
1037  }
1038  }
1039 
1040  using VecU = std::vector<uint>;
1041  using LevelSums = std::array<uint, CAT>;
1042 
1048  struct Indicator
1049  {
1050  VecU data{};
1051  uint cnt{0};
1052  double frac{0};
1053  double pS {0};
1054  double pL {0};
1055  double pLW{0};
1056  double cL {0};
1057  double cLW{0};
1058  double sL {0};
1059  double sLW{0};
1060 
1061  void
1062  addPoint (uint levelID, uint sublevelID, uint width, uint items)
1063  {
1064  REQUIRE (levelID == data.size()); // ID is zero based
1065  REQUIRE (width > 0);
1066  data.push_back (items);
1067  cnt += items;
1068  pS += items;
1069  pL += items;
1070  pLW += items / double(width);
1071  cL += levelID * items;
1072  cLW += levelID * items/double(width);
1073  sL += sublevelID * items;
1074  sLW += sublevelID * items/double(width);
1075  }
1076 
1077  void
1078  closeAverages (uint nodes, uint levels, uint segments, double avgheight)
1079  {
1080  REQUIRE (levels == data.size());
1081  REQUIRE (levels > 0);
1082  frac = cnt / double(nodes);
1083  cL = pL? cL/pL :0; // weighted averages: normalise to weight sum
1084  cLW = pLW? cLW/pLW :0;
1085  sL = pL? sL/pL :0;
1086  sLW = pLW? sLW/pLW :0;
1087  pS /= segments; // simple averages : normalise to number of levels
1088  pL /= levels; // simple averages : normalise to number of levels
1089  pLW /= levels;
1090  cL /= levels-1; // weight centres : as fraction of maximum level-ID
1091  cLW /= levels-1;
1092  ASSERT (avgheight >= 1.0);
1093  if (avgheight > 1.0)
1094  { // likewise for weight centres relative to subgraph
1095  sL /= avgheight-1; // height is 1-based, while the contribution was 0-based
1096  sLW /= avgheight-1;
1097  }
1098  else
1099  sL = sLW = 0.5;
1100  }
1101  };
1102 
1106  struct Statistic
1107  {
1108  uint nodes{0};
1109  uint levels{0};
1110  uint segments{1};
1111  uint maxheight{0};
1112  double avgheight{0};
1113  VecU width{};
1114  VecU sublevel{};
1115 
1116  std::array<Indicator, CAT> indicators;
1117 
1118  explicit
1119  Statistic (uint lvls)
1120  : nodes{0}
1121  , levels{0}
1122  {
1123  reserve (lvls);
1124  }
1125 
1126  void
1127  addPoint (uint levelWidth, uint sublevelID, LevelSums& particulars)
1128  {
1129  levels += 1;
1130  nodes += levelWidth;
1131  width.push_back (levelWidth);
1132  sublevel.push_back (sublevelID);
1133  ASSERT (levels == width.size());
1134  ASSERT (0 < levels);
1135  ASSERT (0 < levelWidth);
1136  for (uint i=0; i< CAT; ++i)
1137  indicators[i].addPoint (levels-1, sublevelID, levelWidth, particulars[i]);
1138  }
1139 
1140  void
1141  closeAverages (uint segs, uint maxSublevelID)
1142  {
1143  segments = segs;
1144  maxheight = maxSublevelID + 1;
1145  avgheight = levels / double(segments);
1146  for (uint i=0; i< CAT; ++i)
1147  indicators[i].closeAverages (nodes,levels,segments,avgheight);
1148  }
1149 
1150  private:
1151  void
1152  reserve (uint lvls)
1153  {
1154  width.reserve (lvls);
1155  sublevel.reserve(lvls);
1156  for (uint i=0; i< CAT; ++i)
1157  {
1158  indicators[i] = Indicator{};
1159  indicators[i].data.reserve(lvls);
1160  }
1161  }
1162  };
1163 
1164 
1165 
1178  template<size_t maxFan>
1179  inline Statistic
1181  {
1182  auto totalLevels = uint(topLevel());
1183  auto classify = prepareEvaluations<Node>();
1184  Statistic stat(totalLevels);
1185  LevelSums particulars{0};
1186  size_t level{0},
1187  sublevel{0},
1188  maxsublevel{0};
1189  size_t segs{0};
1190  uint width{0};
1191  auto detectSubgraphs = [&]{ // to be called when a level is complete
1192  if (width==1 and particulars[IDX_SEED]==1)
1193  { // previous level actually started new subgraph
1194  sublevel = 0;
1195  ++segs;
1196  }
1197  else
1198  maxsublevel = max (sublevel,maxsublevel);
1199  };
1200 
1201  for (Node& node : allNodes())
1202  {
1203  if (level != node.level)
1204  {// Level completed...
1205  detectSubgraphs();
1206  // record statistics for previous level
1207  stat.addPoint (width, sublevel, particulars);
1208  // switch to next time-level
1209  ++level;
1210  ++sublevel;
1211  ENSURE (level == node.level);
1212  particulars = LevelSums{0};
1213  width = 0;
1214  }
1215  // classify and account..
1216  ++width;
1217  for (uint i=0; i<CAT; ++i)
1218  particulars[i] += classify[i](node);
1219  }
1220  ENSURE (level == topLevel());
1221  detectSubgraphs();
1222  stat.addPoint (width, sublevel, particulars);
1223  stat.closeAverages (segs, maxsublevel);
1224  return stat;
1225  }
1226 
1227 
1228 
1261  template<size_t maxFan>
1262  inline TestChainLoad<maxFan>&&
1264  {
1265  cout << "INDI: cnt frac ∅pS ∅pL ∅pLW γL◆ γLW◆ γL⬙ γLW⬙\n";
1266  _Fmt line{"%4s: %3d %3.0f%% %5.1f %5.2f %4.2f %4.2f %4.2f %4.2f %4.2f\n"};
1267  Statistic stat = computeGraphStatistics();
1268  for (uint i=0; i< CAT; ++i)
1269  {
1270  Indicator& indi = stat.indicators[i];
1271  cout << line % KEYS[i]
1272  % indi.cnt
1273  % (indi.frac*100)
1274  % indi.pS
1275  % indi.pL
1276  % indi.pLW
1277  % indi.cL
1278  % indi.cLW
1279  % indi.sL
1280  % indi.sLW
1281  ;
1282  }
1283  cout << _Fmt{"LEVL: %3d\n"} % stat.levels;
1284  cout << _Fmt{"SEGS: %3d h = ∅%3.1f / max.%2d\n"}
1285  % stat.segments
1286  % stat.avgheight
1287  % stat.maxheight;
1288  cout << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
1289  << endl;
1290  return move(*this);
1291  }
1292 
1293 
1294 
1295 
1296 
1297  /* ========= Configurable Computational Load ========= */
1298 
1321  {
1322  using Sink = volatile size_t;
1323 
1324  static double&
1325  computationSpeed (bool mem)
1326  {
1327  static double cpuSpeed{LOAD_SPEED_BASELINE};
1328  static double memSpeed{LOAD_SPEED_BASELINE};
1329  return mem? memSpeed : cpuSpeed;
1330  }
1331 
1332  public:
1333  microseconds timeBase = LOAD_DEFAULT_TIME;
1334  size_t sizeBase = LOAD_DEFAULT_MEM_SIZE;
1335  bool useAllocation = false;
1336 
1338  double
1339  invoke (uint scaleStep =1)
1340  {
1341  if (scaleStep == 0 or timeBase < 1us)
1342  return 0; // disabled
1343  return useAllocation? benchmarkTime ([this,scaleStep]{ causeMemProcessLoad (scaleStep); })
1344  : benchmarkTime ([this,scaleStep]{ causeComputationLoad(scaleStep); });
1345  }
1346 
1348  double
1349  benchmark (uint scaleStep =1)
1350  {
1351  return microBenchmark ([&]{ invoke(scaleStep);}
1353  .first; // ∅ runtime in µs
1354  }
1355 
1356  void
1357  calibrate()
1358  {
1359  TRANSIENTLY(useAllocation) = false;
1360  performIncrementalCalibration();
1361  useAllocation = true;
1362  performIncrementalCalibration();
1363  }
1364 
1365  void
1366  maybeCalibrate()
1367  {
1368  if (not isCalibrated())
1369  calibrate();
1370  }
1371 
1372  bool
1373  isCalibrated() const
1374  {
1375  return computationSpeed(false) != LOAD_SPEED_BASELINE;
1376  }
1377 
1378  private:
1379  uint64_t
1380  roundsNeeded (uint scaleStep)
1381  {
1382  auto desiredMicros = scaleStep*timeBase.count();
1383  return uint64_t(desiredMicros*computationSpeed(useAllocation));
1384  }
1385 
1386  auto
1387  allocNeeded (uint scaleStep)
1388  {
1389  auto cnt = roundsNeeded(scaleStep);
1390  auto siz = max (scaleStep * sizeBase, 1u);
1391  auto rep = max (cnt/siz, 1u);
1392  // increase size to fit
1393  siz = cnt / rep;
1394  return make_pair (siz,rep);
1395  }
1396 
1397  void
1398  causeComputationLoad (uint scaleStep)
1399  {
1400  auto round = roundsNeeded (scaleStep);
1401  Sink sink;
1402  size_t scree{sink};
1403  for ( ; 0 < round; --round)
1404  boost::hash_combine (scree,scree);
1405  sink = scree;
1406  sink++;
1407  }
1408 
1409  void
1410  causeMemProcessLoad (uint scaleStep)
1411  {
1412  auto [siz,round] = allocNeeded (scaleStep);
1413  lib::UninitialisedDynBlock<size_t> memBlock{siz};
1414  Sink sink;
1415  *memBlock.front() = sink+1;
1416  for ( ; 0 < round; --round)
1417  for (size_t i=0; i<memBlock.size()-1; ++i)
1418  memBlock[i+1] += memBlock[i];
1419  sink = *memBlock.back();
1420  sink++;
1421  }
1422 
1423  double
1424  determineSpeed()
1425  {
1426  uint step4gauge = 1;
1427  double micros = benchmark (step4gauge);
1428  auto stepsDone = roundsNeeded (step4gauge);
1429  return stepsDone / micros;
1430  }
1431 
1432  void
1433  performIncrementalCalibration()
1434  {
1435  double& speed = computationSpeed(useAllocation);
1436  double prev{speed},delta;
1437  do {
1438  speed = determineSpeed();
1439  delta = abs(1.0 - speed/prev);
1440  prev = speed;
1441  }
1442  while (delta > 0.05);
1443  }
1444  };
1445 
1446 
1448  inline SpecialJobFun
1449  onetimeCrunch (milliseconds runTime)
1450  { // ensure calibration prior to use
1451  ComputationalLoad().maybeCalibrate();
1452  //
1453  return SpecialJobFun{
1454  [runTime](JobParameter) -> void
1455  {
1456  ComputationalLoad crunch;
1457  crunch.timeBase = runTime;
1458  crunch.invoke();
1459  }};
1460  }
1461 
1462 
1463 
1464 
1471  template<size_t maxFan>
1473  TestChainLoad<maxFan>::performGraphSynchronously (microseconds timeBase, size_t sizeBase)
1474  {
1475  ComputationalLoad compuLoad;
1476  compuLoad.timeBase = timeBase;
1477  if (not sizeBase)
1478  {
1479  compuLoad.sizeBase = LOAD_DEFAULT_MEM_SIZE;
1480  compuLoad.useAllocation =false;
1481  }
1482  else
1483  {
1484  compuLoad.sizeBase = sizeBase;
1485  compuLoad.useAllocation =true;
1486  }
1487  compuLoad.maybeCalibrate();
1488 
1489  size_t seed = this->getSeed();
1490  for (Node& n : allNodes())
1491  {
1492  n.hash = isStart(n)? seed : 0;
1493  if (n.weight)
1494  compuLoad.invoke (n.weight);
1495  n.calculate();
1496  }
1497  return move(*this);
1498  }
1499 
1500 
1501 
1502 
1503 
1504  /* ========= Render Job generation and Scheduling ========= */
1505 
1510  : public JobClosure
1511  {
1512 
1513  static lib::time::Grid&
1514  testGrid()
1515  {
1517  return gridOne;
1518  }
1519 
1520  /* === JobFunctor Interface === */
1521 
1522  string diagnostic() const =0;
1523  void invokeJobOperation (JobParameter) =0;
1524 
1525  JobKind
1526  getJobKind() const
1527  {
1528  return TEST_JOB;
1529  }
1530 
1532  buildInstanceID (HashVal) const override
1533  {
1534  return InvocationInstanceID();
1535  }
1536 
1537  size_t
1538  hashOfInstance (InvocationInstanceID invoKey) const override
1539  {
1540  std::hash<size_t> hashr;
1541  HashVal res = hashr (invoKey.frameNumber);
1542  return res;
1543  }
1544 
1545  public:
1546 
1551  static InvocationInstanceID
1552  encodeNodeID (size_t idx)
1553  {
1554  InvocationInstanceID invoKey;
1555  invoKey.code.w1 = idx;
1556  return invoKey;
1557  };
1558 
1559  static size_t
1560  decodeNodeID (InvocationInstanceID invoKey)
1561  {
1562  return size_t(invoKey.code.w1);
1563  };
1564 
1565  static Time
1566  encodeLevel (size_t level)
1567  {
1568  return Time{testGrid().timeOf (FrameCnt(level))};
1569  }
1570 
1571  static size_t
1572  decodeLevel (TimeValue nominalTime)
1573  {
1574  return testGrid().gridPoint (nominalTime);
1575  }
1576  };
1577 
1578 
1579 
1587  template<size_t maxFan>
1589  : public ChainFunctor
1590  {
1591  using Node = typename TestChainLoad<maxFan>::Node;
1592  using Watch = lib::IncidenceCount;
1593 
1594  Node* startNode_;
1595  ComputationalLoad* compuLoad_;
1596  Watch* watch_;
1597 
1598  public:
1599  RandomChainCalcFunctor(Node& startNode, ComputationalLoad* load =nullptr, Watch* watch =nullptr)
1600  : startNode_{&startNode}
1601  , compuLoad_{load}
1602  , watch_{watch}
1603  { }
1604 
1605 
1607  void
1608  invokeJobOperation (JobParameter param) override
1609  {
1610  if (watch_) watch_->markEnter();
1611  size_t nodeIdx = decodeNodeID (param.invoKey);
1612  size_t level = decodeLevel (TimeValue{param.nominalTime});
1613  Node& target = startNode_[nodeIdx];
1614  ASSERT (target.level == level);
1615  // invoke the »media calculation«
1616  if (compuLoad_ and target.weight)
1617  compuLoad_->invoke (target.weight);
1618  target.calculate();
1619  if (watch_) watch_->markLeave();
1620  }
1621 
1622  string diagnostic() const override
1623  {
1624  return _Fmt{"ChainCalc(w:%d)◀%s"}
1625  % maxFan
1626  % util::showAddr(startNode_);
1627  }
1628  };
1629 
1630 
1635  template<size_t maxFan>
1637  : public ChainFunctor
1638  {
1639  using Node = typename TestChainLoad<maxFan>::Node;
1640 
1641  function<void(size_t,size_t)> scheduleCalcJob_;
1642  function<void(Node*,Node*)> markDependency_;
1643  function<void(size_t,size_t,size_t,bool)> continuation_;
1644 
1645  size_t maxCnt_;
1646  Node* nodes_;
1647 
1648  size_t currIdx_{0}; // Note: this test-JobFunctor is statefull
1649 
1650  public:
1651  template<class CAL, class DEP, class CON>
1652  RandomChainPlanFunctor(Node& nodeArray, size_t nodeCnt,
1653  CAL&& schedule, DEP&& markDepend,
1654  CON&& continuation)
1655  : scheduleCalcJob_{forward<CAL> (schedule)}
1656  , markDependency_{forward<DEP> (markDepend)}
1657  , continuation_{forward<CON> (continuation)}
1658  , maxCnt_{nodeCnt}
1659  , nodes_{&nodeArray}
1660  { }
1661 
1662 
1667  void
1668  invokeJobOperation (JobParameter param) override
1669  {
1670  size_t start{currIdx_};
1671  size_t reachedLevel{0};
1672  size_t targetNodeIDX = decodeNodeID (param.invoKey);
1673  for ( ; currIdx_<maxCnt_; ++currIdx_)
1674  {
1675  Node* n = &nodes_[currIdx_];
1676  if (currIdx_ <= targetNodeIDX)
1677  reachedLevel = n->level;
1678  else // continue until end of current level
1679  if (n->level > reachedLevel)
1680  break;
1681  scheduleCalcJob_(currIdx_, n->level);
1682  for (Node* pred: n->pred)
1683  markDependency_(pred,n);
1684  }
1685  ENSURE (currIdx_ > 0);
1686  continuation_(start, currIdx_-1, reachedLevel, currIdx_ < maxCnt_);
1687  }
1688 
1689 
1690  string diagnostic() const override
1691  {
1692  return "ChainPlan";
1693  }
1694  };
1695 
1696 
1697 
1698 
1711  template<size_t maxFan>
1713  : util::MoveOnly
1714  {
1715  TestChainLoad& chainLoad_;
1716  Scheduler& scheduler_;
1717 
1719 
1720  FrameRate levelSpeed_{1, SCHEDULE_LEVEL_STEP};
1721  FrameRate planSpeed_{1, SCHEDULE_PLAN_STEP};
1722  TimeVar nodeExpense_{SCHEDULE_NODE_STEP};
1723  double stressFac_{1.0};
1724  bool schedNotify_{SCHED_NOTIFY};
1725  bool schedDepends_{SCHED_DEPENDS};
1726  uint blockLoadFac_{2};
1727  size_t chunkSize_{DEFAULT_CHUNKSIZE};
1728  TimeVar startTime_{Time::ANYTIME};
1729  microseconds deadline_{STANDARD_DEADLINE};
1730  microseconds preRoll_{guessPlanningPreroll()};
1731  ManifestationID manID_{};
1732 
1733  std::vector<TimeVar> startTimes_{};
1734  std::promise<void> signalDone_{};
1735 
1736  std::unique_ptr<ComputationalLoad> compuLoad_;
1737  std::unique_ptr<RandomChainCalcFunctor<maxFan>> calcFunctor_;
1738  std::unique_ptr<RandomChainPlanFunctor<maxFan>> planFunctor_;
1739 
1740  std::unique_ptr<lib::IncidenceCount> watchInvocations_;
1741 
1742 
1743  /* ==== Callbacks from job planning ==== */
1744 
1746  void
1747  disposeStep (size_t idx, size_t level)
1748  {
1749  schedule_[idx] = scheduler_.defineSchedule(calcJob (idx,level))
1750  .manifestation(manID_)
1751  .startTime (jobStartTime(level, idx))
1752  .lifeWindow (deadline_);
1753  Node& n = chainLoad_.nodes_[idx];
1754  if (isnil (n.pred)
1755  or schedDepends_)
1756  schedule_[idx].post();
1757  }// Node with dependencies will be triggered by NOTIFY
1758  // and thus must not necessarily be scheduled explicitly.
1759 
1761  void
1762  setDependency (Node* pred, Node* succ)
1763  {
1764  size_t predIdx = chainLoad_.nodeID (pred);
1765  size_t succIdx = chainLoad_.nodeID (succ);
1766  bool unlimitedTime = not schedNotify_;
1767  schedule_[predIdx].linkToSuccessor (schedule_[succIdx], unlimitedTime);
1768  }
1769 
1771  void
1772  continuation (size_t chunkStart, size_t lastNodeIDX, size_t levelDone, bool work_left)
1773  {
1774  if (work_left)
1775  {
1776  size_t nextChunkEndNode = calcNextChunkEnd (lastNodeIDX);
1777  scheduler_.continueMetaJob (calcPlanScheduleTime (lastNodeIDX+1)
1778  ,planningJob (nextChunkEndNode)
1779  ,manID_);
1780  }
1781  else
1782  {
1783  auto wakeUp = scheduler_.defineSchedule(wakeUpJob())
1784  .manifestation (manID_)
1785  .startTime(jobStartTime (levelDone+1, lastNodeIDX+1) + SCHEDULE_WAKE_UP)
1786  .lifeWindow(SAFETY_TIMEOUT)
1787  .post();
1788  for (size_t exitIDX : lastExitNodes (chunkStart))
1789  wakeUp.linkToPredecessor (schedule_[exitIDX]);
1790  } // Setup wait-dependency on last computations
1791  }
1792 
1793 
1794  std::future<void>
1795  performRun()
1796  {
1797  auto finished = attachNewCompletionSignal();
1798  size_t numNodes = chainLoad_.size();
1799  size_t firstChunkEndNode = calcNextChunkEnd(0);
1800  schedule_.allocate (numNodes);
1801  compuLoad_->maybeCalibrate();
1802  calcFunctor_.reset (new RandomChainCalcFunctor<maxFan>{chainLoad_.nodes_[0], compuLoad_.get(), watchInvocations_.get()});
1803  planFunctor_.reset (new RandomChainPlanFunctor<maxFan>{chainLoad_.nodes_[0], chainLoad_.numNodes_
1804  ,[this](size_t i, size_t l){ disposeStep(i,l); }
1805  ,[this](auto* p, auto* s) { setDependency(p,s);}
1806  ,[this](size_t s,size_t n,size_t l, bool w)
1807  { continuation(s,n,l,w); }
1808  });
1809  startTime_ = anchorSchedule();
1810  scheduler_.seedCalcStream (planningJob(firstChunkEndNode)
1811  ,manID_
1812  ,calcLoadHint());
1813  return finished;
1814  }
1815 
1816 
1817  public:
1818  ScheduleCtx (TestChainLoad& mother, Scheduler& scheduler)
1819  : chainLoad_{mother}
1820  , scheduler_{scheduler}
1821  , compuLoad_{new ComputationalLoad}
1822  , calcFunctor_{}
1823  , planFunctor_{}
1824  { }
1825 
1829  double
1831  try {
1832  return benchmarkTime ([this]
1833  {
1834  awaitBlocking(
1835  performRun());
1836  })
1837  -_uSec(preRoll_); // timing accounted without pre-roll
1838  }
1839  ERROR_LOG_AND_RETHROW(test,"Scheduler testing")
1840 
1841  auto
1842  getScheduleSeq()
1843  {
1844  if (isnil (startTimes_))
1845  fillDefaultSchedule();
1846 
1847  return lib::explore(startTimes_)
1848  .transform([&](Time jobTime) -> TimeVar
1849  {
1850  return jobTime - startTimes_.front();
1851  });
1852  }
1853 
1854  double
1855  getExpectedEndTime()
1856  {
1857  return _raw(startTimes_.back() - startTimes_.front()
1858  + Duration{nodeExpense_}*(chainLoad_.size()/stressFac_));
1859  }
1860 
1861  auto
1862  getInvocationStatistic()
1863  {
1864  return watchInvocations_? watchInvocations_->evaluate()
1866  }
1867 
1868  double
1869  calcRuntimeReference()
1870  {
1871  microseconds timeBase = compuLoad_->timeBase;
1872  size_t sizeBase = compuLoad_->useAllocation? compuLoad_->sizeBase : 0;
1873  return chainLoad_.calcRuntimeReference (timeBase, sizeBase);
1874  }
1875 
1876  double getStressFac() { return stressFac_; }
1877 
1878 
1879 
1880  /* ===== Setter / builders for custom configuration ===== */
1881 
1882  ScheduleCtx&&
1883  withInstrumentation (bool doWatch =true)
1884  {
1885  if (doWatch)
1886  {
1887  watchInvocations_.reset (new lib::IncidenceCount);
1888  watchInvocations_->expectThreads (work::Config::COMPUTATION_CAPACITY)
1889  .expectIncidents(chainLoad_.size());
1890  }
1891  else
1892  watchInvocations_.reset();
1893  return move(*this);
1894  }
1895 
1896  ScheduleCtx&&
1897  withPlanningStep (microseconds planningTime_per_node)
1898  {
1899  planSpeed_ = FrameRate{1, Duration{_uTicks(planningTime_per_node)}};
1900  preRoll_ = guessPlanningPreroll();
1901  return move(*this);
1902  }
1903 
1904  ScheduleCtx&&
1905  withChunkSize (size_t nodes_per_chunk)
1906  {
1907  chunkSize_ = nodes_per_chunk;
1908  preRoll_ = guessPlanningPreroll();
1909  return move(*this);
1910  }
1911 
1912  ScheduleCtx&&
1913  withPreRoll (microseconds planning_headstart)
1914  {
1915  preRoll_ = planning_headstart;
1916  return move(*this);
1917  }
1918 
1919  ScheduleCtx&&
1920  withUpfrontPlanning()
1921  {
1922  withChunkSize (chainLoad_.size());
1923  preRoll_ *= UPFRONT_PLANNING_BOOST;
1924  return move(*this);
1925  }
1926 
1927  ScheduleCtx&&
1928  withLevelDuration (microseconds fixedTime_per_level)
1929  {
1930  levelSpeed_ = FrameRate{1, Duration{_uTicks(fixedTime_per_level)}};
1931  return move(*this);
1932  }
1933 
1934  ScheduleCtx&&
1935  withBaseExpense (microseconds fixedTime_per_node)
1936  {
1937  nodeExpense_ = _uTicks(fixedTime_per_node);
1938  return move(*this);
1939  }
1940 
1941  ScheduleCtx&&
1942  withSchedDepends (bool explicitly)
1943  {
1944  schedDepends_ = explicitly;
1945  return move(*this);
1946  }
1947 
1948  ScheduleCtx&&
1949  withSchedNotify (bool doSetTime =true)
1950  {
1951  schedNotify_ = doSetTime;
1952  return move(*this);
1953  }
1954 
1961  ScheduleCtx&&
1962  withAdaptedSchedule (double stressFac =1.0, uint concurrency=0, double formFac =1.0)
1963  {
1964  if (not concurrency) // use hardware concurrency (#cores) by default
1965  concurrency = defaultConcurrency();
1966  ENSURE (isLimited (1u, concurrency, 3*defaultConcurrency()));
1967  REQUIRE (formFac > 0.0);
1968  stressFac /= formFac;
1969  withLevelDuration (compuLoad_->timeBase);
1970  fillAdaptedSchedule (stressFac, concurrency);
1971  return move(*this);
1972  }
1973 
1974  double
1975  determineEmpiricFormFactor (uint concurrency=0)
1976  {
1977  if (not watchInvocations_) return 1.0;
1978  auto stat = watchInvocations_->evaluate();
1979  if (0 == stat.activationCnt) return 1.0;
1980  // looks like we have actual measurement data...
1981  ENSURE (0.0 < stat.avgConcurrency);
1982  if (not concurrency)
1983  concurrency = defaultConcurrency();
1984  double worktimeRatio = 1 - stat.timeAtConc(0) / stat.coveredTime;
1985  double workConcurrency = stat.avgConcurrency / worktimeRatio;
1986  double weightSum = chainLoad_.calcWeightSum();
1987  double expectedCompoundedWeight = chainLoad_.calcExpectedCompoundedWeight(concurrency);
1988  double expectedConcurrency = weightSum / expectedCompoundedWeight;
1989  double formFac = 1 / (workConcurrency / expectedConcurrency);
1990  double expectedNodeTime = _uSec(compuLoad_->timeBase) * weightSum / chainLoad_.size();
1991  double realAvgNodeTime = stat.activeTime / stat.activationCnt;
1992  formFac *= realAvgNodeTime / expectedNodeTime;
1993  return formFac;
1994  }
1995 
1996  ScheduleCtx&&
1997  withJobDeadline (microseconds deadline_after_start)
1998  {
1999  deadline_ = deadline_after_start;
2000  return move(*this);
2001  }
2002 
2003  ScheduleCtx&&
2004  withAnnouncedLoadFactor (uint factor_on_levelSpeed)
2005  {
2006  blockLoadFac_ = factor_on_levelSpeed;
2007  return move(*this);
2008  }
2009 
2010  ScheduleCtx&&
2011  withManifestation (ManifestationID manID)
2012  {
2013  manID_ = manID;
2014  return move(*this);
2015  }
2016 
2017  ScheduleCtx&&
2018  withLoadTimeBase (microseconds timeBase =LOAD_DEFAULT_TIME)
2019  {
2020  compuLoad_->timeBase = timeBase;
2021  return move(*this);
2022  }
2023 
2024  ScheduleCtx&&
2025  deactivateLoad()
2026  {
2027  compuLoad_->timeBase = 0us;
2028  return move(*this);
2029  }
2030 
2031  ScheduleCtx&&
2032  withLoadMem (size_t sizeBase =LOAD_DEFAULT_MEM_SIZE)
2033  {
2034  if (not sizeBase)
2035  {
2036  compuLoad_->sizeBase = LOAD_DEFAULT_MEM_SIZE;
2037  compuLoad_->useAllocation =false;
2038  }
2039  else
2040  {
2041  compuLoad_->sizeBase = sizeBase;
2042  compuLoad_->useAllocation =true;
2043  }
2044  return move(*this);
2045  }
2046 
2047  private:
2049  std::future<void>
2051  {
2052  std::promise<void> notYetTriggered;
2053  signalDone_.swap (notYetTriggered);
2054  return signalDone_.get_future();
2055  }
2056 
2057  void
2058  awaitBlocking(std::future<void> signal)
2059  {
2060  if (std::future_status::timeout == signal.wait_for (SAFETY_TIMEOUT))
2061  throw err::Fatal("Timeout on Scheduler test exceeded.");
2062  }
2063 
2064  Job
2065  calcJob (size_t idx, size_t level)
2066  {
2067  return Job{*calcFunctor_
2068  , calcFunctor_->encodeNodeID(idx)
2069  , calcFunctor_->encodeLevel(level)
2070  };
2071  }
2072 
2073  Job
2074  planningJob (size_t endNodeIDX)
2075  {
2076  return Job{*planFunctor_
2077  , planFunctor_->encodeNodeID(endNodeIDX)
2078  , Time::ANYTIME
2079  };
2080  }
2081 
2082  Job
2083  wakeUpJob ()
2084  {
2085  SpecialJobFun wakeUpFun{[this](JobParameter)
2086  {
2087  signalDone_.set_value();
2088  }};
2089  return Job{ wakeUpFun
2091  , Time::ANYTIME
2092  };
2093  }
2094 
2095  microseconds
2096  guessPlanningPreroll()
2097  {
2098  return microseconds(_raw(Time{chunkSize_ / planSpeed_}));
2099  }
2100 
2101  FrameRate
2102  calcLoadHint()
2103  {
2104  return FrameRate{levelSpeed_ * blockLoadFac_};
2105  }
2106 
2107  size_t
2108  calcNextChunkEnd (size_t lastNodeIDX)
2109  {
2110  lastNodeIDX += chunkSize_;
2111  return min (lastNodeIDX, chainLoad_.size()-1);
2112  } // prevent out-of-bound access
2113 
2114  Time
2115  anchorSchedule()
2116  {
2117  Time anchor = RealClock::now() + _uTicks(preRoll_);
2118  if (isnil (startTimes_))
2119  fillDefaultSchedule();
2120  size_t numPoints = chainLoad_.topLevel()+2;
2121  ENSURE (startTimes_.size() == numPoints);
2122  Offset base{startTimes_.front(), anchor};
2123  for (size_t level=0; level<numPoints; ++level)
2124  startTimes_[level] += base;
2125  return anchor;
2126  }
2127 
2128  void
2129  fillDefaultSchedule()
2130  {
2131  size_t numPoints = chainLoad_.topLevel()+2;
2132  stressFac_ = 1.0;
2133  startTimes_.clear();
2134  startTimes_.reserve (numPoints);
2135  for (size_t level=0; level<numPoints; ++level)
2136  startTimes_.push_back (level / levelSpeed_);
2137  }
2138 
2139  void
2140  fillAdaptedSchedule (double stressFact, uint concurrency)
2141  {
2142  REQUIRE (stressFact > 0);
2143  stressFac_ = stressFact;
2144  size_t numPoints = chainLoad_.topLevel()+2;
2145  startTimes_.clear();
2146  startTimes_.reserve (numPoints);
2147  startTimes_.push_back (Time::ZERO);
2148  chainLoad_.levelScheduleSequence (concurrency)
2149  .transform([&](double scheduleFact){ return (scheduleFact/stressFac_) * Offset{1,levelSpeed_};})
2150  .effuse(startTimes_);
2151  }
2152 
2153  Time
2154  jobStartTime (size_t level, size_t nodeIDX =0)
2155  {
2156  ENSURE (level < startTimes_.size());
2157  return startTimes_[level]
2158  + nodeExpense_ * (nodeIDX/stressFac_);
2159  }
2160 
2161  auto
2162  lastExitNodes (size_t lastChunkStartIDX)
2163  {
2164  return chainLoad_.allExitNodes()
2165  .transform([&](Node& n){ return chainLoad_.nodeID(n); })
2166  .filter([=](size_t idx){ return idx >= lastChunkStartIDX; });
2167  } // index of all Exit-Nodes within last planning-chunk...
2168 
2169  Time
2170  calcPlanScheduleTime (size_t lastNodeIDX)
2171  {/* must be at least 1 level ahead,
2172  because dependencies are defined backwards;
2173  the chain-load graph only defines dependencies over one level
2174  thus the first level in the next chunk must still be able to attach
2175  dependencies to the last row of the preceding chunk, implying that
2176  those still need to be ahead of schedule, and not yet dispatched.
2177  */
2178  lastNodeIDX = min (lastNodeIDX, chainLoad_.size()-1); // prevent out-of-bound access
2179  size_t nextChunkLevel = chainLoad_.nodes_[lastNodeIDX].level;
2180  nextChunkLevel = nextChunkLevel>2? nextChunkLevel-2 : 0;
2181  return jobStartTime(nextChunkLevel) - _uTicks(preRoll_);
2182  }
2183  };
2184 
2185 
2190  template<size_t maxFan>
2193  {
2194  clearNodeHashes();
2195  return ScheduleCtx{*this, scheduler};
2196  }
2197 
2198 
2199 
2200 }}} // namespace vault::gear::test
2201 #endif /*VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H*/
static const Time ANYTIME
border condition marker value. ANYTIME <= any time value
Definition: timevalue.hpp:322
Distribution indicators for one kind of evaluation.
a mutable time value, behaving like a plain number, allowing copy and re-accessing ...
Definition: timevalue.hpp:241
const double LOAD_SPEED_BASELINE
initial assumption for calculation speed (without calibration)
const StatKey STAT_NODE
all nodes
void setDependency(Node *pred, Node *succ)
Callback: define a dependency between scheduled jobs.
Table with connections to other Node records.
double calcExpectedCompoundedWeight(uint concurrency=1)
calculate the simplified/theoretic reduction of compounded weight through concurrency ...
double cLW
weight centre level width-reduced
const StatKey STAT_LINK
1:1 linking node
const StatKey STAT_SEED
seed node
TestChainLoad && configureShape_short_chains3_interleaved()
preconfigured topology: simple 3-step chains, starting interleaved
const Duration SCHEDULE_PLAN_STEP
time budget to reserve for each node to be planned and scheduled
const size_t LOAD_DEFAULT_MEM_SIZE
default allocation base size used if ComputationalLoad.useAllocation
const size_t LOAD_BENCHMARK_RUNS
repetition count for calibration benchmark for ComputationalLoad
Types marked with this mix-in may be moved and move-assigned.
Definition: nocopy.hpp:72
void seedCalcStream(Job planningJob, ManifestationID manID=ManifestationID(), FrameRate expectedAdditionalLoad=FrameRate(25))
Set the Scheduler to work on a new CalcStream.
Definition: scheduler.hpp:327
const StatKey STAT_KNOT
knot (joins and forks)
Automatically use custom string conversion in C++ stream output.
Support for generation of Graphviz-DOT code for structure visualisation.
const bool SCHED_NOTIFY
explicitly set notify dispatch time to the dependency&#39;s start time.
#define TRANSIENTLY(_OO_)
Macro to simplify capturing assignments.
static InvocationInstanceID encodeNodeID(size_t idx)
package the node-index to invoke.
Adaptor to handle further mapping functions.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
const StatKey STAT_WGHT
node weight
bool filter(Placement< DummyMO > const &candidate)
a filter predicate to pick some objects from a resultset.
AnyPair entry(Query< TY > const &query, typename WrapReturn< TY >::Wrapper &obj)
helper to simplify creating mock table entries, wrapped correctly
Definition: run.hpp:49
Framerate specified as frames per second.
Definition: timevalue.hpp:664
const bool SCHED_DEPENDS
explicitly schedule a dependent job (or rely on NOTIFY)
auto levelScheduleSequence(uint concurrency=1)
sequence of the summed compounded weight factors after each level
static Rule rule()
Abbreviation for starting rules.
const StatKey STAT_EXIT
exit node
const StatKey STAT_INNR
inner node
Types marked with this mix-in may be moved but not copied.
Definition: nocopy.hpp:58
const Offset SCHEDULE_WAKE_UP
tiny offset to place the final wake-up job behind any systematic schedule
Front-end for printf-style string template interpolation.
TestChainLoad && configure_isolated_nodes()
preconfigured topology: only unconnected seed/exit nodes
A configurable one-time job to invoke some special function.
ScheduleCtx && withAdaptedSchedule(double stressFac=1.0, uint concurrency=0, double formFac=1.0)
Establish a differentiated schedule per level, taking node weights into account.
Primary class template for std::hash.
ScheduleSpec post()
build Activity chain and hand-over to the Scheduler.
Definition: scheduler.hpp:546
size_t getHash() const
global hash is the combination of all exit node hashes != 0
double invoke(uint scaleStep=1)
cause a delay by computational load
TestChainLoad && buildTopology()
Use current configuration and seed to (re)build Node connectivity.
Functions to perform (multithreaded) timing measurement on a given functor.
void continuation(size_t chunkStart, size_t lastNodeIDX, size_t levelDone, bool work_left)
continue planning: schedule follow-up planning job
Front-end to configure a special job functor for one-time use.
A Result Value confined into fixed bounds.
A Generator for synthetic Render Jobs for Scheduler load testing.
A front-end for using printf-style formatting.
Record and evaluate concurrent activations.
TestChainLoad && setSeed(size_t seed=rand())
Set the overall seed value.
static const Duration NIL
constant to indicate "no duration"
Definition: timevalue.hpp:515
const size_t DEFAULT_CHUNKSIZE
number of computation jobs to prepare in each planning round
void invokeJobOperation(JobParameter param) override
render job invocation to trigger one batch of scheduling; the installed callback-λ should actually pl...
const Duration SCHEDULE_NODE_STEP
additional time step to include in the plan for each job (node).
ScheduleSpec defineSchedule(Job job)
Render Job builder: start definition of a schedule to invoke the given Job.
Definition: scheduler.hpp:366
double pS
average per segment
const auto SAFETY_TIMEOUT
maximum time limit for test run, abort if exceeded
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
auto microBenchmark(FUN const &testSubject, const size_t repeatCnt=DEFAULT_RUNS)
perform a simple looped microbenchmark.
std::future< void > attachNewCompletionSignal()
push away any existing wait state and attach new clean state
double benchmarkTime(FUN const &invokeTestCode, const size_t repeatCnt=1)
Helper to invoke a functor or λ to observe its running time.
ScheduleCtx setupSchedule(Scheduler &scheduler)
establish and configure the context used for scheduling computations.
double sL
weight centre on subgraph
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:199
JobKind
Definition: job.h:71
»Scheduler-Service« : coordinate render activities.
Definition: scheduler.hpp:222
static const FrameRate STEP
1 frame per second
Definition: timevalue.hpp:682
Service for coordination and dispatch of render activities.
Marker for current (and obsolete) manifestations of a CalcStream processed by the Render-Engine...
Definition: activity.hpp:93
TestChainLoad && recalculate()
Recalculate all node hashes and propagate seed value.
TestChainLoad && configureShape_short_chains2()
preconfigured topology: isolated simple 2-step chains
const size_t DEFAULT_SIZ
default node count for the complete load graph
void disposeStep(size_t idx, size_t level)
Callback: place a single job into the scheduler.
int reduce(Gtk::Button &icon)
attempt to reduce space consumption
size_t calcWeightSum()
overall sum of configured node weights
Render JobFunctor to invoke the calculation of a single Node.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
double computeWeightFactor(LevelWeight const &lw, uint concurrency)
simplified model for expense of a level of nodes, computed concurrently.
double calcRuntimeReference(microseconds timeBase=LOAD_DEFAULT_TIME, size_t sizeBase=0, size_t repeatCnt=GRAPH_BENCHMARK_RUNS)
Conduct a number of benchmark runs over processing the Graph synchronously.
std::string toString(TY const &val) noexcept
get some string representation of any object, reliably.
Definition: format-obj.hpp:200
double pL
average per level
Managed uninitialised Heap-allocated storage with array like access.
static size_t defaultSrc(Node *node)
by default use Node-hash directly as source of randomness
Baseclass: JobFunctor to invoke TestChainLoad.
TestChainLoad && configureShape_chain_loadBursts()
preconfigured topology: single graph with massive »load bursts«
test and diagnostic and research
Definition: job.h:76
const size_t DEFAULT_FAN
default maximum connectivity per Node
const StatKey STAT_JOIN
joining node
Statistic computeGraphStatistics()
Operator on TestChainLoad to evaluate current graph connectivity.
void continueMetaJob(Time nextStart, Job planningJob, ManifestationID manID=ManifestationID())
Place a follow-up job-planning job into the timeline.
Definition: scheduler.hpp:342
A raw memory block with proper alignment and array access.
Policy/Binding for generation of random parameters by »drawing« based on the node-hash.
Definition of a render job.
Test helper to perform temporary manipulations within a test scope.
A recorder for concurrent incidences.
opaque ID attached to each individual job invocation.
Definition: job.h:112
const StatKey STAT_FORK
forking node
Library functions to support the formation of grid-aligned time values.
Basic set of definitions and includes commonly used together (Vault).
size_t HashVal
a STL compatible hash value
Definition: hash-value.h:56
Interface of the closure for frame rendering jobs.
Definition: job.h:244
Offset measures a distance in time.
Definition: timevalue.hpp:367
Statistic data calculated for a given chain-load topology.
const auto STANDARD_DEADLINE
deadline to use for each individual computation job
double sLW
weight centre on subgraph width-reduced
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:477
NUM constexpr limited(NB lowerBound, NUM val, NB upperBound)
force a numeric to be within bounds, inclusively
Definition: util.hpp:101
const microseconds LOAD_DEFAULT_TIME
default time delay produced by ComputationalLoad at Node.weight==1
static double & computationSpeed(bool mem)
const double UPFRONT_PLANNING_BOOST
factor to increase the computed pre-roll to ensure up-front planning
Setup and wiring for a test run to schedule a computation structure as defined by this TestChainLoad ...
Accessing a STL element range through a Lumiera forward iterator, An instance of this iterator adapte...
SpecialJobFun onetimeCrunch(milliseconds runTime)
a »throw-away« render-job
Build a component to select limited values randomly.
static size_t COMPUTATION_CAPACITY
Nominal »full size« of a pool of concurrent workers.
Definition: work-force.hpp:115
TestChainLoad && clearNodeHashes()
Clear node hashes and propagate seed value.
TestChainLoad && configureShape_short_segments3_interleaved()
preconfigured topology: simple interwoven 3-step graph segments
double launch_and_wait()
dispose one complete run of the graph into the scheduler
double cL
weight centre level for this indicator
Statistic evaluate()
Visit all data captured thus far, construct an unified timeline and then compute statistics evaluatio...
Abstraction of a value alignment grid.
Definition: grid.hpp:67
Building tree expanding and backtracking evaluations within hierarchical scopes.
Render JobFunctor to perform chunk wise planning of Node jobs to calculate a complete Chain-Load grap...
Token to capture a value and restore original when leaving scope.
Definition: transiently.hpp:54
void invokeJobOperation(JobParameter param) override
render job invocation to trigger one Node recalculation
double frac
fraction of all nodes
Individual frame rendering task, forwarding to a closure.
Definition: job.h:277
a family of time value like entities and their relationships.
double pLW
average per level and level-width
const size_t GRAPH_BENCHMARK_RUNS
repetition count for reference calculation of a complete node graph
basic constant internal time value.
Definition: timevalue.hpp:142
const Duration SCHEDULE_LEVEL_STEP
time budget to plan for the calculation of each »time level« of jobs
typename _Fun< FUN >::Ret _FunRet
abbreviation for referring to a function&#39;s return type
Definition: function.hpp:196
Vault-Layer implementation namespace root.
auto allLevelWeights()
calculate node weights aggregated per level
TestChainLoad && performGraphSynchronously(microseconds timeBase=LOAD_DEFAULT_TIME, size_t sizeBase=0)
Emulate complete graph processing in a single threaded loop.
static size_t getDefaultComputationCapacity()
default value for full computing capacity is to use all (virtual) cores.
Definition: work-force.cpp:60
A calibratable CPU load to be invoked from a node job functor.
TestChainLoad && printTopologyStatistics()
Print a tabular summary of graph characteristics.
Simple stand-alone Quantiser implementation based on a constant sized gird.
Definition: quantiser.hpp:144
TestChainLoad && setWeight(size_t fixedNodeWeight=1)
Set a fixed weight for all nodes.
uint cnt
global sum over all levels